scratch-blocks 2.1.4 → 2.1.6

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 (188) hide show
  1. package/AGENTS.md +76 -24
  2. package/README.md +40 -0
  3. package/dist/main.mjs +1 -1
  4. package/dist/types/src/block_reporting.d.ts.map +1 -1
  5. package/dist/types/src/blocks/procedures.d.ts +2 -2
  6. package/dist/types/src/blocks/procedures.d.ts.map +1 -1
  7. package/dist/types/src/checkable_continuous_flyout.d.ts +6 -3
  8. package/dist/types/src/checkable_continuous_flyout.d.ts.map +1 -1
  9. package/dist/types/src/checkbox_bubble.d.ts +7 -7
  10. package/dist/types/src/checkbox_bubble.d.ts.map +1 -1
  11. package/dist/types/src/colours.d.ts.map +1 -1
  12. package/dist/types/src/context_menu_items.d.ts.map +1 -1
  13. package/dist/types/src/events/events_block_comment_base.d.ts +1 -1
  14. package/dist/types/src/events/events_block_comment_base.d.ts.map +1 -1
  15. package/dist/types/src/events/events_block_drag_end.d.ts +1 -1
  16. package/dist/types/src/events/events_block_drag_end.d.ts.map +1 -1
  17. package/dist/types/src/events/events_block_drag_outside.d.ts +1 -1
  18. package/dist/types/src/events/events_block_drag_outside.d.ts.map +1 -1
  19. package/dist/types/src/fields/field_colour_slider.d.ts.map +1 -1
  20. package/dist/types/src/fields/field_matrix.d.ts.map +1 -1
  21. package/dist/types/src/fields/field_note.d.ts +6 -4
  22. package/dist/types/src/fields/field_note.d.ts.map +1 -1
  23. package/dist/types/src/fields/field_textinput_removable.d.ts.map +1 -1
  24. package/dist/types/src/fields/field_variable_getter.d.ts.map +1 -1
  25. package/dist/types/src/fields/field_vertical_separator.d.ts.map +1 -1
  26. package/dist/types/src/fields/scratch_field_angle.d.ts.map +1 -1
  27. package/dist/types/src/fields/scratch_field_dropdown.d.ts.map +1 -1
  28. package/dist/types/src/fields/scratch_field_number.d.ts.map +1 -1
  29. package/dist/types/src/fields/scratch_field_variable.d.ts +1 -0
  30. package/dist/types/src/fields/scratch_field_variable.d.ts.map +1 -1
  31. package/dist/types/src/flyout_checkbox_icon.d.ts +5 -5
  32. package/dist/types/src/flyout_checkbox_icon.d.ts.map +1 -1
  33. package/dist/types/src/glows.d.ts.map +1 -1
  34. package/dist/types/src/index.d.ts +1 -0
  35. package/dist/types/src/index.d.ts.map +1 -1
  36. package/dist/types/src/procedures.d.ts +4 -4
  37. package/dist/types/src/procedures.d.ts.map +1 -1
  38. package/dist/types/src/recyclable_block_flyout_inflater.d.ts +2 -2
  39. package/dist/types/src/recyclable_block_flyout_inflater.d.ts.map +1 -1
  40. package/dist/types/src/renderer/cat/cat_face.d.ts +1 -1
  41. package/dist/types/src/renderer/cat/cat_face.d.ts.map +1 -1
  42. package/dist/types/src/renderer/cat/drawer.d.ts.map +1 -1
  43. package/dist/types/src/renderer/constants.d.ts.map +1 -1
  44. package/dist/types/src/renderer/drawer.d.ts.map +1 -1
  45. package/dist/types/src/renderer/render_info.d.ts.map +1 -1
  46. package/dist/types/src/scratch_blocks_utils.d.ts +22 -0
  47. package/dist/types/src/scratch_blocks_utils.d.ts.map +1 -1
  48. package/dist/types/src/scratch_c_block_wrap.d.ts +2 -0
  49. package/dist/types/src/scratch_c_block_wrap.d.ts.map +1 -0
  50. package/dist/types/src/scratch_comment_bubble.d.ts +4 -4
  51. package/dist/types/src/scratch_comment_bubble.d.ts.map +1 -1
  52. package/dist/types/src/scratch_comment_icon.d.ts +1 -1
  53. package/dist/types/src/scratch_comment_icon.d.ts.map +1 -1
  54. package/dist/types/src/scratch_continuous_category.d.ts +3 -1
  55. package/dist/types/src/scratch_continuous_category.d.ts.map +1 -1
  56. package/dist/types/src/scratch_continuous_toolbox.d.ts +2 -1
  57. package/dist/types/src/scratch_continuous_toolbox.d.ts.map +1 -1
  58. package/dist/types/src/status_indicator_label.d.ts +3 -3
  59. package/dist/types/src/status_indicator_label.d.ts.map +1 -1
  60. package/dist/types/src/status_indicator_label_flyout_inflater.d.ts.map +1 -1
  61. package/dist/types/src/variables.d.ts +1 -1
  62. package/dist/types/src/variables.d.ts.map +1 -1
  63. package/dist/types/src/workspace_block_lookup.d.ts +4 -0
  64. package/dist/types/src/workspace_block_lookup.d.ts.map +1 -0
  65. package/eslint.config.mjs +23 -26
  66. package/package.json +10 -3
  67. package/src/block_reporting.ts +5 -5
  68. package/src/blocks/control.ts +5 -5
  69. package/src/blocks/event.ts +1 -1
  70. package/src/blocks/motion.ts +2 -2
  71. package/src/blocks/procedures.ts +162 -69
  72. package/src/blocks/sensing.ts +0 -1
  73. package/src/blocks/vertical_extensions.ts +11 -8
  74. package/src/checkable_continuous_flyout.ts +45 -12
  75. package/src/checkbox_bubble.ts +7 -7
  76. package/src/colours.ts +4 -2
  77. package/src/context_menu_items.ts +41 -16
  78. package/src/data_category.ts +11 -3
  79. package/src/events/events_block_comment_base.ts +5 -1
  80. package/src/events/events_block_comment_change.ts +5 -1
  81. package/src/events/events_block_comment_collapse.ts +6 -2
  82. package/src/events/events_block_comment_create.ts +5 -1
  83. package/src/events/events_block_comment_move.ts +6 -2
  84. package/src/events/events_block_comment_resize.ts +6 -2
  85. package/src/events/events_block_drag_end.ts +5 -1
  86. package/src/events/events_block_drag_outside.ts +5 -1
  87. package/src/events/events_scratch_variable_create.ts +5 -1
  88. package/src/fields/field_colour_slider.ts +3 -5
  89. package/src/fields/field_matrix.ts +33 -17
  90. package/src/fields/field_note.ts +56 -20
  91. package/src/fields/field_textinput_removable.ts +13 -4
  92. package/src/fields/field_variable_getter.ts +20 -6
  93. package/src/fields/field_vertical_separator.ts +5 -1
  94. package/src/fields/scratch_field_angle.ts +32 -21
  95. package/src/fields/scratch_field_dropdown.ts +6 -2
  96. package/src/fields/scratch_field_number.ts +22 -13
  97. package/src/fields/scratch_field_variable.ts +26 -12
  98. package/src/flyout_checkbox_icon.ts +9 -5
  99. package/src/glows.ts +5 -5
  100. package/src/index.ts +21 -11
  101. package/src/procedures.ts +92 -42
  102. package/src/recyclable_block_flyout_inflater.ts +5 -4
  103. package/src/renderer/cat/cat_face.ts +1 -1
  104. package/src/renderer/cat/drawer.ts +4 -1
  105. package/src/renderer/constants.ts +19 -14
  106. package/src/renderer/drawer.ts +2 -1
  107. package/src/renderer/render_info.ts +12 -9
  108. package/src/renderer/renderer.ts +1 -1
  109. package/src/scratch_blocks_utils.ts +0 -2
  110. package/src/scratch_c_block_wrap.ts +108 -0
  111. package/src/scratch_comment_bubble.ts +30 -19
  112. package/src/scratch_comment_icon.ts +9 -12
  113. package/src/scratch_connection_checker.ts +1 -2
  114. package/src/scratch_continuous_category.ts +20 -11
  115. package/src/scratch_continuous_toolbox.ts +12 -3
  116. package/src/scratch_dragger.ts +2 -2
  117. package/src/scratch_variable_map.ts +1 -1
  118. package/src/status_indicator_label.ts +13 -9
  119. package/src/status_indicator_label_flyout_inflater.ts +2 -1
  120. package/src/variables.ts +21 -14
  121. package/src/workspace_block_lookup.ts +14 -0
  122. package/src/xml.ts +1 -1
  123. package/tsconfig.build.json +4 -0
  124. package/tsconfig.json +1 -1
  125. package/vitest.config.ts +35 -0
  126. package/dist/types/tests/blocks/logic_ternary_test.d.ts +0 -13
  127. package/dist/types/tests/blocks/logic_ternary_test.d.ts.map +0 -1
  128. package/dist/types/tests/jsunit/block_test.d.ts +0 -4
  129. package/dist/types/tests/jsunit/block_test.d.ts.map +0 -1
  130. package/dist/types/tests/jsunit/connection_db_test.d.ts +0 -25
  131. package/dist/types/tests/jsunit/connection_db_test.d.ts.map +0 -1
  132. package/dist/types/tests/jsunit/connection_test.d.ts +0 -39
  133. package/dist/types/tests/jsunit/connection_test.d.ts.map +0 -1
  134. package/dist/types/tests/jsunit/db_test.d.ts +0 -7
  135. package/dist/types/tests/jsunit/db_test.d.ts.map +0 -1
  136. package/dist/types/tests/jsunit/event_test.d.ts +0 -76
  137. package/dist/types/tests/jsunit/event_test.d.ts.map +0 -1
  138. package/dist/types/tests/jsunit/extensions_test.d.ts +0 -18
  139. package/dist/types/tests/jsunit/extensions_test.d.ts.map +0 -1
  140. package/dist/types/tests/jsunit/field_angle_test.d.ts +0 -3
  141. package/dist/types/tests/jsunit/field_angle_test.d.ts.map +0 -1
  142. package/dist/types/tests/jsunit/field_number_test.d.ts +0 -3
  143. package/dist/types/tests/jsunit/field_number_test.d.ts.map +0 -1
  144. package/dist/types/tests/jsunit/field_test.d.ts +0 -8
  145. package/dist/types/tests/jsunit/field_test.d.ts.map +0 -1
  146. package/dist/types/tests/jsunit/field_variable_getter_test.d.ts +0 -5
  147. package/dist/types/tests/jsunit/field_variable_getter_test.d.ts.map +0 -1
  148. package/dist/types/tests/jsunit/field_variable_test.d.ts +0 -19
  149. package/dist/types/tests/jsunit/field_variable_test.d.ts.map +0 -1
  150. package/dist/types/tests/jsunit/generator_test.d.ts +0 -2
  151. package/dist/types/tests/jsunit/generator_test.d.ts.map +0 -1
  152. package/dist/types/tests/jsunit/gesture_test.d.ts +0 -10
  153. package/dist/types/tests/jsunit/gesture_test.d.ts.map +0 -1
  154. package/dist/types/tests/jsunit/input_test.d.ts +0 -9
  155. package/dist/types/tests/jsunit/input_test.d.ts.map +0 -1
  156. package/dist/types/tests/jsunit/json_test.d.ts +0 -11
  157. package/dist/types/tests/jsunit/json_test.d.ts.map +0 -1
  158. package/dist/types/tests/jsunit/names_test.d.ts +0 -5
  159. package/dist/types/tests/jsunit/names_test.d.ts.map +0 -1
  160. package/dist/types/tests/jsunit/procedure_test.d.ts +0 -15
  161. package/dist/types/tests/jsunit/procedure_test.d.ts.map +0 -1
  162. package/dist/types/tests/jsunit/scratch_block_comment_test.d.ts +0 -14
  163. package/dist/types/tests/jsunit/scratch_block_comment_test.d.ts.map +0 -1
  164. package/dist/types/tests/jsunit/svg_test.d.ts +0 -14
  165. package/dist/types/tests/jsunit/svg_test.d.ts.map +0 -1
  166. package/dist/types/tests/jsunit/test_runner.d.ts +0 -2
  167. package/dist/types/tests/jsunit/test_runner.d.ts.map +0 -1
  168. package/dist/types/tests/jsunit/test_utilities.d.ts +0 -50
  169. package/dist/types/tests/jsunit/test_utilities.d.ts.map +0 -1
  170. package/dist/types/tests/jsunit/utils_test.d.ts +0 -10
  171. package/dist/types/tests/jsunit/utils_test.d.ts.map +0 -1
  172. package/dist/types/tests/jsunit/variable_map_test.d.ts +0 -28
  173. package/dist/types/tests/jsunit/variable_map_test.d.ts.map +0 -1
  174. package/dist/types/tests/jsunit/variable_model_test.d.ts +0 -14
  175. package/dist/types/tests/jsunit/variable_model_test.d.ts.map +0 -1
  176. package/dist/types/tests/jsunit/widget_div_test.d.ts +0 -37
  177. package/dist/types/tests/jsunit/widget_div_test.d.ts.map +0 -1
  178. package/dist/types/tests/jsunit/workspace_comment_test.d.ts +0 -13
  179. package/dist/types/tests/jsunit/workspace_comment_test.d.ts.map +0 -1
  180. package/dist/types/tests/jsunit/workspace_test.d.ts +0 -22
  181. package/dist/types/tests/jsunit/workspace_test.d.ts.map +0 -1
  182. package/dist/types/tests/jsunit/workspace_undo_redo_test.d.ts +0 -33
  183. package/dist/types/tests/jsunit/workspace_undo_redo_test.d.ts.map +0 -1
  184. package/dist/types/tests/jsunit/xml_test.d.ts +0 -55
  185. package/dist/types/tests/jsunit/xml_test.d.ts.map +0 -1
  186. package/dist/types/tests/workspace_svg/workspace_svg_test.d.ts +0 -12
  187. package/dist/types/tests/workspace_svg/workspace_svg_test.d.ts.map +0 -1
  188. package/types/continuous-toolbox.d.ts +0 -1
@@ -29,8 +29,8 @@ import type { ScratchDragger } from '../scratch_dragger'
29
29
  type ConnectionMap = Record<
30
30
  string,
31
31
  {
32
- shadow: Element
33
- block: Blockly.BlockSvg
32
+ shadow: Element | undefined
33
+ block: Blockly.BlockSvg | null
34
34
  } | null
35
35
  >
36
36
 
@@ -43,6 +43,86 @@ enum ArgumentType {
43
43
  BOOLEAN = 'b',
44
44
  }
45
45
 
46
+ /**
47
+ * Parse a serialized procedure argument type token.
48
+ * @param value Serialized token from procCode.
49
+ * @returns The matching argument type.
50
+ */
51
+ function parseArgumentType(value: string): ArgumentType {
52
+ switch (value) {
53
+ case 'n':
54
+ return ArgumentType.NUMBER
55
+ case 'b':
56
+ return ArgumentType.BOOLEAN
57
+ case 's':
58
+ return ArgumentType.STRING
59
+ default:
60
+ throw new Error(`Found a custom procedure with an invalid type: ${value}`)
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Read a required mutation attribute or throw if missing.
66
+ * @param xmlElement The mutation element that should contain required attributes.
67
+ * @param name The specific mutation attribute to retrieve.
68
+ * @returns Attribute value.
69
+ */
70
+ function getRequiredMutationAttribute(xmlElement: Element, name: string): string {
71
+ const value = xmlElement.getAttribute(name)
72
+ if (value === null) {
73
+ throw new Error(`Missing required mutation attribute: ${name}`)
74
+ }
75
+ return value
76
+ }
77
+
78
+ /**
79
+ * Parse a required mutation attribute as JSON, then validate its type.
80
+ * @param xmlElement The mutation element that should contain required attributes.
81
+ * @param name The specific mutation attribute to retrieve and parse.
82
+ * @param parse Validates and narrows the parsed JSON value.
83
+ * @returns Parsed and validated mutation attribute value.
84
+ */
85
+ function parseRequiredMutationJson<T>(
86
+ xmlElement: Element,
87
+ name: string,
88
+ parse: (value: unknown, name: string) => T,
89
+ ): T {
90
+ const rawValue = getRequiredMutationAttribute(xmlElement, name)
91
+ let parsedValue: unknown
92
+ try {
93
+ parsedValue = JSON.parse(rawValue)
94
+ } catch {
95
+ throw new Error(`Invalid JSON in mutation attribute: ${name}`)
96
+ }
97
+ return parse(parsedValue, name)
98
+ }
99
+
100
+ /**
101
+ * Validate a parsed mutation value as a boolean.
102
+ * @param value Parsed mutation value.
103
+ * @param name Attribute name used in error messages.
104
+ * @returns Validated boolean value.
105
+ */
106
+ function parseBooleanMutationValue(value: unknown, name: string): boolean {
107
+ if (typeof value !== 'boolean') {
108
+ throw new Error(`Expected boolean JSON value in mutation attribute: ${name}`)
109
+ }
110
+ return value
111
+ }
112
+
113
+ /**
114
+ * Validate a parsed mutation value as a string array.
115
+ * @param value Parsed mutation value.
116
+ * @param name Attribute name used in error messages.
117
+ * @returns Validated string array value.
118
+ */
119
+ function parseStringArrayMutationValue(value: unknown, name: string): string[] {
120
+ if (!Array.isArray(value) || !value.every((entry) => typeof entry === 'string')) {
121
+ throw new Error(`Expected string[] JSON value in mutation attribute: ${name}`)
122
+ }
123
+ return value
124
+ }
125
+
46
126
  /**
47
127
  * A drag strategy for the procedures_prototype block that delegates all drag
48
128
  * operations to its parent (the procedures_definition block). This lets the
@@ -193,10 +273,10 @@ function callerMutationToDom(this: ProcedureCallBlock): Element {
193
273
  * @param xmlElement XML storage element.
194
274
  */
195
275
  function callerDomToMutation(this: ProcedureCallBlock, xmlElement: Element) {
196
- this.procCode_ = xmlElement.getAttribute('proccode')!
197
- this.generateShadows_ = JSON.parse(xmlElement.getAttribute('generateshadows')!)
198
- this.argumentIds_ = JSON.parse(xmlElement.getAttribute('argumentids')!)
199
- this.warp_ = JSON.parse(xmlElement.getAttribute('warp')!)
276
+ this.procCode_ = getRequiredMutationAttribute(xmlElement, 'proccode')
277
+ this.generateShadows_ = parseRequiredMutationJson(xmlElement, 'generateshadows', parseBooleanMutationValue)
278
+ this.argumentIds_ = parseRequiredMutationJson(xmlElement, 'argumentids', parseStringArrayMutationValue)
279
+ this.warp_ = parseRequiredMutationJson(xmlElement, 'warp', parseBooleanMutationValue)
200
280
  this.updateDisplay_()
201
281
  }
202
282
 
@@ -230,15 +310,15 @@ function definitionMutationToDom(
230
310
  * @param xmlElement XML storage element.
231
311
  */
232
312
  function definitionDomToMutation(this: ProcedurePrototypeBlock | ProcedureDeclarationBlock, xmlElement: Element) {
233
- this.procCode_ = xmlElement.getAttribute('proccode')!
234
- this.warp_ = JSON.parse(xmlElement.getAttribute('warp')!)
313
+ this.procCode_ = getRequiredMutationAttribute(xmlElement, 'proccode')
314
+ this.warp_ = parseRequiredMutationJson(xmlElement, 'warp', parseBooleanMutationValue)
235
315
 
236
316
  const prevArgIds = this.argumentIds_
237
317
  const prevDisplayNames = this.displayNames_
238
318
 
239
- this.argumentIds_ = JSON.parse(xmlElement.getAttribute('argumentids')!)
240
- this.displayNames_ = JSON.parse(xmlElement.getAttribute('argumentnames')!)
241
- this.argumentDefaults_ = JSON.parse(xmlElement.getAttribute('argumentdefaults')!)
319
+ this.argumentIds_ = parseRequiredMutationJson(xmlElement, 'argumentids', parseStringArrayMutationValue)
320
+ this.displayNames_ = parseRequiredMutationJson(xmlElement, 'argumentnames', parseStringArrayMutationValue)
321
+ this.argumentDefaults_ = parseRequiredMutationJson(xmlElement, 'argumentdefaults', parseStringArrayMutationValue)
242
322
 
243
323
  // During full XML deserialization (Blockly.Xml.domToWorkspace), the mutation element
244
324
  // is part of the parsed XML tree and its parent element also contains <value> children
@@ -308,13 +388,11 @@ function disconnectOldBlocks_(this: ProcedureBlock): ConnectionMap {
308
388
  const connectionMap: ConnectionMap = {}
309
389
  for (const input of this.inputList) {
310
390
  if (input.connection) {
311
- const target = input.connection.targetBlock() as Blockly.BlockSvg
312
- const saveInfo = {
313
- shadow: input.connection.getShadowDom(true)!,
314
- block: target,
391
+ const target = input.connection.targetBlock()
392
+ connectionMap[input.name] = {
393
+ shadow: input.connection.getShadowDom(true) ?? undefined,
394
+ block: target as Blockly.BlockSvg | null,
315
395
  }
316
- connectionMap[input.name] = saveInfo
317
-
318
396
  if (target) {
319
397
  input.connection.disconnect()
320
398
  }
@@ -349,16 +427,7 @@ function createAllInputs_(this: ProcedureBlock, connectionMap: ConnectionMap) {
349
427
  for (const component of procComponents) {
350
428
  let labelText
351
429
  if (component.startsWith('%')) {
352
- const argumentType = component.substring(1, 2)
353
- if (
354
- !(
355
- argumentType === ArgumentType.NUMBER ||
356
- argumentType === ArgumentType.BOOLEAN ||
357
- argumentType === ArgumentType.STRING
358
- )
359
- ) {
360
- throw new Error('Found an custom procedure with an invalid type: ' + argumentType)
361
- }
430
+ const argumentType = parseArgumentType(component.substring(1, 2))
362
431
  labelText = component.substring(2).trim()
363
432
 
364
433
  const id = this.argumentIds_[argumentCount]
@@ -386,19 +455,20 @@ function createAllInputs_(this: ProcedureBlock, connectionMap: ConnectionMap) {
386
455
  * connected to those IDs at the beginning of the mutation.
387
456
  */
388
457
  function disposeObsoleteBlocks_(this: ProcedureBlock, connectionMap: ConnectionMap) {
389
- for (const id in connectionMap) {
390
- const saveInfo = connectionMap[id]
391
- if (saveInfo) {
392
- const block = saveInfo.block
393
- const isOrphanedArgumentReporter =
394
- this.type === 'procedures_prototype' &&
395
- (block.type === 'argument_reporter_string_number' || block.type === 'argument_reporter_boolean')
396
- if (block.isShadow() || isOrphanedArgumentReporter) {
397
- block.dispose()
398
- connectionMap[id] = null
399
- // At this point we know which shadow DOMs are about to be orphaned in
400
- // the VM. What do we do with that information?
401
- }
458
+ for (const [id, saveInfo] of Object.entries(connectionMap)) {
459
+ const block = saveInfo?.block
460
+ if (!block) {
461
+ continue
462
+ }
463
+
464
+ const isOrphanedArgumentReporter =
465
+ this.type === 'procedures_prototype' &&
466
+ (block.type === 'argument_reporter_string_number' || block.type === 'argument_reporter_boolean')
467
+ if (block.isShadow() || isOrphanedArgumentReporter) {
468
+ block.dispose()
469
+ connectionMap[id] = null
470
+ // At this point we know which shadow DOMs are about to be orphaned in
471
+ // the VM. What do we do with that information?
402
472
  }
403
473
  }
404
474
  }
@@ -458,6 +528,9 @@ function buildShadowDom_(type: ArgumentType): Element {
458
528
  */
459
529
  function attachShadow_(this: ProcedureCallBlock, input: Blockly.Input, argumentType: ArgumentType) {
460
530
  if (argumentType === ArgumentType.NUMBER || argumentType === ArgumentType.STRING) {
531
+ if (!input.connection) {
532
+ throw new Error(`Expected input connection for argument ${String(argumentType)}`)
533
+ }
461
534
  const blockType = argumentType === ArgumentType.NUMBER ? 'math_number' : 'text'
462
535
  Blockly.Events.disable()
463
536
  let newBlock
@@ -479,7 +552,7 @@ function attachShadow_(this: ProcedureCallBlock, input: Blockly.Input, argumentT
479
552
  if (Blockly.Events.isEnabled()) {
480
553
  Blockly.Events.fire(new (Blockly.Events.get(Blockly.Events.BLOCK_CREATE))(newBlock))
481
554
  }
482
- newBlock.outputConnection.connect(input.connection!)
555
+ newBlock.outputConnection.connect(input.connection)
483
556
  }
484
557
  }
485
558
 
@@ -516,7 +589,7 @@ function createArgumentReporter_(
516
589
  Blockly.Events.enable()
517
590
  }
518
591
  if (Blockly.Events.isEnabled()) {
519
- Blockly.Events.fire(new (Blockly.Events.get(Blockly.Events.BLOCK_CREATE))(newBlock))
592
+ void Blockly.Events.fire(new (Blockly.Events.get(Blockly.Events.BLOCK_CREATE))(newBlock))
520
593
  }
521
594
  return newBlock
522
595
  }
@@ -538,21 +611,25 @@ function populateArgumentOnCaller_(
538
611
  id: string,
539
612
  input: Blockly.Input,
540
613
  ) {
541
- let oldBlock: Blockly.BlockSvg | undefined
614
+ let oldBlock: Blockly.BlockSvg | null | undefined
542
615
  let oldShadow: Element | undefined
543
- if (connectionMap && id in connectionMap) {
616
+ if (id in connectionMap) {
544
617
  const saveInfo = connectionMap[id]
545
618
  oldBlock = saveInfo?.block
546
619
  oldShadow = saveInfo?.shadow
547
620
  }
548
621
 
549
- if (connectionMap && oldBlock) {
622
+ const conn = input.connection
623
+ if (!conn) {
624
+ throw new Error(`Expected caller input connection for argument id ${id}`)
625
+ }
626
+ if (oldBlock) {
550
627
  // Reattach the old block and shadow DOM.
551
628
  connectionMap[input.name] = null
552
- oldBlock.outputConnection.connect(input.connection!)
629
+ oldBlock.outputConnection.connect(conn)
553
630
  if (type !== ArgumentType.BOOLEAN && this.generateShadows_) {
554
- const shadowDom = oldShadow || this.buildShadowDom_(type)
555
- input.connection!.setShadowDom(shadowDom)
631
+ const shadowDom = oldShadow ?? this.buildShadowDom_(type)
632
+ conn.setShadowDom(shadowDom)
556
633
  }
557
634
  } else if (this.generateShadows_) {
558
635
  this.attachShadow_(input, type)
@@ -584,7 +661,12 @@ function populateArgumentOnPrototype_(
584
661
  }
585
662
 
586
663
  let oldBlock: Blockly.BlockSvg | null = null
587
- if (connectionMap && id in connectionMap) {
664
+ const conn = input.connection
665
+ if (!conn) {
666
+ throw new Error(`Expected prototype input connection for argument id ${id}`)
667
+ }
668
+
669
+ if (id in connectionMap) {
588
670
  const saveInfo = connectionMap[id]
589
671
  oldBlock = saveInfo?.block ?? null
590
672
  }
@@ -594,7 +676,7 @@ function populateArgumentOnPrototype_(
594
676
 
595
677
  // Decide which block to attach.
596
678
  let argumentReporter: Blockly.BlockSvg
597
- if (connectionMap && oldBlock && oldTypeMatches) {
679
+ if (oldBlock && oldTypeMatches) {
598
680
  // Update the text if needed. The old argument reporter is the same type,
599
681
  // and on the same input, but the argument's display name may have changed.
600
682
  argumentReporter = oldBlock
@@ -605,7 +687,7 @@ function populateArgumentOnPrototype_(
605
687
  }
606
688
 
607
689
  // Attach the block.
608
- input.connection!.connect(argumentReporter.outputConnection)
690
+ conn.connect(argumentReporter.outputConnection)
609
691
  }
610
692
 
611
693
  /**
@@ -627,13 +709,17 @@ function populateArgumentOnDeclaration_(
627
709
  input: Blockly.Input,
628
710
  ) {
629
711
  let oldBlock: Blockly.BlockSvg | null = null
630
- if (connectionMap && id in connectionMap) {
712
+ if (id in connectionMap) {
631
713
  const saveInfo = connectionMap[id]
632
714
  oldBlock = saveInfo?.block ?? null
633
715
  }
634
716
 
635
717
  const oldTypeMatches = checkOldEditorTypeMatches_(oldBlock, type)
636
718
  const displayName = this.displayNames_[index]
719
+ const conn = input.connection
720
+ if (!conn) {
721
+ throw new Error(`Expected declaration input connection for argument id ${id}`)
722
+ }
637
723
 
638
724
  // Decide which block to attach.
639
725
  let argumentEditor: Blockly.BlockSvg
@@ -646,7 +732,7 @@ function populateArgumentOnDeclaration_(
646
732
  }
647
733
 
648
734
  // Attach the block.
649
- input.connection!.connect(argumentEditor.outputConnection)
735
+ conn.connect(argumentEditor.outputConnection)
650
736
  }
651
737
 
652
738
  /**
@@ -721,13 +807,13 @@ function createArgumentEditor_(
721
807
  newBlock.setShadow(true)
722
808
  if (!this.isInsertionMarker()) {
723
809
  newBlock.initSvg()
724
- newBlock.queueRender()
810
+ void newBlock.queueRender()
725
811
  }
726
812
  } finally {
727
813
  Blockly.Events.enable()
728
814
  }
729
815
  if (Blockly.Events.isEnabled()) {
730
- Blockly.Events.fire(new (Blockly.Events.get(Blockly.Events.BLOCK_CREATE))(newBlock))
816
+ void Blockly.Events.fire(new (Blockly.Events.get(Blockly.Events.BLOCK_CREATE))(newBlock))
731
817
  }
732
818
  return newBlock
733
819
  }
@@ -749,8 +835,11 @@ function updateDeclarationProcCode_(this: ProcedureDeclarationBlock) {
749
835
  this.procCode_ += input.fieldRow[0].getValue()
750
836
  } else if (input.type === Blockly.inputs.inputTypes.VALUE) {
751
837
  // Inspect the argument editor.
752
- const target = input.connection!.targetBlock()!
753
- this.displayNames_.push(target.getFieldValue('TEXT'))
838
+ const target = input.connection?.targetBlock()
839
+ if (!target) {
840
+ throw new Error(`Expected argument editor block on input ${input.name}`)
841
+ }
842
+ this.displayNames_.push(String(target.getFieldValue('TEXT')))
754
843
  this.argumentIds_.push(input.name)
755
844
  if (target.type === 'argument_editor_boolean') {
756
845
  this.procCode_ += '%b'
@@ -758,7 +847,7 @@ function updateDeclarationProcCode_(this: ProcedureDeclarationBlock) {
758
847
  this.procCode_ += '%s'
759
848
  }
760
849
  } else {
761
- throw new Error('Unexpected input type on a procedure mutator root: ' + input.type)
850
+ throw new Error(`Unexpected input type on a procedure mutator root: ${String(input.type)}`)
762
851
  }
763
852
  }
764
853
  }
@@ -773,8 +862,15 @@ function focusLastEditor_(this: ProcedureDeclarationBlock) {
773
862
  newInput.fieldRow[0].showEditor()
774
863
  } else if (newInput.type === Blockly.inputs.inputTypes.VALUE) {
775
864
  // Inspect the argument editor.
776
- const target = newInput.connection!.targetBlock()!
777
- target.getField('TEXT')!.showEditor()
865
+ const target = newInput.connection?.targetBlock()
866
+ if (!target) {
867
+ throw new Error(`Expected argument editor block on input ${newInput.name}`)
868
+ }
869
+ const field = target.getField('TEXT')
870
+ if (!field) {
871
+ throw new Error(`Expected TEXT field on argument editor block ${target.id}`)
872
+ }
873
+ field.showEditor()
778
874
  }
779
875
  }
780
876
  }
@@ -843,16 +939,15 @@ function removeFieldCallback(this: ProcedureDeclarationBlock, field: Blockly.Fie
843
939
  return
844
940
  }
845
941
  let inputNameToRemove = null
846
- for (let n = 0; n < this.inputList.length; n++) {
847
- const input = this.inputList[n]
942
+ for (const input of this.inputList) {
848
943
  if (input.connection) {
849
- const target = input.connection.targetBlock()!
850
- if (field.name && target.getField(field.name) === field) {
944
+ const target = input.connection.targetBlock()
945
+ if (target && field.name && target.getField(field.name) === field) {
851
946
  inputNameToRemove = input.name
852
947
  }
853
948
  } else {
854
- for (let j = 0; j < input.fieldRow.length; j++) {
855
- if (input.fieldRow[j] === field) {
949
+ for (const inputField of input.fieldRow) {
950
+ if (inputField === field) {
856
951
  inputNameToRemove = input.name
857
952
  }
858
953
  }
@@ -875,9 +970,7 @@ function removeArgumentCallback_(
875
970
  field: Blockly.Field,
876
971
  ) {
877
972
  const parent = this.getParent()
878
- if (parent && parent.removeFieldCallback) {
879
- parent.removeFieldCallback(field)
880
- }
973
+ ;(parent as ProcedureDeclarationBlock | null)?.removeFieldCallback(field)
881
974
  }
882
975
 
883
976
  /**
@@ -17,7 +17,6 @@
17
17
  * limitations under the License.
18
18
  */
19
19
  import * as Blockly from 'blockly/core'
20
- import * as Constants from '../constants'
21
20
 
22
21
  Blockly.Blocks.sensing_touchingobject = {
23
22
  /**
@@ -156,14 +156,14 @@ const OUTPUT_BOOLEAN = function (this: Blockly.Block) {
156
156
  */
157
157
  const MONITOR_BLOCK = function (this: Blockly.BlockSvg) {
158
158
  this.addIcon(new FlyoutCheckboxIcon(this))
159
- ;(this as any).checkboxInFlyout = true
159
+ ;(this as Blockly.BlockSvg & { checkboxInFlyout?: boolean }).checkboxInFlyout = true
160
160
  }
161
161
 
162
162
  /**
163
163
  * Mixin to add a context menu for a procedure definition block.
164
164
  * It adds the "edit" option and removes the "duplicate" option.
165
165
  */
166
- const PROCEDURE_DEF_CONTEXTMENU = function (this: Blockly.Block) {
166
+ const PROCEDURE_DEF_CONTEXTMENU = function (this: Blockly.BlockSvg) {
167
167
  /**
168
168
  * Add the "edit" option and removes the "duplicate" option from the context
169
169
  * menu.
@@ -172,6 +172,7 @@ const PROCEDURE_DEF_CONTEXTMENU = function (this: Blockly.Block) {
172
172
  this.mixin(
173
173
  {
174
174
  customContextMenu: function (
175
+ this: Blockly.BlockSvg,
175
176
  menuOptions: (
176
177
  | Blockly.ContextMenuRegistry.ContextMenuOption
177
178
  | Blockly.ContextMenuRegistry.LegacyContextMenuOption
@@ -181,18 +182,20 @@ const PROCEDURE_DEF_CONTEXTMENU = function (this: Blockly.Block) {
181
182
  menuOptions.push(ScratchProcedures.makeEditOption(this))
182
183
 
183
184
  // Find and remove the duplicate option
184
- for (let i = 0, option; (option = menuOptions[i]); i++) {
185
- if (option.text == Blockly.Msg.DUPLICATE_BLOCK) {
185
+ for (let i = 0; i < menuOptions.length; i++) {
186
+ if (menuOptions[i].text == Blockly.Msg.DUPLICATE_BLOCK) {
186
187
  menuOptions.splice(i, 1)
187
188
  break
188
189
  }
189
190
  }
190
191
  },
191
- checkAndDelete: function () {
192
+ checkAndDelete: function (this: Blockly.BlockSvg) {
192
193
  const input = this.getInput('custom_block')
193
194
  // this is the root block, not the shadow block.
194
- if (input?.connection?.targetBlock()) {
195
- const procCode = input.connection.targetBlock().getProcCode()
195
+ const targetBlock = input?.connection?.targetBlock()
196
+ const targetWithProcCode = targetBlock as (Blockly.Block & { getProcCode?(): string }) | null
197
+ if (targetWithProcCode?.getProcCode) {
198
+ const procCode = targetWithProcCode.getProcCode()
196
199
  const didDelete = ScratchProcedures.deleteProcedureDefCallback(procCode, this)
197
200
  if (!didDelete) {
198
201
  alert(Blockly.Msg.PROCEDURE_USED)
@@ -227,7 +230,7 @@ const PROCEDURE_CALL_CONTEXTMENU = {
227
230
  }
228
231
 
229
232
  const SCRATCH_EXTENSION = function (this: Blockly.Block) {
230
- ;(this as any).isScratchExtension = true
233
+ ;(this as Blockly.Block & { isScratchExtension?: boolean }).isScratchExtension = true
231
234
  }
232
235
 
233
236
  /**
@@ -2,13 +2,29 @@
2
2
  * Copyright 2024 Google LLC
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
- import { ContinuousFlyout } from '@blockly/continuous-toolbox'
5
+ import { ContinuousFlyout, type LabelFlyoutItem } from '@blockly/continuous-toolbox'
6
6
  import * as Blockly from 'blockly/core'
7
7
  import { CheckboxBubble } from './checkbox_bubble'
8
8
  import { StatusIndicatorLabel } from './status_indicator_label'
9
9
  import { STATUS_INDICATOR_LABEL_TYPE } from './status_indicator_label_flyout_inflater'
10
10
 
11
+ interface ReflowElement extends Blockly.BlockSvg {
12
+ checkboxInFlyout?: boolean
13
+ }
14
+
15
+ interface CheckboxIcon {
16
+ setChecked: (value: boolean) => void
17
+ }
18
+
19
+ function isCheckboxIcon(icon: Blockly.IIcon | undefined): icon is Blockly.IIcon & CheckboxIcon {
20
+ return !!icon && typeof (icon as { setChecked?: unknown }).setChecked === 'function'
21
+ }
22
+
11
23
  export class CheckableContinuousFlyout extends ContinuousFlyout {
24
+ declare protected tabWidth_: number
25
+ declare MARGIN: number
26
+ declare GAP_Y: number
27
+
12
28
  /**
13
29
  * Creates a new CheckableContinuousFlyout.
14
30
  * @param workspaceOptions Configuration options for the flyout workspace.
@@ -42,7 +58,16 @@ export class CheckableContinuousFlyout extends ContinuousFlyout {
42
58
  * @param value Value to set the checkbox to.
43
59
  */
44
60
  setCheckboxState(blockId: string, value: boolean) {
45
- this.getWorkspace().getBlockById(blockId)?.getIcon('checkbox')?.setChecked(value)
61
+ const icon = this.getWorkspace().getBlockById(blockId)?.getIcon('checkbox')
62
+ if (!icon) {
63
+ return
64
+ }
65
+ if (!isCheckboxIcon(icon)) {
66
+ throw new Error(
67
+ `[CheckableContinuousFlyout.setCheckboxState] Expected checkbox icon with setChecked for block ${blockId}`,
68
+ )
69
+ }
70
+ icon.setChecked(value)
46
71
  }
47
72
 
48
73
  getFlyoutScale() {
@@ -61,14 +86,18 @@ export class CheckableContinuousFlyout extends ContinuousFlyout {
61
86
  // contents, and adjusts blocks in RTL mode accordingly. In Scratch, the
62
87
  // flyout width is fixed (and blocks may exceed it), so re-adjust blocks
63
88
  // accordingly based on the actual fixed width.
64
- for (const item of this.getContents()) {
65
- const oldX = item.getElement().getBoundingRectangle().left
66
- let newX =
67
- this.getWidth() / this.workspace_.scale - item.getElement().getBoundingRectangle().getWidth() - this.MARGIN
68
- if ('checkboxInFlyout' in item.getElement() && item.getElement().checkboxInFlyout) {
89
+ const flyoutItems = this.getContents()
90
+ for (const item of flyoutItems) {
91
+ const element = item.getElement()
92
+ if (!(element instanceof Blockly.BlockSvg)) {
93
+ continue
94
+ }
95
+ const oldX = element.getBoundingRectangle().left
96
+ let newX = this.getWidth() / this.workspace_.scale - element.getBoundingRectangle().getWidth() - this.MARGIN
97
+ if ('checkboxInFlyout' in element && (element as ReflowElement).checkboxInFlyout) {
69
98
  newX -= CheckboxBubble.CHECKBOX_SIZE + CheckboxBubble.CHECKBOX_MARGIN
70
99
  }
71
- item.getElement().moveBy(newX - oldX, 0)
100
+ element.moveBy(newX - oldX, 0)
72
101
  }
73
102
  }
74
103
  }
@@ -78,8 +107,11 @@ export class CheckableContinuousFlyout extends ContinuousFlyout {
78
107
  * @param item The toolbox item to check.
79
108
  * @returns True if the item represents a label in the flyout.
80
109
  */
81
- protected toolboxItemIsLabel(item: Blockly.FlyoutItem) {
82
- return item.getType() === STATUS_INDICATOR_LABEL_TYPE || super.toolboxItemIsLabel(item)
110
+ protected toolboxItemIsLabel(item: Blockly.FlyoutItem): item is LabelFlyoutItem {
111
+ if (item.getType() === STATUS_INDICATOR_LABEL_TYPE) {
112
+ return true
113
+ }
114
+ return super.toolboxItemIsLabel(item)
83
115
  }
84
116
 
85
117
  /**
@@ -87,8 +119,9 @@ export class CheckableContinuousFlyout extends ContinuousFlyout {
87
119
  */
88
120
  refreshStatusButtons() {
89
121
  for (const item of this.contents) {
90
- if (item.element instanceof StatusIndicatorLabel) {
91
- item.element.refreshStatus()
122
+ const element = item.getElement()
123
+ if (element instanceof StatusIndicatorLabel) {
124
+ element.refreshStatus()
92
125
  }
93
126
  }
94
127
  }
@@ -155,10 +155,10 @@ export class CheckboxBubble implements Blockly.IBubble, Blockly.IRenderedElement
155
155
  * Returns whether or not the specified block has its checkbox checked.
156
156
  *
157
157
  * This method is patched by scratch-gui to query the VM state.
158
- * @param blockId The ID of the block in question.
158
+ * @param _blockId The ID of the block in question.
159
159
  * @returns True if the block's checkbox should be checked.
160
160
  */
161
- isChecked(blockId: string): boolean {
161
+ isChecked(_blockId: string): boolean {
162
162
  return false
163
163
  }
164
164
 
@@ -252,17 +252,17 @@ export class CheckboxBubble implements Blockly.IBubble, Blockly.IRenderedElement
252
252
  // to its block and is not draggable by the user.
253
253
  showContextMenu() {}
254
254
 
255
- setDragging(dragging: boolean) {}
255
+ setDragging(_dragging: boolean) {}
256
256
 
257
- startDrag(event: PointerEvent) {}
257
+ startDrag(_event: PointerEvent) {}
258
258
 
259
- drag(newLocation: Blockly.utils.Coordinate, event: PointerEvent) {}
259
+ drag(_newLocation: Blockly.utils.Coordinate, _event: PointerEvent) {}
260
260
 
261
- moveDuringDrag(newLocation: Blockly.utils.Coordinate) {}
261
+ moveDuringDrag(_newLocation: Blockly.utils.Coordinate) {}
262
262
 
263
263
  endDrag() {}
264
264
 
265
265
  revertDrag() {}
266
266
 
267
- setDeleteStyle(enable: boolean) {}
267
+ setDeleteStyle(_enable: boolean) {}
268
268
  }
package/src/colours.ts CHANGED
@@ -62,13 +62,15 @@ const Colours = {
62
62
  * @param prefix A prefix to prepend to the CSS variables.
63
63
  * @returns A string containing CSS variable definitions for the colours.
64
64
  */
65
- function varify(coloursObj: object, prefix = '--colour'): string {
65
+ function varify(coloursObj: Record<string, unknown>, prefix = '--colour'): string {
66
66
  return Object.entries(coloursObj)
67
67
  .map(([key, colour]) => {
68
68
  if (typeof colour === 'string') {
69
69
  return `${prefix}-${key}: ${colour};`
70
+ } else if (typeof colour === 'object' && colour !== null) {
71
+ return varify(colour as Record<string, unknown>, `${prefix}-${key}`)
70
72
  } else {
71
- return varify(colour, `${prefix}-${key}`)
73
+ return ''
72
74
  }
73
75
  })
74
76
  .join('\n')