scratch-blocks 2.1.15 → 2.1.17
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/dist/main.mjs +1 -1
- package/dist/types/src/checkable_continuous_flyout.d.ts.map +1 -1
- package/dist/types/src/scratch_block_paster.d.ts.map +1 -1
- package/dist/types/src/scratch_blocks_utils.d.ts +10 -0
- package/dist/types/src/scratch_blocks_utils.d.ts.map +1 -1
- package/dist/types/src/xml.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/checkable_continuous_flyout.ts +7 -5
- package/src/scratch_block_paster.ts +7 -0
- package/src/scratch_blocks_utils.ts +25 -0
- package/src/xml.ts +69 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkable_continuous_flyout.d.ts","sourceRoot":"","sources":["../../../src/checkable_continuous_flyout.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,6BAA6B,CAAA;AACpF,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"checkable_continuous_flyout.d.ts","sourceRoot":"","sources":["../../../src/checkable_continuous_flyout.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,gBAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,6BAA6B,CAAA;AACpF,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AAkBvC,qBAAa,yBAA0B,SAAQ,gBAAgB;IAC7D,UAAkB,SAAS,EAAE,MAAM,CAAA;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IAErB;;;OAGG;gBACS,gBAAgB,EAAE,OAAO,CAAC,OAAO;IAQ7C;;;;OAIG;IACH,SAAS,CAAC,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,QAAQ;IAWhD;;;;OAIG;IACH,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO;IAahD,cAAc;IAId,QAAQ;IAIR,SAAS,CAAC,eAAe;IAwBzB;;;;OAIG;IACH,SAAS,CAAC,kBAAkB,CAAC,IAAI,EAAE,OAAO,CAAC,UAAU,GAAG,IAAI,IAAI,eAAe;IAO/E;;OAEG;IACH,oBAAoB;IASpB,QAAQ,CAAC,QAAQ,EAAE,MAAM;CAG1B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scratch_block_paster.d.ts","sourceRoot":"","sources":["../../../src/scratch_block_paster.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"scratch_block_paster.d.ts","sourceRoot":"","sources":["../../../src/scratch_block_paster.ts"],"names":[],"mappings":"AAiDA;;;GAGG;AACH,wBAAgB,0BAA0B,SAGzC"}
|
|
@@ -20,6 +20,16 @@
|
|
|
20
20
|
* @file Utility methods for Scratch Blocks but not Blockly.
|
|
21
21
|
* @author fenichel@google.com (Rachel Fenichel)
|
|
22
22
|
*/
|
|
23
|
+
import type * as Blockly from 'blockly/core';
|
|
24
|
+
/**
|
|
25
|
+
* Recursively strip `id` properties from a serialized block state tree
|
|
26
|
+
* so that every block (including shadows and nested inputs) gets a fresh
|
|
27
|
+
* ID when deserialized onto the workspace. This prevents two blocks from
|
|
28
|
+
* sharing the same shadow block in the VM, which would cause deleting
|
|
29
|
+
* one to destroy the other's shadow.
|
|
30
|
+
* @param state A serialized block state object.
|
|
31
|
+
*/
|
|
32
|
+
export declare function stripIds(state: Blockly.serialization.blocks.State): void;
|
|
23
33
|
/**
|
|
24
34
|
* Compare strings with natural number sorting.
|
|
25
35
|
* @param str1 First input.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scratch_blocks_utils.d.ts","sourceRoot":"","sources":["../../../src/scratch_blocks_utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH;;;GAGG;AACH;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAKjE"}
|
|
1
|
+
{"version":3,"file":"scratch_blocks_utils.d.ts","sourceRoot":"","sources":["../../../src/scratch_blocks_utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH;;;GAGG;AACH,OAAO,KAAK,KAAK,OAAO,MAAM,cAAc,CAAA;AAE5C;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,KAAK,GAAG,IAAI,CAaxE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAKjE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"xml.d.ts","sourceRoot":"","sources":["../../../src/xml.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"xml.d.ts","sourceRoot":"","sources":["../../../src/xml.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AAwEvC;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC,YAAY,GAAG,MAAM,EAAE,CAkDpG"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scratch-blocks",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.17",
|
|
4
4
|
"description": "Scratch Blocks is a library for building creative computing interfaces.",
|
|
5
5
|
"author": "Massachusetts Institute of Technology",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@vitest/browser": "4.1.4",
|
|
42
42
|
"@vitest/browser-playwright": "4.1.4",
|
|
43
43
|
"eslint": "9.39.4",
|
|
44
|
-
"eslint-config-scratch": "14.1.
|
|
44
|
+
"eslint-config-scratch": "14.1.12",
|
|
45
45
|
"husky": "9.1.7",
|
|
46
46
|
"playwright": "1.59.1",
|
|
47
47
|
"prettier": "3.8.3",
|
|
@@ -5,6 +5,7 @@
|
|
|
5
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
|
+
import { stripIds } from './scratch_blocks_utils'
|
|
8
9
|
import { StatusIndicatorLabel } from './status_indicator_label'
|
|
9
10
|
import { STATUS_INDICATOR_LABEL_TYPE } from './status_indicator_label_flyout_inflater'
|
|
10
11
|
|
|
@@ -44,11 +45,12 @@ export class CheckableContinuousFlyout extends ContinuousFlyout {
|
|
|
44
45
|
*/
|
|
45
46
|
protected serializeBlock(block: Blockly.BlockSvg) {
|
|
46
47
|
const json = super.serializeBlock(block)
|
|
47
|
-
//
|
|
48
|
-
// placed on the workspace.
|
|
49
|
-
//
|
|
50
|
-
// to
|
|
51
|
-
|
|
48
|
+
// Strip all IDs so every block in the tree (including shadows) gets a
|
|
49
|
+
// fresh ID when placed on the workspace. Without this, disposed shadows
|
|
50
|
+
// from a previous copy can reuse the flyout's IDs, causing two workspace
|
|
51
|
+
// blocks to share the same shadow in the VM. Deleting one then destroys
|
|
52
|
+
// the other's shadow (bug 878291).
|
|
53
|
+
stripIds(json)
|
|
52
54
|
return json
|
|
53
55
|
}
|
|
54
56
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
5
|
import * as Blockly from 'blockly/core'
|
|
6
|
+
import { stripIds } from './scratch_blocks_utils'
|
|
6
7
|
import type { ScratchCommentIcon } from './scratch_comment_icon'
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -22,6 +23,12 @@ class ScratchBlockPaster extends Blockly.clipboard.BlockPaster {
|
|
|
22
23
|
workspace: Blockly.WorkspaceSvg,
|
|
23
24
|
coordinate: Blockly.utils.Coordinate,
|
|
24
25
|
) {
|
|
26
|
+
// Strip all block IDs so that every block in the pasted tree (including
|
|
27
|
+
// shadows) gets a fresh ID. Without this, disposed shadows from the
|
|
28
|
+
// original block can reuse the same ID, causing the VM to think both
|
|
29
|
+
// blocks share the same shadow. Deleting one then destroys the other's
|
|
30
|
+
// shadow (forum topic 878291).
|
|
31
|
+
stripIds(copyData.blockState)
|
|
25
32
|
const block = super.paste(copyData, workspace, coordinate)
|
|
26
33
|
if (block?.type === 'argument_reporter_boolean' || block?.type === 'argument_reporter_string_number') {
|
|
27
34
|
block.setDragStrategy(new Blockly.dragging.BlockDragStrategy(block))
|
|
@@ -20,6 +20,31 @@
|
|
|
20
20
|
* @file Utility methods for Scratch Blocks but not Blockly.
|
|
21
21
|
* @author fenichel@google.com (Rachel Fenichel)
|
|
22
22
|
*/
|
|
23
|
+
import type * as Blockly from 'blockly/core'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Recursively strip `id` properties from a serialized block state tree
|
|
27
|
+
* so that every block (including shadows and nested inputs) gets a fresh
|
|
28
|
+
* ID when deserialized onto the workspace. This prevents two blocks from
|
|
29
|
+
* sharing the same shadow block in the VM, which would cause deleting
|
|
30
|
+
* one to destroy the other's shadow.
|
|
31
|
+
* @param state A serialized block state object.
|
|
32
|
+
*/
|
|
33
|
+
export function stripIds(state: Blockly.serialization.blocks.State): void {
|
|
34
|
+
delete state.id
|
|
35
|
+
if (state.inputs) {
|
|
36
|
+
for (const inputName in state.inputs) {
|
|
37
|
+
const conn = state.inputs[inputName]
|
|
38
|
+
if (conn.shadow) stripIds(conn.shadow)
|
|
39
|
+
if (conn.block) stripIds(conn.block)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (state.next) {
|
|
43
|
+
if (state.next.shadow) stripIds(state.next.shadow)
|
|
44
|
+
if (state.next.block) stripIds(state.next.block)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
23
48
|
/**
|
|
24
49
|
* Compare strings with natural number sorting.
|
|
25
50
|
* @param str1 First input.
|
package/src/xml.ts
CHANGED
|
@@ -5,6 +5,75 @@
|
|
|
5
5
|
import * as Blockly from 'blockly/core'
|
|
6
6
|
import { ScratchVariableModel } from './scratch_variable_model'
|
|
7
7
|
|
|
8
|
+
// Blockly's blockToDom has a bug where the `empty` flag for an input's
|
|
9
|
+
// container element is only set to false when a non-shadow target block
|
|
10
|
+
// exists. If a connection has shadow DOM but no target block (which can
|
|
11
|
+
// happen transiently during connect/disconnect operations or if shadow
|
|
12
|
+
// respawn fails), the shadow clone IS appended to the <value> container
|
|
13
|
+
// but the container is never appended to the output because `empty`
|
|
14
|
+
// stays true. This causes the input to silently disappear from the XML,
|
|
15
|
+
// which corrupts the VM's block representation on the next save/load.
|
|
16
|
+
//
|
|
17
|
+
// We fix this by wrapping blockToDom: after the original runs, we check
|
|
18
|
+
// each value/statement input. If the connection has shadow state but the
|
|
19
|
+
// output XML is missing the corresponding <value>/<statement> element,
|
|
20
|
+
// we serialize the shadow ourselves and inject it.
|
|
21
|
+
const originalBlockToDom = Blockly.Xml.blockToDom.bind(Blockly.Xml)
|
|
22
|
+
|
|
23
|
+
Blockly.Xml.blockToDom = function blockToDomFixed(
|
|
24
|
+
block: Blockly.Block,
|
|
25
|
+
opt_noId?: boolean,
|
|
26
|
+
): Element | DocumentFragment {
|
|
27
|
+
const result = originalBlockToDom(block, opt_noId)
|
|
28
|
+
if (!(result instanceof Element)) return result
|
|
29
|
+
|
|
30
|
+
for (const input of block.inputList) {
|
|
31
|
+
if (!input.connection) continue
|
|
32
|
+
|
|
33
|
+
const name = input.name
|
|
34
|
+
// Check if this input already appears in the serialized XML.
|
|
35
|
+
// Use localName for case-insensitive comparison across DOM implementations.
|
|
36
|
+
const alreadySerialized = Array.from(result.children).some((child) => {
|
|
37
|
+
const tag = child.localName
|
|
38
|
+
return (tag === 'value' || tag === 'statement') && child.getAttribute('name') === name
|
|
39
|
+
})
|
|
40
|
+
if (alreadySerialized) continue
|
|
41
|
+
|
|
42
|
+
// The input is missing from the XML. Check if there's shadow state
|
|
43
|
+
// that should have been included.
|
|
44
|
+
if (!input.connection.getShadowState()) continue
|
|
45
|
+
|
|
46
|
+
// If shadow DOM is still available on the connection, clone that DOM
|
|
47
|
+
// into a new container and append it to the serialized output.
|
|
48
|
+
const existingShadowDom = input.connection.getShadowDom()
|
|
49
|
+
if (existingShadowDom) {
|
|
50
|
+
const tagName = input.type === Blockly.inputs.inputTypes.VALUE ? 'value' : 'statement'
|
|
51
|
+
const doc = result.ownerDocument
|
|
52
|
+
const container = doc.createElementNS(result.namespaceURI, tagName)
|
|
53
|
+
container.setAttribute('name', name)
|
|
54
|
+
const importedShadow = doc.importNode(existingShadowDom, true)
|
|
55
|
+
if (opt_noId) {
|
|
56
|
+
importedShadow.removeAttribute('id')
|
|
57
|
+
for (const el of importedShadow.querySelectorAll('[id]')) {
|
|
58
|
+
el.removeAttribute('id')
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
container.appendChild(importedShadow)
|
|
62
|
+
result.appendChild(container)
|
|
63
|
+
console.warn(
|
|
64
|
+
`[scratch-blocks] blockToDom fix: recovered missing input "${name}" on ${block.type} (${block.id})`,
|
|
65
|
+
)
|
|
66
|
+
} else {
|
|
67
|
+
console.warn(
|
|
68
|
+
`[scratch-blocks] blockToDom fix: input "${name}" on ${block.type} (${block.id}) has shadow state` +
|
|
69
|
+
` but no shadow DOM — cannot recover`,
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return result
|
|
75
|
+
}
|
|
76
|
+
|
|
8
77
|
/**
|
|
9
78
|
* Clears the workspace and loads the given serialized state.
|
|
10
79
|
* @param xml XML representation of a Blockly workspace.
|