diff --git a/src/engine/adapter.js b/src/engine/adapter.js
index 57c413f3..90e401d6 100644
--- a/src/engine/adapter.js
+++ b/src/engine/adapter.js
@@ -114,11 +114,16 @@ function domToBlock (blockDOM, blocks, isTopBlock) {
case 'statement':
// Recursively generate block structure for input block.
domToBlock(childBlockNode, blocks, false);
+ if (childShadowNode && childBlockNode != childShadowNode) {
+ // Also generate the shadow block.
+ domToBlock(childShadowNode, blocks, false);
+ }
// Link this block's input to the child block.
var inputName = xmlChild.attribs.name;
block.inputs[inputName] = {
name: inputName,
- block: childBlockNode.attribs.id
+ block: childBlockNode.attribs.id,
+ shadow: childShadowNode ? childShadowNode.attribs.id : null
};
break;
case 'next':
diff --git a/src/engine/blocks.js b/src/engine/blocks.js
index 771ad9a9..a3d949a5 100644
--- a/src/engine/blocks.js
+++ b/src/engine/blocks.js
@@ -166,6 +166,10 @@ Blocks.prototype.blocklyListen = function (e, isFlyout, opt_runtime) {
});
break;
case 'delete':
+ // Don't accept delete events for shadow blocks being obscured.
+ if (this._blocks[e.blockId].shadow) {
+ return;
+ }
this.deleteBlock({
id: e.blockId
});
@@ -181,9 +185,13 @@ Blocks.prototype.blocklyListen = function (e, isFlyout, opt_runtime) {
* @param {boolean} opt_isFlyoutBlock Whether the block is in the flyout.
*/
Blocks.prototype.createBlock = function (block, opt_isFlyoutBlock) {
- // Create new block
+ // Does the block already exist?
+ // Could happen, e.g., for an unobscured shadow.
+ if (this._blocks.hasOwnProperty(block.id)) {
+ return;
+ }
+ // Create new block.
this._blocks[block.id] = block;
-
// Push block id to scripts array.
// Blocks are added as a top-level stack if they are marked as a top-block
// (if they were top-level XML in the event) and if they are not
@@ -239,11 +247,11 @@ Blocks.prototype.moveBlock = function (e) {
this._deleteScript(e.id);
// Otherwise, try to connect it in its new place.
if (e.newInput !== undefined) {
- // Moved to the new parent's input.
- this._blocks[e.newParent].inputs[e.newInput] = {
- name: e.newInput,
- block: e.id
- };
+ // Moved to the new parent's input.
+ // Don't obscure the shadow block.
+ var newInput = this._blocks[e.newParent].inputs[e.newInput];
+ newInput.name = e.newInput;
+ newInput.block = e.id;
} else {
// Moved to the new parent's next connection.
this._blocks[e.newParent].next = e.id;
@@ -272,6 +280,11 @@ Blocks.prototype.deleteBlock = function (e) {
if (block.inputs[input].block !== null) {
this.deleteBlock({id: block.inputs[input].block});
}
+ // Delete obscured shadow blocks.
+ if (block.inputs[input].shadow !== null &&
+ block.inputs[input].shadow !== block.inputs[input].block) {
+ this.deleteBlock({id: block.inputs[input].shadow});
+ }
}
// Delete any script starting with this block.
@@ -319,9 +332,16 @@ Blocks.prototype.blockToXML = function (blockId) {
for (var input in block.inputs) {
var blockInput = block.inputs[input];
// Only encode a value tag if the value input is occupied.
- if (blockInput.block) {
- xmlString += '' +
- this.blockToXML(blockInput.block) + '';
+ if (blockInput.block || blockInput.shadow) {
+ xmlString += '';
+ if (blockInput.block) {
+ xmlString += this.blockToXML(blockInput.block);
+ }
+ if (blockInput.shadow && blockInput.shadow != blockInput.block) {
+ // Obscured shadow.
+ xmlString += this.blockToXML(blockInput.shadow);
+ }
+ xmlString += '';
}
}
// Add any fields on this block.
diff --git a/src/import/sb2import.js b/src/import/sb2import.js
index beab39dd..75bd418f 100644
--- a/src/import/sb2import.js
+++ b/src/import/sb2import.js
@@ -183,6 +183,7 @@ function parseBlock (sb2block) {
opcode: blockMetadata.opcode, // Converted, e.g. "motion_movesteps".
inputs: {}, // Inputs to this block and the blocks they point to.
fields: {}, // Fields on this block and their values.
+ next: null, // Next block.
shadow: false, // No shadow blocks in an SB2 by default.
children: [] // Store any generated children, flattened in `flatten`.
};
@@ -192,13 +193,16 @@ function parseBlock (sb2block) {
for (var i = 0; i < blockMetadata.argMap.length; i++) {
var expectedArg = blockMetadata.argMap[i];
var providedArg = sb2block[i + 1]; // (i = 0 is opcode)
+ // Whether the input is obscuring a shadow.
+ var shadowObscured = false;
// Positional argument is an input.
if (expectedArg.type == 'input') {
// Create a new block and input metadata.
var inputUid = uid();
activeBlock.inputs[expectedArg.inputName] = {
name: expectedArg.inputName,
- block: inputUid
+ block: null,
+ shadow: null
};
if (typeof providedArg == 'object') {
// Block or block list occupies the input.
@@ -210,37 +214,61 @@ function parseBlock (sb2block) {
// Single block occupies the input.
innerBlocks = [parseBlock(providedArg)];
}
- activeBlock.inputs[expectedArg.inputName] = {
- name: expectedArg.inputName,
- block: innerBlocks[0].id
- };
+ // Obscures any shadow.
+ shadowObscured = true;
+ activeBlock.inputs[expectedArg.inputName].block = (
+ innerBlocks[0].id
+ );
activeBlock.children = (
activeBlock.children.concat(innerBlocks)
);
- } else if (expectedArg.inputOp) {
- // Unoccupied input. Generate a shadow block to occupy it.
- var fieldName = expectedArg.inputName;
- if (expectedArg.inputOp == 'math_number') {
- fieldName = 'NUM';
- } else if (expectedArg.inputOp == 'text') {
- fieldName = 'TEXT';
- } else if (expectedArg.inputOp == 'colour_picker') {
- fieldName = 'COLOR';
+ }
+ // Generate a shadow block to occupy the input.
+ // The shadow block is either visible or obscured.
+ if (!expectedArg.inputOp) {
+ // No editable shadow input; e.g., for a boolean.
+ continue;
+ }
+ // Each shadow has a field generated for it automatically.
+ // Value to be filled in the field.
+ var fieldValue = providedArg;
+ // Shadows' field names match the input name, except for these:
+ var fieldName = expectedArg.inputName;
+ if (expectedArg.inputOp == 'math_number') {
+ fieldName = 'NUM';
+ // Fields are given Scratch 2.0 default values if obscured.
+ if (shadowObscured) {
+ fieldValue = 10;
}
- var fields = {};
- fields[fieldName] = {
- name: fieldName,
- value: providedArg
- };
- activeBlock.children.push({
- id: inputUid,
- opcode: expectedArg.inputOp,
- inputs: {},
- fields: fields,
- next: null,
- topLevel: false,
- shadow: true
- });
+ } else if (expectedArg.inputOp == 'text') {
+ fieldName = 'TEXT';
+ if (shadowObscured) {
+ fieldValue = '';
+ }
+ } else if (expectedArg.inputOp == 'colour_picker') {
+ fieldName = 'COLOR';
+ if (shadowObscured) {
+ fieldValue = '#990000';
+ }
+ }
+ var fields = {};
+ fields[fieldName] = {
+ name: fieldName,
+ value: fieldValue
+ };
+ activeBlock.children.push({
+ id: inputUid,
+ opcode: expectedArg.inputOp,
+ inputs: {},
+ fields: fields,
+ next: null,
+ topLevel: false,
+ shadow: true
+ });
+ activeBlock.inputs[expectedArg.inputName].shadow = inputUid;
+ // If no block occupying the input, alias the block to the shadow.
+ if (!activeBlock.inputs[expectedArg.inputName].block) {
+ activeBlock.inputs[expectedArg.inputName].block = inputUid;
}
} else if (expectedArg.type == 'field') {
// Add as a field on this block.
diff --git a/test/fixtures/events.json b/test/fixtures/events.json
index c1605cbd..d91370f6 100644
--- a/test/fixtures/events.json
+++ b/test/fixtures/events.json
@@ -59,5 +59,11 @@
"xml": {
"outerHTML": ""
}
+ },
+ "createobscuredshadow": {
+ "name": "block",
+ "xml": {
+ "outerHTML": ""
+ }
}
}
diff --git a/test/unit/adapter.js b/test/unit/adapter.js
index 63e82345..19289dac 100644
--- a/test/unit/adapter.js
+++ b/test/unit/adapter.js
@@ -55,7 +55,9 @@ test('create with branch', function (t) {
t.equal(result[0].topLevel, true);
// In branch
var branchBlockId = result[0].inputs['SUBSTACK']['block'];
+ var branchShadowId = result[0].inputs['SUBSTACK']['shadow'];
t.type(branchBlockId, 'string');
+ t.equal(branchShadowId, null);
// Find actual branch block
var branchBlock = null;
for (var i = 0; i < result.length; i++) {
@@ -83,6 +85,10 @@ test('create with two branches', function (t) {
var secondBranchBlockId = result[0].inputs['SUBSTACK2']['block'];
t.type(firstBranchBlockId, 'string');
t.type(secondBranchBlockId, 'string');
+ var firstBranchShadowBlockId = result[0].inputs['SUBSTACK']['shadow'];
+ var secondBranchShadowBlockId = result[0].inputs['SUBSTACK2']['shadow'];
+ t.equal(firstBranchShadowBlockId, null);
+ t.equal(secondBranchShadowBlockId, null);
// Find actual branch blocks
var firstBranchBlock = null;
var secondBranchBlock = null;
@@ -142,6 +148,13 @@ test('create with next connection', function (t) {
t.end();
});
+test('create with obscured shadow', function (t) {
+ var result = adapter(events.createobscuredshadow);
+ t.ok(Array.isArray(result));
+ t.equal(result.length, 4);
+ t.end();
+});
+
test('create with invalid block xml', function (t) {
// Entirely invalid block XML
var result = adapter(events.createinvalid);
diff --git a/test/unit/blocks.js b/test/unit/blocks.js
index bd2348c0..81fb0ad9 100644
--- a/test/unit/blocks.js
+++ b/test/unit/blocks.js
@@ -130,7 +130,8 @@ test('getBranch', function (t) {
inputs: {
SUBSTACK: {
name: 'SUBSTACK',
- block: 'foo2'
+ block: 'foo2',
+ shadow: null
}
},
topLevel: true
@@ -164,11 +165,13 @@ test('getBranch2', function (t) {
inputs: {
SUBSTACK: {
name: 'SUBSTACK',
- block: 'foo2'
+ block: 'foo2',
+ shadow: null
},
SUBSTACK2: {
name: 'SUBSTACK2',
- block: 'foo3'
+ block: 'foo3',
+ shadow: null
}
},
topLevel: true
@@ -416,11 +419,13 @@ test('delete inputs', function (t) {
inputs: {
input1: {
name: 'input1',
- block: 'foo2'
+ block: 'foo2',
+ shadow: 'foo2'
},
SUBSTACK: {
name: 'SUBSTACK',
- block: 'foo3'
+ block: 'foo3',
+ shadow: null
}
},
topLevel: true
@@ -433,6 +438,14 @@ test('delete inputs', function (t) {
inputs: {},
topLevel: false
});
+ b.createBlock({
+ id: 'foo5',
+ opcode: 'TEST_OBSCURED_SHADOW',
+ next: null,
+ fields: {},
+ inputs: {},
+ topLevel: false
+ });
b.createBlock({
id: 'foo3',
opcode: 'TEST_BLOCK',
@@ -441,7 +454,8 @@ test('delete inputs', function (t) {
inputs: {
subinput: {
name: 'subinput',
- block: 'foo4'
+ block: 'foo4',
+ shadow: 'foo5'
}
},
topLevel: false
@@ -461,6 +475,7 @@ test('delete inputs', function (t) {
t.type(b._blocks['foo2'], 'undefined');
t.type(b._blocks['foo3'], 'undefined');
t.type(b._blocks['foo4'], 'undefined');
+ t.type(b._blocks['foo5'], 'undefined');
t.equal(b._scripts.indexOf('foo'), -1);
t.equal(Object.keys(b._blocks).length, 0);
t.equal(b._scripts.length, 0);