scratch-blocks 2.1.5 → 2.1.7

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 (118) hide show
  1. package/AGENTS.md +58 -14
  2. package/dist/main.mjs +1 -1
  3. package/dist/types/src/block_reporting.d.ts.map +1 -1
  4. package/dist/types/src/blocks/procedures.d.ts +2 -2
  5. package/dist/types/src/blocks/procedures.d.ts.map +1 -1
  6. package/dist/types/src/checkable_continuous_flyout.d.ts +6 -3
  7. package/dist/types/src/checkable_continuous_flyout.d.ts.map +1 -1
  8. package/dist/types/src/checkbox_bubble.d.ts +7 -7
  9. package/dist/types/src/checkbox_bubble.d.ts.map +1 -1
  10. package/dist/types/src/colours.d.ts.map +1 -1
  11. package/dist/types/src/context_menu_items.d.ts.map +1 -1
  12. package/dist/types/src/events/events_block_comment_base.d.ts +1 -1
  13. package/dist/types/src/events/events_block_comment_base.d.ts.map +1 -1
  14. package/dist/types/src/events/events_block_drag_end.d.ts +1 -1
  15. package/dist/types/src/events/events_block_drag_end.d.ts.map +1 -1
  16. package/dist/types/src/events/events_block_drag_outside.d.ts +1 -1
  17. package/dist/types/src/events/events_block_drag_outside.d.ts.map +1 -1
  18. package/dist/types/src/fields/field_colour_slider.d.ts.map +1 -1
  19. package/dist/types/src/fields/field_matrix.d.ts.map +1 -1
  20. package/dist/types/src/fields/field_note.d.ts +6 -4
  21. package/dist/types/src/fields/field_note.d.ts.map +1 -1
  22. package/dist/types/src/fields/field_textinput_removable.d.ts.map +1 -1
  23. package/dist/types/src/fields/field_variable_getter.d.ts.map +1 -1
  24. package/dist/types/src/fields/field_vertical_separator.d.ts.map +1 -1
  25. package/dist/types/src/fields/scratch_field_angle.d.ts.map +1 -1
  26. package/dist/types/src/fields/scratch_field_dropdown.d.ts.map +1 -1
  27. package/dist/types/src/fields/scratch_field_number.d.ts.map +1 -1
  28. package/dist/types/src/fields/scratch_field_variable.d.ts +1 -0
  29. package/dist/types/src/fields/scratch_field_variable.d.ts.map +1 -1
  30. package/dist/types/src/flyout_checkbox_icon.d.ts +5 -5
  31. package/dist/types/src/flyout_checkbox_icon.d.ts.map +1 -1
  32. package/dist/types/src/glows.d.ts.map +1 -1
  33. package/dist/types/src/procedures.d.ts +4 -4
  34. package/dist/types/src/procedures.d.ts.map +1 -1
  35. package/dist/types/src/recyclable_block_flyout_inflater.d.ts +2 -2
  36. package/dist/types/src/recyclable_block_flyout_inflater.d.ts.map +1 -1
  37. package/dist/types/src/renderer/cat/cat_face.d.ts +1 -1
  38. package/dist/types/src/renderer/cat/cat_face.d.ts.map +1 -1
  39. package/dist/types/src/renderer/cat/drawer.d.ts.map +1 -1
  40. package/dist/types/src/renderer/constants.d.ts.map +1 -1
  41. package/dist/types/src/renderer/drawer.d.ts.map +1 -1
  42. package/dist/types/src/renderer/render_info.d.ts.map +1 -1
  43. package/dist/types/src/scratch_blocks_utils.d.ts +22 -0
  44. package/dist/types/src/scratch_blocks_utils.d.ts.map +1 -1
  45. package/dist/types/src/scratch_comment_bubble.d.ts +4 -4
  46. package/dist/types/src/scratch_comment_bubble.d.ts.map +1 -1
  47. package/dist/types/src/scratch_comment_icon.d.ts +1 -1
  48. package/dist/types/src/scratch_comment_icon.d.ts.map +1 -1
  49. package/dist/types/src/scratch_continuous_category.d.ts +3 -1
  50. package/dist/types/src/scratch_continuous_category.d.ts.map +1 -1
  51. package/dist/types/src/scratch_continuous_toolbox.d.ts +2 -1
  52. package/dist/types/src/scratch_continuous_toolbox.d.ts.map +1 -1
  53. package/dist/types/src/status_indicator_label.d.ts +3 -3
  54. package/dist/types/src/status_indicator_label.d.ts.map +1 -1
  55. package/dist/types/src/status_indicator_label_flyout_inflater.d.ts.map +1 -1
  56. package/dist/types/src/variables.d.ts +1 -1
  57. package/dist/types/src/variables.d.ts.map +1 -1
  58. package/dist/types/src/workspace_block_lookup.d.ts +4 -0
  59. package/dist/types/src/workspace_block_lookup.d.ts.map +1 -0
  60. package/eslint.config.mjs +21 -28
  61. package/package.json +1 -1
  62. package/src/block_reporting.ts +5 -5
  63. package/src/blocks/control.ts +5 -5
  64. package/src/blocks/data.ts +1 -1
  65. package/src/blocks/event.ts +1 -1
  66. package/src/blocks/motion.ts +2 -2
  67. package/src/blocks/procedures.ts +162 -69
  68. package/src/blocks/sensing.ts +0 -1
  69. package/src/blocks/vertical_extensions.ts +11 -8
  70. package/src/checkable_continuous_flyout.ts +45 -12
  71. package/src/checkbox_bubble.ts +7 -7
  72. package/src/colours.ts +4 -2
  73. package/src/context_menu_items.ts +41 -16
  74. package/src/data_category.ts +11 -3
  75. package/src/events/events_block_comment_base.ts +5 -1
  76. package/src/events/events_block_comment_change.ts +5 -1
  77. package/src/events/events_block_comment_collapse.ts +6 -2
  78. package/src/events/events_block_comment_create.ts +5 -1
  79. package/src/events/events_block_comment_move.ts +6 -2
  80. package/src/events/events_block_comment_resize.ts +6 -2
  81. package/src/events/events_block_drag_end.ts +5 -1
  82. package/src/events/events_block_drag_outside.ts +5 -1
  83. package/src/events/events_scratch_variable_create.ts +5 -1
  84. package/src/fields/field_colour_slider.ts +3 -5
  85. package/src/fields/field_matrix.ts +33 -17
  86. package/src/fields/field_note.ts +56 -20
  87. package/src/fields/field_textinput_removable.ts +13 -4
  88. package/src/fields/field_variable_getter.ts +20 -6
  89. package/src/fields/field_vertical_separator.ts +5 -1
  90. package/src/fields/scratch_field_angle.ts +32 -21
  91. package/src/fields/scratch_field_dropdown.ts +6 -2
  92. package/src/fields/scratch_field_number.ts +22 -13
  93. package/src/fields/scratch_field_variable.ts +26 -12
  94. package/src/flyout_checkbox_icon.ts +9 -5
  95. package/src/glows.ts +5 -5
  96. package/src/index.ts +18 -6
  97. package/src/procedures.ts +92 -42
  98. package/src/recyclable_block_flyout_inflater.ts +5 -4
  99. package/src/renderer/cat/cat_face.ts +1 -1
  100. package/src/renderer/cat/drawer.ts +4 -1
  101. package/src/renderer/constants.ts +19 -14
  102. package/src/renderer/drawer.ts +2 -1
  103. package/src/renderer/render_info.ts +12 -9
  104. package/src/renderer/renderer.ts +1 -1
  105. package/src/scratch_blocks_utils.ts +0 -2
  106. package/src/scratch_c_block_wrap.ts +37 -21
  107. package/src/scratch_comment_bubble.ts +30 -19
  108. package/src/scratch_comment_icon.ts +9 -12
  109. package/src/scratch_continuous_category.ts +20 -11
  110. package/src/scratch_continuous_toolbox.ts +12 -3
  111. package/src/scratch_dragger.ts +2 -2
  112. package/src/scratch_variable_map.ts +1 -1
  113. package/src/status_indicator_label.ts +13 -9
  114. package/src/status_indicator_label_flyout_inflater.ts +2 -1
  115. package/src/variables.ts +21 -14
  116. package/src/workspace_block_lookup.ts +14 -0
  117. package/src/xml.ts +1 -1
  118. package/types/continuous-toolbox.d.ts +0 -1
@@ -29,6 +29,13 @@ import { createVariable, renameVariable } from '../variables'
29
29
  export class ScratchFieldVariable extends Blockly.FieldVariable {
30
30
  private originalStyle!: string
31
31
 
32
+ private getSourceWorkspaceSvg_(sourceBlock: Blockly.Block): Blockly.WorkspaceSvg {
33
+ if (!(sourceBlock.workspace instanceof Blockly.WorkspaceSvg)) {
34
+ throw new Error('[scratch_field_variable] Expected source block workspace to be a WorkspaceSvg')
35
+ }
36
+ return sourceBlock.workspace
37
+ }
38
+
32
39
  constructor(
33
40
  varName: string | null | typeof Blockly.Field.SKIP_SETUP,
34
41
  validator?: Blockly.FieldVariableValidator,
@@ -40,14 +47,15 @@ export class ScratchFieldVariable extends Blockly.FieldVariable {
40
47
  // dropdownCreate returns MenuOption[] rather than Blockly.MenuGenerator's
41
48
  // MenuOption[][] variant; the cast is needed to satisfy FieldVariable's
42
49
  // menuGenerator_ type while the actual runtime shape is compatible.
43
- this.menuGenerator_ = ScratchFieldVariable.dropdownCreate as unknown as Blockly.MenuGenerator
50
+ this.menuGenerator_ = ScratchFieldVariable.dropdownCreate.bind(this) as unknown as Blockly.MenuGenerator
44
51
  }
45
52
 
46
53
  initModel() {
47
54
  if (!this.getVariable()) {
48
55
  const sourceBlock = this.getSourceBlock()
49
56
  if (sourceBlock) {
50
- const broadcastVariable = this.initFlyoutBroadcast(sourceBlock.workspace as Blockly.WorkspaceSvg)
57
+ const sourceWorkspace = this.getSourceWorkspaceSvg_(sourceBlock)
58
+ const broadcastVariable = this.initFlyoutBroadcast(sourceWorkspace)
51
59
  if (broadcastVariable) {
52
60
  this.doValueUpdate_(broadcastVariable.getId())
53
61
  return
@@ -96,7 +104,8 @@ export class ScratchFieldVariable extends Blockly.FieldVariable {
96
104
  if (option[1] === Blockly.RENAME_VARIABLE_ID) {
97
105
  return [ScratchMsgs.translate('RENAME_LIST'), option[1]]
98
106
  } else if (option[1] === Blockly.DELETE_VARIABLE_ID) {
99
- return [ScratchMsgs.translate('DELETE_LIST').replace('%1', this.getText()), option[1]]
107
+ const fieldText = (this as Blockly.FieldVariable).getText()
108
+ return [String(ScratchMsgs.translate('DELETE_LIST')).replace('%1', fieldText), option[1]]
100
109
  }
101
110
  return option
102
111
  })
@@ -118,8 +127,9 @@ export class ScratchFieldVariable extends Blockly.FieldVariable {
118
127
  if (sourceBlock && !sourceBlock.isDeadOrDying()) {
119
128
  const selectedItem = menuItem.getValue()
120
129
  if (selectedItem === Constants.NEW_BROADCAST_MESSAGE_ID) {
130
+ const sourceWorkspace = this.getSourceWorkspaceSvg_(sourceBlock)
121
131
  createVariable(
122
- sourceBlock.workspace as Blockly.WorkspaceSvg,
132
+ sourceWorkspace,
123
133
  (varId) => {
124
134
  if (varId) {
125
135
  this.setValue(varId)
@@ -129,7 +139,7 @@ export class ScratchFieldVariable extends Blockly.FieldVariable {
129
139
  )
130
140
  return
131
141
  } else if (selectedItem === Blockly.RENAME_VARIABLE_ID) {
132
- renameVariable(sourceBlock.workspace as Blockly.WorkspaceSvg, this.getVariable() as ScratchVariableModel)
142
+ renameVariable(sourceBlock.workspace, this.getVariable() as ScratchVariableModel)
133
143
  return
134
144
  }
135
145
  }
@@ -138,26 +148,30 @@ export class ScratchFieldVariable extends Blockly.FieldVariable {
138
148
 
139
149
  showEditor_(event: PointerEvent) {
140
150
  super.showEditor_(event)
141
- const sourceBlock = this.getSourceBlock()!
151
+ const sourceBlock = this.getSourceBlock()
152
+ if (!sourceBlock) {
153
+ throw new Error('[scratch_field_variable] Missing source block in showEditor_')
154
+ }
155
+ const sourceWorkspace = this.getSourceWorkspaceSvg_(sourceBlock)
142
156
  const styleName = sourceBlock.getStyleName()
143
- const style = (sourceBlock.workspace as Blockly.WorkspaceSvg)
144
- .getRenderer()
145
- .getConstants()
146
- .getBlockStyle(styleName)
157
+ const style = sourceWorkspace.getRenderer().getConstants().getBlockStyle(styleName)
147
158
  if (sourceBlock.isShadow()) {
148
159
  this.originalStyle = styleName
149
160
  sourceBlock.setStyle(`${this.originalStyle}_selected`)
150
161
  } else if (this.borderRect_) {
151
162
  this.borderRect_.setAttribute(
152
163
  'fill',
153
- 'colourQuaternary' in style ? `${style.colourQuaternary}` : style.colourTertiary,
164
+ 'colourQuaternary' in style ? String(style.colourQuaternary) : style.colourTertiary,
154
165
  )
155
166
  }
156
167
  }
157
168
 
158
169
  dropdownDispose_() {
159
170
  super.dropdownDispose_()
160
- const sourceBlock = this.getSourceBlock()!
171
+ const sourceBlock = this.getSourceBlock()
172
+ if (!sourceBlock) {
173
+ throw new Error('[scratch_field_variable] Missing source block in dropdownDispose_')
174
+ }
161
175
  if (sourceBlock.isShadow()) {
162
176
  sourceBlock.setStyle(this.originalStyle)
163
177
  }
@@ -9,7 +9,7 @@ import { CheckboxBubble } from './checkbox_bubble'
9
9
  * Invisible icon that exists solely to host the corresponding checkbox bubble.
10
10
  */
11
11
  export class FlyoutCheckboxIcon extends Blockly.icons.Icon implements Blockly.IHasBubble {
12
- private checkboxBubble!: CheckboxBubble
12
+ private checkboxBubble?: CheckboxBubble
13
13
  private type = new Blockly.icons.IconType('checkbox')
14
14
 
15
15
  constructor(protected override sourceBlock: Blockly.BlockSvg) {
@@ -36,7 +36,7 @@ export class FlyoutCheckboxIcon extends Blockly.icons.Icon implements Blockly.IH
36
36
  return this.sourceBlock.workspace.isFlyout
37
37
  }
38
38
 
39
- onLocationChange(blockOrigin: Blockly.utils.Coordinate) {
39
+ onLocationChange(_blockOrigin: Blockly.utils.Coordinate) {
40
40
  this.checkboxBubble?.updateLocation()
41
41
  }
42
42
 
@@ -52,16 +52,20 @@ export class FlyoutCheckboxIcon extends Blockly.icons.Icon implements Blockly.IH
52
52
  // These methods are required by the interfaces, but intentionally have no
53
53
  // implementation, largely because this icon has no visual representation.
54
54
 
55
- async setBubbleVisible(visible: boolean) {}
55
+ setBubbleVisible(_visible: boolean): Promise<void> {
56
+ return Promise.resolve()
57
+ }
56
58
 
57
- initView(pointerDownListener: (e: PointerEvent) => void) {}
59
+ initView(_pointerDownListener: (e: PointerEvent) => void) {
60
+ return
61
+ }
58
62
 
59
63
  canBeFocused() {
60
64
  return false
61
65
  }
62
66
 
63
67
  getBubble() {
64
- return this.checkboxBubble
68
+ return this.checkboxBubble ?? null
65
69
  }
66
70
  }
67
71
 
package/src/glows.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import * as Blockly from 'blockly/core'
6
6
  import { Colours } from './colours'
7
+ import { getBlockSvgById, getRequiredMainWorkspaceSvg } from './workspace_block_lookup'
7
8
 
8
9
  /**
9
10
  * Glow/unglow a stack in the workspace.
@@ -11,11 +12,10 @@ import { Colours } from './colours'
11
12
  * @param isGlowingStack Whether to glow the stack.
12
13
  */
13
14
  export function glowStack(id: string, isGlowingStack: boolean) {
14
- const block = (Blockly.getMainWorkspace().getBlockById(id) ||
15
- (Blockly.getMainWorkspace() as Blockly.WorkspaceSvg)
16
- .getFlyout()
17
- ?.getWorkspace()
18
- ?.getBlockById(id)) as Blockly.BlockSvg
15
+ const mainWorkspace = getRequiredMainWorkspaceSvg()
16
+ const flyout = mainWorkspace.getFlyout()
17
+ const flyoutBlock = flyout ? getBlockSvgById(flyout.getWorkspace(), id) : null
18
+ const block = getBlockSvgById(mainWorkspace, id) ?? flyoutBlock
19
19
  if (!block) {
20
20
  throw new Error('Tried to glow block that does not exist.')
21
21
  }
package/src/index.ts CHANGED
@@ -159,7 +159,7 @@ export function isContentNodeFocused(): boolean {
159
159
  return Blockly.getFocusManager().getFocusedNode() !== null
160
160
  }
161
161
 
162
- registerContinuousToolbox()
162
+ ;(registerContinuousToolbox as () => void)()
163
163
  Blockly.Scrollbar.scrollbarThickness = Blockly.Touch.TOUCH_ENABLED ? 14 : 11
164
164
  Blockly.FlyoutButton.TEXT_MARGIN_X = 40
165
165
  Blockly.FlyoutButton.TEXT_MARGIN_Y = 10
@@ -169,12 +169,23 @@ Blockly.ContextMenuItems.registerCommentOptions()
169
169
  // Blockly hides "Add Comment" for simple reporters because comments can't be
170
170
  // read in the default renderer. In Scratch they're shown differently, so
171
171
  // remove that restriction by dropping the isFullBlockField check.
172
- Blockly.ContextMenuRegistry.registry.getItem('blockComment')!.preconditionFn = (scope) => {
173
- const block = scope.block
174
- if (block && !block.isInFlyout && block.workspace.options.comments && !block.isCollapsed() && block.isEditable()) {
175
- return 'enabled'
172
+ const blockCommentMenuItem = Blockly.ContextMenuRegistry.registry.getItem('blockComment')
173
+ if (!blockCommentMenuItem) {
174
+ console.error('[index] Missing context menu item: blockComment')
175
+ } else {
176
+ blockCommentMenuItem.preconditionFn = (scope) => {
177
+ const block = scope.block
178
+ if (
179
+ block &&
180
+ !block.isInFlyout &&
181
+ block.workspace.options.comments &&
182
+ !block.isCollapsed() &&
183
+ block.isEditable()
184
+ ) {
185
+ return 'enabled'
186
+ }
187
+ return 'hidden'
176
188
  }
177
- return 'hidden'
178
189
  }
179
190
  Blockly.ContextMenuRegistry.registry.unregister('blockDelete')
180
191
  contextMenuItems.registerDeleteBlock()
@@ -191,6 +202,7 @@ Blockly.comments.CommentView.defaultCommentSize = new Blockly.utils.Size(200, 20
191
202
  // to the workspace itself (whose onNodeFocus is a no-op) rather than to a
192
203
  // specific block, so deleting a block doesn't reset the scroll position.
193
204
  // We may need to re-evaluate this when we explicitly work on keyboard navigation.
205
+ // eslint-disable-next-line @typescript-eslint/unbound-method -- preserve original prototype method for patched wrapper
194
206
  const originalGetRestoredFocusableNode = Blockly.WorkspaceSvg.prototype.getRestoredFocusableNode
195
207
  Blockly.WorkspaceSvg.prototype.getRestoredFocusableNode = function (previousNode) {
196
208
  if (!previousNode && !this.isFlyout) return null
package/src/procedures.ts CHANGED
@@ -33,7 +33,12 @@ function allProcedureMutations(root: Blockly.WorkspaceSvg): Element[] {
33
33
  const blocks = root.getAllBlocks()
34
34
  return blocks
35
35
  .filter((b) => b.type === Constants.PROCEDURES_PROTOTYPE_BLOCK_TYPE)
36
- .map((b) => b.mutationToDom!(/* opt_generateShadows */ true))
36
+ .map((b) => {
37
+ if (!b.mutationToDom) {
38
+ throw new Error(`Expected mutationToDom on procedure prototype block ${b.id}`)
39
+ }
40
+ return b.mutationToDom(/* opt_generateShadows */ true)
41
+ })
37
42
  }
38
43
 
39
44
  /**
@@ -44,8 +49,11 @@ function allProcedureMutations(root: Blockly.WorkspaceSvg): Element[] {
44
49
  */
45
50
  function sortProcedureMutations(mutations: Element[]): Element[] {
46
51
  return mutations.slice().sort((a, b) => {
47
- const procCodeA = a.getAttribute('proccode')!
48
- const procCodeB = b.getAttribute('proccode')!
52
+ const procCodeA = a.getAttribute('proccode')
53
+ const procCodeB = b.getAttribute('proccode')
54
+ if (!procCodeA || !procCodeB) {
55
+ throw new Error('Expected proccode attribute in procedure mutation element')
56
+ }
49
57
 
50
58
  return scratchBlocksUtils.compareStrings(procCodeA, procCodeB)
51
59
  })
@@ -114,23 +122,19 @@ function addCreateButton(workspace: Blockly.WorkspaceSvg, xmlList: Element[]) {
114
122
  */
115
123
  export function getCallers(
116
124
  name: string,
117
- workspace: Blockly.WorkspaceSvg,
118
- definitionRoot: Blockly.BlockSvg,
125
+ workspace: Blockly.Workspace,
126
+ definitionRoot: Pick<Blockly.Block, 'id'>,
119
127
  allowRecursive: boolean,
120
- ): Blockly.BlockSvg[] {
128
+ ): ProcedureBlock[] {
121
129
  return workspace.getTopBlocks().flatMap((block) => {
122
130
  if (block.id === definitionRoot.id && !allowRecursive) {
123
131
  return []
124
132
  }
125
133
 
126
- return block
127
- .getDescendants(false)
128
- .filter(
129
- (descendant) =>
130
- isProcedureBlock(descendant) &&
131
- descendant.type === Constants.PROCEDURES_CALL_BLOCK_TYPE &&
132
- descendant.getProcCode() === name,
133
- )
134
+ const procedureDescendants = block.getDescendants(false).filter(isProcedureBlock)
135
+ return procedureDescendants.filter(
136
+ (descendant) => descendant.type === Constants.PROCEDURES_CALL_BLOCK_TYPE && descendant.getProcCode() === name,
137
+ )
134
138
  })
135
139
  }
136
140
 
@@ -147,16 +151,22 @@ function mutateCallersAndPrototype(name: string, workspace: Blockly.WorkspaceSvg
147
151
  alert('No define block on workspace') // TODO decide what to do about this.
148
152
  return
149
153
  }
154
+ if (!isProcedureBlock(prototypeBlock)) {
155
+ throw new Error(`Expected procedure prototype block for procCode ${name}`)
156
+ }
150
157
 
151
158
  const callers = getCallers(name, defineBlock.workspace, defineBlock, true /* allowRecursive */)
152
159
  callers.push(prototypeBlock)
153
160
  Blockly.Events.setGroup(true)
154
161
  callers.forEach((caller) => {
155
- const oldMutationDom = caller.mutationToDom!()
156
- const oldMutation = oldMutationDom && Blockly.Xml.domToText(oldMutationDom)
157
- caller.domToMutation!(mutation)
158
- const newMutationDom = caller.mutationToDom!()
159
- const newMutation = newMutationDom && Blockly.Xml.domToText(newMutationDom)
162
+ if (!caller.mutationToDom || !caller.domToMutation) {
163
+ throw new Error(`Expected mutation APIs on block ${caller.id} (${caller.type})`)
164
+ }
165
+ const oldMutationDom = caller.mutationToDom()
166
+ const oldMutation = Blockly.Xml.domToText(oldMutationDom)
167
+ caller.domToMutation(mutation)
168
+ const newMutationDom = caller.mutationToDom()
169
+ const newMutation = Blockly.Xml.domToText(newMutationDom)
160
170
  if (oldMutation !== newMutation) {
161
171
  Blockly.Events.fire(
162
172
  new (Blockly.Events.get(Blockly.Events.BLOCK_CHANGE))(caller, 'mutation', null, oldMutation, newMutation),
@@ -172,16 +182,31 @@ function mutateCallersAndPrototype(name: string, workspace: Blockly.WorkspaceSvg
172
182
  * @param workspace The workspace to search.
173
183
  * @returns The procedure definition block, or undefined if not found.
174
184
  */
175
- function getDefineBlock(procCode: string, workspace: Blockly.WorkspaceSvg): Blockly.BlockSvg | undefined {
185
+ function getDefineBlock(procCode: string, workspace: Blockly.Workspace): Blockly.BlockSvg | undefined {
176
186
  // Assume that a procedure definition is a top block.
177
- return workspace.getTopBlocks(false).find((block) => {
187
+ for (const block of workspace.getTopBlocks(false)) {
178
188
  if (block.type === Constants.PROCEDURES_DEFINITION_BLOCK_TYPE) {
179
- const prototypeBlock = block.getInput('custom_block')!.connection!.targetBlock() as Blockly.BlockSvg
180
- return isProcedureBlock(prototypeBlock) && prototypeBlock.getProcCode() === procCode
189
+ const input = block.getInput('custom_block')
190
+ if (!input?.connection) {
191
+ throw new Error(`Expected custom_block input connection on block ${block.id}`)
192
+ }
193
+ const raw = input.connection.targetBlock()
194
+ if (!raw) {
195
+ throw new Error(`Expected custom_block target block on block ${block.id}`)
196
+ }
197
+ if (!(raw instanceof Blockly.BlockSvg)) {
198
+ throw new Error(`Expected custom_block target BlockSvg on block ${block.id}`)
199
+ }
200
+ const prototypeBlock = raw
201
+ if (isProcedureBlock(prototypeBlock) && prototypeBlock.getProcCode() === procCode) {
202
+ if (!(block instanceof Blockly.BlockSvg)) {
203
+ throw new Error(`Expected procedure definition BlockSvg for block ${block.id}`)
204
+ }
205
+ return block
206
+ }
181
207
  }
182
-
183
- return false
184
- })
208
+ }
209
+ return undefined
185
210
  }
186
211
 
187
212
  /**
@@ -190,10 +215,21 @@ function getDefineBlock(procCode: string, workspace: Blockly.WorkspaceSvg): Bloc
190
215
  * @param workspace The workspace to search.
191
216
  * @returns The procedure prototype block, or undefined if not found.
192
217
  */
193
- function getPrototypeBlock(procCode: string, workspace: Blockly.WorkspaceSvg): Blockly.BlockSvg | undefined {
218
+ function getPrototypeBlock(procCode: string, workspace: Blockly.Workspace): Blockly.BlockSvg | undefined {
194
219
  const defineBlock = getDefineBlock(procCode, workspace)
195
220
  if (defineBlock) {
196
- return defineBlock.getInput('custom_block')!.connection!.targetBlock() as Blockly.BlockSvg
221
+ const input = defineBlock.getInput('custom_block')
222
+ if (!input?.connection) {
223
+ throw new Error(`Expected custom_block input connection on block ${defineBlock.id}`)
224
+ }
225
+ const target = input.connection.targetBlock()
226
+ if (!target) {
227
+ throw new Error(`Expected custom_block target block on block ${defineBlock.id}`)
228
+ }
229
+ if (!(target instanceof Blockly.BlockSvg)) {
230
+ throw new Error(`Expected custom_block target BlockSvg on block ${defineBlock.id}`)
231
+ }
232
+ return target
197
233
  }
198
234
  return undefined
199
235
  }
@@ -213,7 +249,9 @@ function newProcedureMutation(): Element {
213
249
  warp="false">
214
250
  </mutation>
215
251
  </xml>`
216
- return Blockly.utils.xml.textToDom(mutationText).firstElementChild!
252
+ const el = Blockly.utils.xml.textToDom(mutationText).firstElementChild
253
+ if (!el) throw new Error('Failed to parse mutation XML')
254
+ return el
217
255
  }
218
256
 
219
257
  /**
@@ -221,7 +259,11 @@ function newProcedureMutation(): Element {
221
259
  * @param workspace The workspace to create the new procedure on.
222
260
  */
223
261
  function createProcedureDefCallback(workspace: Blockly.WorkspaceSvg) {
224
- ScratchProcedures.externalProcedureDefCallback!(newProcedureMutation(), createProcedureCallbackFactory(workspace))
262
+ const callback = ScratchProcedures.externalProcedureDefCallback
263
+ if (!callback) {
264
+ throw new Error('ScratchProcedures.externalProcedureDefCallback is not set')
265
+ }
266
+ callback(newProcedureMutation(), createProcedureCallbackFactory(workspace))
225
267
  }
226
268
 
227
269
  /**
@@ -243,10 +285,13 @@ function createProcedureCallbackFactory(workspace: Blockly.WorkspaceSvg): (mutat
243
285
  </statement>
244
286
  </block>
245
287
  </xml>`
246
- const blockDom = Blockly.utils.xml.textToDom(blockText).firstElementChild!
288
+ const blockDom = Blockly.utils.xml.textToDom(blockText).firstElementChild
289
+ if (!blockDom) {
290
+ throw new Error('Failed to parse procedure definition XML')
291
+ }
247
292
  Blockly.Events.setGroup(true)
248
293
  const block = Blockly.Xml.domToBlock(blockDom, workspace) as Blockly.BlockSvg
249
- Blockly.renderManagement.finishQueuedRenders().then(() => {
294
+ void Blockly.renderManagement.finishQueuedRenders().then(() => {
250
295
  // To convert from pixel units to workspace units
251
296
  const scale = workspace.scale
252
297
  // Position the block so that it is at the top left of the visible
@@ -293,7 +338,9 @@ function editProcedureCallback(block: Blockly.BlockSvg) {
293
338
  } else if (block.type === Constants.PROCEDURES_CALL_BLOCK_TYPE && isProcedureBlock(block)) {
294
339
  // This is a call block, find the prototype corresponding to the procCode.
295
340
  // Make sure to search the correct workspace, call block can be in flyout.
296
- const workspaceToSearch = block.workspace.isFlyout ? block.workspace.targetWorkspace! : block.workspace
341
+ const workspaceToSearch = block.workspace.isFlyout
342
+ ? (block.workspace.targetWorkspace ?? block.workspace)
343
+ : block.workspace
297
344
  const foundBlock = getPrototypeBlock(block.getProcCode(), workspaceToSearch)
298
345
  if (!foundBlock) {
299
346
  console.warn('editProcedureCallback: could not find prototype for', block.getProcCode())
@@ -304,10 +351,14 @@ function editProcedureCallback(block: Blockly.BlockSvg) {
304
351
  prototypeBlock = block
305
352
  }
306
353
  // Block now refers to the procedure prototype block, it is safe to proceed.
307
- ScratchProcedures.externalProcedureDefCallback!(
308
- prototypeBlock.mutationToDom!(),
309
- editProcedureCallbackFactory(prototypeBlock),
310
- )
354
+ const callback = ScratchProcedures.externalProcedureDefCallback
355
+ if (!callback) {
356
+ throw new Error('ScratchProcedures.externalProcedureDefCallback is not set')
357
+ }
358
+ if (!prototypeBlock.mutationToDom) {
359
+ throw new Error(`Expected mutationToDom on block ${prototypeBlock.id} (${prototypeBlock.type})`)
360
+ }
361
+ callback(prototypeBlock.mutationToDom(), editProcedureCallbackFactory(prototypeBlock))
311
362
  }
312
363
 
313
364
  /**
@@ -349,15 +400,14 @@ function makeEditOption(block: Blockly.BlockSvg): Blockly.ContextMenuRegistry.Co
349
400
  * procedure.
350
401
  * @returns True if the custom procedure was deleted, false otherwise.
351
402
  */
352
- function deleteProcedureDefCallback(procCode: string, definitionRoot: Blockly.BlockSvg): boolean {
403
+ function deleteProcedureDefCallback(procCode: string, definitionRoot: Blockly.Block): boolean {
353
404
  const callers = getCallers(procCode, definitionRoot.workspace, definitionRoot, false /* allowRecursive */)
354
405
  if (callers.length > 0) {
355
406
  return false
356
407
  }
357
408
 
358
- const workspace = definitionRoot.workspace
359
409
  // Bypass the checkAndDelete provided by the procedure block mixin
360
- Blockly.BlockSvg.prototype.checkAndDelete.call(definitionRoot)
410
+ Blockly.BlockSvg.prototype.checkAndDelete.call(definitionRoot as Blockly.BlockSvg)
361
411
  return true
362
412
  }
363
413
 
@@ -366,7 +416,7 @@ function deleteProcedureDefCallback(procCode: string, definitionRoot: Blockly.Bl
366
416
  * @param block The block to check.
367
417
  * @returns True if the block is a procedure block, otherwise false.
368
418
  */
369
- export function isProcedureBlock(block: Blockly.BlockSvg): block is ProcedureBlock {
419
+ export function isProcedureBlock(block: Blockly.Block): block is ProcedureBlock {
370
420
  return (
371
421
  block.type === Constants.PROCEDURES_CALL_BLOCK_TYPE ||
372
422
  block.type === Constants.PROCEDURES_DECLARATION_BLOCK_TYPE ||
@@ -378,7 +428,7 @@ export function isProcedureBlock(block: Blockly.BlockSvg): block is ProcedureBlo
378
428
  * Interface for procedure blocks, which have the getProcCode method added
379
429
  * through an extension.
380
430
  */
381
- interface ProcedureBlock extends Blockly.BlockSvg {
431
+ interface ProcedureBlock extends Blockly.Block {
382
432
  getProcCode(): string
383
433
  }
384
434
 
@@ -13,13 +13,14 @@ export class RecyclableBlockFlyoutInflater extends BlocklyRecyclableBlockFlyoutI
13
13
  /**
14
14
  * Creates a block on the flyout workspace from the given block definition.
15
15
  * @param state A JSON representation of a block to load.
16
- * @param flyoutWorkspace The workspace on which the block will be inflated.
16
+ * @param flyout The flyout on which the block will be inflated.
17
17
  * @returns The newly created block.
18
18
  */
19
- load(state: Blockly.utils.toolbox.BlockInfo, flyoutWorkspace: Blockly.WorkspaceSvg): Blockly.FlyoutItem {
20
- const flyoutItem = super.load(state, flyoutWorkspace)
19
+ load(state: object, flyout: Blockly.IFlyout): Blockly.FlyoutItem {
20
+ const flyoutItem = super.load(state, flyout)
21
21
  const block = flyoutItem.getElement()
22
- if ('checkboxInFlyout' in block && block.checkboxInFlyout) {
22
+ const flyoutWorkspace = flyout.getWorkspace()
23
+ if (block instanceof Blockly.BlockSvg && 'checkboxInFlyout' in block && block.checkboxInFlyout === true) {
23
24
  block.moveBy(
24
25
  (flyoutWorkspace.RTL ? -1 : 1) * (CheckboxBubble.CHECKBOX_SIZE + CheckboxBubble.CHECKBOX_MARGIN),
25
26
  0,
@@ -33,7 +33,7 @@ const setVisibility = (element: SVGElement, visible: boolean) => {
33
33
  * Owned by the PathObject with similar lifetime.
34
34
  */
35
35
  export class CatFace {
36
- faceGroup_!: SVGElement
36
+ faceGroup_: SVGElement | null = null
37
37
  parts_ = {} as Record<FacePart, SVGElement>
38
38
  pathEarState: CatPathState
39
39
  constants_: ConstantProvider
@@ -52,6 +52,9 @@ export class Drawer extends ClassicDrawer {
52
52
  return super.makeReplacementTop_()
53
53
  }
54
54
  const pathObject = this.block_.pathObject as PathObject
55
- return this.constants_.makeCatPath(this.info_.width, pathObject.catFace!.pathEarState)
55
+ if (!pathObject.catFace) {
56
+ throw new Error('[renderer/cat/drawer] Missing catFace while drawing hat block')
57
+ }
58
+ return this.constants_.makeCatPath(this.info_.width, pathObject.catFace.pathEarState)
56
59
  }
57
60
  }
@@ -27,10 +27,10 @@ export class ConstantProvider extends Blockly.zelos.ConstantProvider {
27
27
  root.style.setProperty(varKey, colour)
28
28
  } else {
29
29
  const style = {
30
- colourPrimary: 'colourQuaternary' in colour ? `${colour.colourQuaternary}` : colour.colourTertiary,
31
- colourSecondary: 'colourQuaternary' in colour ? `${colour.colourQuaternary}` : colour.colourTertiary,
32
- colourTertiary: 'colourQuaternary' in colour ? `${colour.colourQuaternary}` : colour.colourTertiary,
33
- colourQuaternary: 'colourQuaternary' in colour ? `${colour.colourQuaternary}` : colour.colourTertiary,
30
+ colourPrimary: 'colourQuaternary' in colour ? String(colour.colourQuaternary) : colour.colourTertiary,
31
+ colourSecondary: 'colourQuaternary' in colour ? String(colour.colourQuaternary) : colour.colourTertiary,
32
+ colourTertiary: 'colourQuaternary' in colour ? String(colour.colourQuaternary) : colour.colourTertiary,
33
+ colourQuaternary: 'colourQuaternary' in colour ? String(colour.colourQuaternary) : colour.colourTertiary,
34
34
  hat: '',
35
35
  }
36
36
  theme.setBlockStyle(`${key}_selected`, style)
@@ -52,38 +52,43 @@ export class ConstantProvider extends Blockly.zelos.ConstantProvider {
52
52
  * @returns The shape object for the given connection.
53
53
  */
54
54
  override shapeFor(connection: Blockly.RenderedConnection): ReturnType<Blockly.zelos.ConstantProvider['shapeFor']> {
55
+ const connectionType = connection.type as Blockly.ConnectionType
55
56
  let checks = connection.getCheck()
57
+ const hexagonal = this.HEXAGONAL
58
+ const rounded = this.ROUNDED
59
+ const squared = this.SQUARED
60
+ if (!hexagonal || !rounded || !squared) return super.shapeFor(connection)
56
61
  if (!checks && connection.targetConnection) {
57
62
  checks = connection.targetConnection.getCheck()
58
63
  }
59
64
 
60
- if (connection.type === Blockly.ConnectionType.OUTPUT_VALUE) {
65
+ if (connectionType === Blockly.ConnectionType.OUTPUT_VALUE) {
61
66
  const outputShape = connection.getSourceBlock().getOutputShape()
62
67
  if (outputShape !== null) {
63
68
  switch (outputShape) {
64
69
  case this.SHAPES.HEXAGONAL:
65
- return this.HEXAGONAL!
70
+ return hexagonal
66
71
  case this.SHAPES.ROUND:
67
- return this.ROUNDED!
72
+ return rounded
68
73
  case this.SHAPES.SQUARE:
69
- return this.SQUARED!
74
+ return squared
70
75
  }
71
76
  }
72
77
  }
73
78
 
74
79
  // For INPUT_VALUE (and OUTPUT_VALUE fallthrough), use connection checks.
75
- if (checks?.includes('Boolean')) return this.HEXAGONAL!
76
- if (checks?.includes('Number')) return this.ROUNDED!
77
- if (checks?.includes('String')) return this.ROUNDED!
80
+ if (checks?.includes('Boolean')) return hexagonal
81
+ if (checks?.includes('Number')) return rounded
82
+ if (checks?.includes('String')) return rounded
78
83
  // For INPUT_VALUE or OUTPUT_VALUE with unrecognized checks, default to
79
84
  // ROUNDED. Don't call super.shapeFor() here: the base implementation
80
85
  // uses getSourceBlock().getOutputShape(), which would incorrectly return
81
86
  // HEXAGONAL for inputs inside Boolean reporters (e.g. `<a = b>`).
82
87
  if (
83
- connection.type === Blockly.ConnectionType.INPUT_VALUE ||
84
- connection.type === Blockly.ConnectionType.OUTPUT_VALUE
88
+ connectionType === Blockly.ConnectionType.INPUT_VALUE ||
89
+ connectionType === Blockly.ConnectionType.OUTPUT_VALUE
85
90
  ) {
86
- return this.ROUNDED!
91
+ return rounded
87
92
  }
88
93
  return super.shapeFor(connection)
89
94
  }
@@ -64,7 +64,8 @@ export class Drawer extends Blockly.zelos.Drawer {
64
64
  */
65
65
  override drawConnectionHighlightPath(measurable: Blockly.blockRendering.Connection) {
66
66
  const conn = measurable.connectionModel
67
- if (conn.type === Blockly.ConnectionType.INPUT_VALUE && measurable.isDynamicShape) {
67
+ const connectionType = conn.type as Blockly.ConnectionType
68
+ if (connectionType === Blockly.ConnectionType.INPUT_VALUE && measurable.isDynamicShape) {
68
69
  const input = measurable as Blockly.blockRendering.InlineInput
69
70
  const EXPAND_X = 0.5
70
71
  const EXPAND_Y = 2
@@ -36,16 +36,20 @@ export class RenderInfo extends Blockly.zelos.RenderInfo {
36
36
  // bowler hat block.
37
37
  // Bowler hat blocks always have exactly one statement row and one input
38
38
  // element, so these find() calls are guaranteed to succeed.
39
- const statementRow = this.rows.find((r) => r.hasStatement)!
40
- this.width =
41
- statementRow.widthWithConnectedBlocks -
42
- statementRow.elements.find((e) => Blockly.blockRendering.Types.isInput(e))!.width +
43
- this.constants_.MEDIUM_PADDING
39
+ const statementRow = this.rows.find((r) => r.hasStatement)
40
+ const input = statementRow?.elements.find((e) => Blockly.blockRendering.Types.isInput(e))
41
+ if (!statementRow || !input) {
42
+ throw new Error('[renderer/render_info] Missing statement row or input for bowler hat block')
43
+ }
44
+ this.width = statementRow.widthWithConnectedBlocks - input.width + this.constants_.MEDIUM_PADDING
44
45
 
45
46
  // The bowler hat's width is the same as the block's width, so it can't
46
47
  // be derived from the constants like a normal hat and has to be set here.
47
48
  // populateTopRow_ always adds a hat element for bowler hat blocks.
48
- const hat = this.topRow.elements.find((e) => Blockly.blockRendering.Types.isHat(e))!
49
+ const hat = this.topRow.elements.find((e) => Blockly.blockRendering.Types.isHat(e))
50
+ if (!hat) {
51
+ throw new Error('[renderer/render_info] Missing hat measurable for bowler hat block')
52
+ }
49
53
  hat.width = this.width
50
54
  this.topRow.measure()
51
55
  }
@@ -57,7 +61,7 @@ export class RenderInfo extends Blockly.zelos.RenderInfo {
57
61
  ): number {
58
62
  if (
59
63
  this.isBowlerHatBlock() &&
60
- ((prev && Blockly.blockRendering.Types.isHat(prev)) || (next && Blockly.blockRendering.Types.isHat(next)))
64
+ (Blockly.blockRendering.Types.isHat(prev) || Blockly.blockRendering.Types.isHat(next))
61
65
  ) {
62
66
  // Bowler hat rows have no spacing/gaps, just the hat.
63
67
  return 0
@@ -82,8 +86,7 @@ export class RenderInfo extends Blockly.zelos.RenderInfo {
82
86
  this.block_.isScratchExtension &&
83
87
  Blockly.blockRendering.Types.isField(elem) &&
84
88
  elem.field instanceof Blockly.FieldImage &&
85
- elem.field === this.block_.inputList[0].fieldRow[0] &&
86
- this.block_.previousConnection
89
+ elem.field === this.block_.inputList[0].fieldRow[0]
87
90
  ) {
88
91
  // Vertically center the icon on extension blocks.
89
92
  return super.getElemCenterline_(row, elem) + this.constants_.GRID_UNIT
@@ -65,7 +65,7 @@ export class ScratchRenderer extends Blockly.zelos.Renderer {
65
65
  * @returns True if we should highlight the connection.
66
66
  */
67
67
  override shouldHighlightConnection(connection: Blockly.RenderedConnection): boolean {
68
- return connection.type === Blockly.ConnectionType.INPUT_VALUE
68
+ return (connection.type as Blockly.ConnectionType) === Blockly.ConnectionType.INPUT_VALUE
69
69
  }
70
70
  }
71
71
 
@@ -20,8 +20,6 @@
20
20
  * @file Utility methods for Scratch Blocks but not Blockly.
21
21
  * @author fenichel@google.com (Rachel Fenichel)
22
22
  */
23
- import * as Blockly from 'blockly/core'
24
-
25
23
  /**
26
24
  * Compare strings with natural number sorting.
27
25
  * @param str1 First input.