scratch-blocks 2.1.5 → 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.
- package/AGENTS.md +58 -14
- package/dist/main.mjs +1 -1
- package/dist/types/src/block_reporting.d.ts.map +1 -1
- package/dist/types/src/blocks/procedures.d.ts +2 -2
- package/dist/types/src/blocks/procedures.d.ts.map +1 -1
- package/dist/types/src/checkable_continuous_flyout.d.ts +6 -3
- package/dist/types/src/checkable_continuous_flyout.d.ts.map +1 -1
- package/dist/types/src/checkbox_bubble.d.ts +7 -7
- package/dist/types/src/checkbox_bubble.d.ts.map +1 -1
- package/dist/types/src/colours.d.ts.map +1 -1
- package/dist/types/src/context_menu_items.d.ts.map +1 -1
- package/dist/types/src/events/events_block_comment_base.d.ts +1 -1
- package/dist/types/src/events/events_block_comment_base.d.ts.map +1 -1
- package/dist/types/src/events/events_block_drag_end.d.ts +1 -1
- package/dist/types/src/events/events_block_drag_end.d.ts.map +1 -1
- package/dist/types/src/events/events_block_drag_outside.d.ts +1 -1
- package/dist/types/src/events/events_block_drag_outside.d.ts.map +1 -1
- package/dist/types/src/fields/field_colour_slider.d.ts.map +1 -1
- package/dist/types/src/fields/field_matrix.d.ts.map +1 -1
- package/dist/types/src/fields/field_note.d.ts +6 -4
- package/dist/types/src/fields/field_note.d.ts.map +1 -1
- package/dist/types/src/fields/field_textinput_removable.d.ts.map +1 -1
- package/dist/types/src/fields/field_variable_getter.d.ts.map +1 -1
- package/dist/types/src/fields/field_vertical_separator.d.ts.map +1 -1
- package/dist/types/src/fields/scratch_field_angle.d.ts.map +1 -1
- package/dist/types/src/fields/scratch_field_dropdown.d.ts.map +1 -1
- package/dist/types/src/fields/scratch_field_number.d.ts.map +1 -1
- package/dist/types/src/fields/scratch_field_variable.d.ts +1 -0
- package/dist/types/src/fields/scratch_field_variable.d.ts.map +1 -1
- package/dist/types/src/flyout_checkbox_icon.d.ts +5 -5
- package/dist/types/src/flyout_checkbox_icon.d.ts.map +1 -1
- package/dist/types/src/glows.d.ts.map +1 -1
- package/dist/types/src/procedures.d.ts +4 -4
- package/dist/types/src/procedures.d.ts.map +1 -1
- package/dist/types/src/recyclable_block_flyout_inflater.d.ts +2 -2
- package/dist/types/src/recyclable_block_flyout_inflater.d.ts.map +1 -1
- package/dist/types/src/renderer/cat/cat_face.d.ts +1 -1
- package/dist/types/src/renderer/cat/cat_face.d.ts.map +1 -1
- package/dist/types/src/renderer/cat/drawer.d.ts.map +1 -1
- package/dist/types/src/renderer/constants.d.ts.map +1 -1
- package/dist/types/src/renderer/drawer.d.ts.map +1 -1
- package/dist/types/src/renderer/render_info.d.ts.map +1 -1
- package/dist/types/src/scratch_blocks_utils.d.ts +22 -0
- package/dist/types/src/scratch_blocks_utils.d.ts.map +1 -1
- package/dist/types/src/scratch_comment_bubble.d.ts +4 -4
- package/dist/types/src/scratch_comment_bubble.d.ts.map +1 -1
- package/dist/types/src/scratch_comment_icon.d.ts +1 -1
- package/dist/types/src/scratch_comment_icon.d.ts.map +1 -1
- package/dist/types/src/scratch_continuous_category.d.ts +3 -1
- package/dist/types/src/scratch_continuous_category.d.ts.map +1 -1
- package/dist/types/src/scratch_continuous_toolbox.d.ts +2 -1
- package/dist/types/src/scratch_continuous_toolbox.d.ts.map +1 -1
- package/dist/types/src/status_indicator_label.d.ts +3 -3
- package/dist/types/src/status_indicator_label.d.ts.map +1 -1
- package/dist/types/src/status_indicator_label_flyout_inflater.d.ts.map +1 -1
- package/dist/types/src/variables.d.ts +1 -1
- package/dist/types/src/variables.d.ts.map +1 -1
- package/dist/types/src/workspace_block_lookup.d.ts +4 -0
- package/dist/types/src/workspace_block_lookup.d.ts.map +1 -0
- package/eslint.config.mjs +21 -28
- package/package.json +1 -1
- package/src/block_reporting.ts +5 -5
- package/src/blocks/control.ts +5 -5
- package/src/blocks/event.ts +1 -1
- package/src/blocks/motion.ts +2 -2
- package/src/blocks/procedures.ts +162 -69
- package/src/blocks/sensing.ts +0 -1
- package/src/blocks/vertical_extensions.ts +11 -8
- package/src/checkable_continuous_flyout.ts +45 -12
- package/src/checkbox_bubble.ts +7 -7
- package/src/colours.ts +4 -2
- package/src/context_menu_items.ts +41 -16
- package/src/data_category.ts +11 -3
- package/src/events/events_block_comment_base.ts +5 -1
- package/src/events/events_block_comment_change.ts +5 -1
- package/src/events/events_block_comment_collapse.ts +6 -2
- package/src/events/events_block_comment_create.ts +5 -1
- package/src/events/events_block_comment_move.ts +6 -2
- package/src/events/events_block_comment_resize.ts +6 -2
- package/src/events/events_block_drag_end.ts +5 -1
- package/src/events/events_block_drag_outside.ts +5 -1
- package/src/events/events_scratch_variable_create.ts +5 -1
- package/src/fields/field_colour_slider.ts +3 -5
- package/src/fields/field_matrix.ts +33 -17
- package/src/fields/field_note.ts +56 -20
- package/src/fields/field_textinput_removable.ts +13 -4
- package/src/fields/field_variable_getter.ts +20 -6
- package/src/fields/field_vertical_separator.ts +5 -1
- package/src/fields/scratch_field_angle.ts +32 -21
- package/src/fields/scratch_field_dropdown.ts +6 -2
- package/src/fields/scratch_field_number.ts +22 -13
- package/src/fields/scratch_field_variable.ts +26 -12
- package/src/flyout_checkbox_icon.ts +9 -5
- package/src/glows.ts +5 -5
- package/src/index.ts +18 -6
- package/src/procedures.ts +92 -42
- package/src/recyclable_block_flyout_inflater.ts +5 -4
- package/src/renderer/cat/cat_face.ts +1 -1
- package/src/renderer/cat/drawer.ts +4 -1
- package/src/renderer/constants.ts +19 -14
- package/src/renderer/drawer.ts +2 -1
- package/src/renderer/render_info.ts +12 -9
- package/src/renderer/renderer.ts +1 -1
- package/src/scratch_blocks_utils.ts +0 -2
- package/src/scratch_c_block_wrap.ts +37 -21
- package/src/scratch_comment_bubble.ts +30 -19
- package/src/scratch_comment_icon.ts +9 -12
- package/src/scratch_continuous_category.ts +20 -11
- package/src/scratch_continuous_toolbox.ts +12 -3
- package/src/scratch_dragger.ts +2 -2
- package/src/scratch_variable_map.ts +1 -1
- package/src/status_indicator_label.ts +13 -9
- package/src/status_indicator_label_flyout_inflater.ts +2 -1
- package/src/variables.ts +21 -14
- package/src/workspace_block_lookup.ts +14 -0
- package/src/xml.ts +1 -1
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 = (
|
|
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 ?
|
|
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
|
|
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(
|
|
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
|
-
|
|
55
|
+
setBubbleVisible(_visible: boolean): Promise<void> {
|
|
56
|
+
return Promise.resolve()
|
|
57
|
+
}
|
|
56
58
|
|
|
57
|
-
initView(
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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')
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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) =>
|
|
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.
|
|
118
|
-
definitionRoot: Blockly.
|
|
125
|
+
workspace: Blockly.Workspace,
|
|
126
|
+
definitionRoot: Pick<Blockly.Block, 'id'>,
|
|
119
127
|
allowRecursive: boolean,
|
|
120
|
-
):
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
.
|
|
129
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
const
|
|
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.
|
|
185
|
+
function getDefineBlock(procCode: string, workspace: Blockly.Workspace): Blockly.BlockSvg | undefined {
|
|
176
186
|
// Assume that a procedure definition is a top block.
|
|
177
|
-
|
|
187
|
+
for (const block of workspace.getTopBlocks(false)) {
|
|
178
188
|
if (block.type === Constants.PROCEDURES_DEFINITION_BLOCK_TYPE) {
|
|
179
|
-
const
|
|
180
|
-
|
|
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
|
-
|
|
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.
|
|
218
|
+
function getPrototypeBlock(procCode: string, workspace: Blockly.Workspace): Blockly.BlockSvg | undefined {
|
|
194
219
|
const defineBlock = getDefineBlock(procCode, workspace)
|
|
195
220
|
if (defineBlock) {
|
|
196
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
309
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
16
|
+
* @param flyout The flyout on which the block will be inflated.
|
|
17
17
|
* @returns The newly created block.
|
|
18
18
|
*/
|
|
19
|
-
load(state:
|
|
20
|
-
const flyoutItem = super.load(state,
|
|
19
|
+
load(state: object, flyout: Blockly.IFlyout): Blockly.FlyoutItem {
|
|
20
|
+
const flyoutItem = super.load(state, flyout)
|
|
21
21
|
const block = flyoutItem.getElement()
|
|
22
|
-
|
|
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_
|
|
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
|
-
|
|
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 ?
|
|
31
|
-
colourSecondary: 'colourQuaternary' in colour ?
|
|
32
|
-
colourTertiary: 'colourQuaternary' in colour ?
|
|
33
|
-
colourQuaternary: 'colourQuaternary' in colour ?
|
|
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 (
|
|
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
|
|
70
|
+
return hexagonal
|
|
66
71
|
case this.SHAPES.ROUND:
|
|
67
|
-
return
|
|
72
|
+
return rounded
|
|
68
73
|
case this.SHAPES.SQUARE:
|
|
69
|
-
return
|
|
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
|
|
76
|
-
if (checks?.includes('Number')) return
|
|
77
|
-
if (checks?.includes('String')) return
|
|
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
|
-
|
|
84
|
-
|
|
88
|
+
connectionType === Blockly.ConnectionType.INPUT_VALUE ||
|
|
89
|
+
connectionType === Blockly.ConnectionType.OUTPUT_VALUE
|
|
85
90
|
) {
|
|
86
|
-
return
|
|
91
|
+
return rounded
|
|
87
92
|
}
|
|
88
93
|
return super.shapeFor(connection)
|
|
89
94
|
}
|
package/src/renderer/drawer.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
(
|
|
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
|
package/src/renderer/renderer.ts
CHANGED
|
@@ -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
|
|