feat: add serialization of child blocks (#5120)

* Add tests for serializing connected blocks

* Add serialization of child blocks

* Add tests for not serializing children

* Add options for not serializing children

* Fix types

* Change addNextBlocks to default to true

* Cleanup

* Fix types
This commit is contained in:
Beka Westberg
2021-08-06 07:55:04 -07:00
committed by alschmiedt
parent 8faa360b74
commit 56d3cb6c8f
3 changed files with 404 additions and 13 deletions

View File

@@ -15,11 +15,25 @@ goog.module.declareLegacyNamespace();
// eslint-disable-next-line no-unused-vars
const Block = goog.requireType('Blockly.Block');
// eslint-disable-next-line no-unused-vars
const Connection = goog.requireType('Blockly.Connection');
const Xml = goog.require('Blockly.Xml');
const inputTypes = goog.require('Blockly.inputTypes');
// TODO: Remove this once lint is fixed.
/* eslint-disable no-use-before-define */
/**
* Represents the state of a connection.
* @typedef {{
* shadow: (!State|undefined),
* block: (!State|undefined)
* }}
*/
var ConnectionState;
exports.ConnectionState = ConnectionState;
/**
* Represents the state of a given block.
* @typedef {{
@@ -35,7 +49,9 @@ const Block = goog.requireType('Blockly.Block');
* inline: (boolean|undefined),
* data: (string|undefined),
* extra-state: *,
* fields: (Object<string, *>|undefined),
* fields: (!Object<string, *>|undefined),
* inputs: (!Object<string, !ConnectionState>|undefined),
* next: (!ConnectionState|undefined)
* }}
*/
var State;
@@ -44,14 +60,27 @@ exports.State = State;
/**
* Returns the state of the given block as a plain JavaScript object.
* @param {!Block} block The block to serialize.
* @param {{addCoordinates: (boolean|undefined)}=} param1
* addCoordinates: If true the coordinates of the block are added to the
* @param {{addCoordinates: (boolean|undefined), addInputBlocks:
* (boolean|undefined), addNextBlocks: (boolean|undefined)}=} param1
* addCoordinates: If true, the coordinates of the block are added to the
* serialized state. False by default.
* addinputBlocks: If true, children of the block which are connected to
* inputs will be serialized. True by default.
* addNextBlocks: If true, children of the block which are connected to the
* block's next connection (if it exists) will be serialized.
* True by default.
* @return {?State} The serialized state of the
* block, or null if the block could not be serialied (eg it was an
* insertion marker).
*/
const save = function(block, {addCoordinates = false} = {}) {
const save = function(
block,
{
addCoordinates = false,
addInputBlocks = true,
addNextBlocks = true
} = {}
) {
if (block.isInsertionMarker()) {
return null;
}
@@ -62,11 +91,17 @@ const save = function(block, {addCoordinates = false} = {}) {
};
if (addCoordinates) {
addCoords(block, state);
saveCoords(block, state);
}
saveAttributes(block, state);
saveExtraState(block, state);
saveFields(block, state);
if (addInputBlocks) {
saveInputBlocks(block, state);
}
if (addNextBlocks) {
saveNextBlocks(block, state);
}
addAttributes(block, state);
addExtraState(block, state);
addFields(block, state);
return state;
};
@@ -78,7 +113,7 @@ exports.save = save;
* @param {!Block} block The block to base the attributes on.
* @param {!State} state The state object to append to.
*/
const addAttributes = function(block, state) {
const saveAttributes = function(block, state) {
if (block.isCollapsed()) {
state['collapsed'] = true;
}
@@ -111,7 +146,7 @@ const addAttributes = function(block, state) {
* @param {!Block} block The block to base the coordinates on
* @param {!State} state The state object to append to
*/
const addCoords = function(block, state) {
const saveCoords = function(block, state) {
const workspace = block.workspace;
const xy = block.getRelativeToSurfaceXY();
state['x'] = Math.round(workspace.RTL ? workspace.getWidth() - xy.x : xy.x);
@@ -123,7 +158,7 @@ const addCoords = function(block, state) {
* @param {!Block} block The block to serialize the extra state of.
* @param {!State} state The state object to append to.
*/
const addExtraState = function(block, state) {
const saveExtraState = function(block, state) {
if (block.saveExtraState) {
const extraState = block.saveExtraState();
if (extraState !== null) {
@@ -137,7 +172,7 @@ const addExtraState = function(block, state) {
* @param {!Block} block The block to serialize the field state of.
* @param {!State} state The state object to append to.
*/
const addFields = function(block, state) {
const saveFields = function(block, state) {
let hasFieldState = false;
let fields = Object.create(null);
for (let i = 0; i < block.inputList.length; i++) {
@@ -154,3 +189,69 @@ const addFields = function(block, state) {
state['fields'] = fields;
}
};
/**
* Adds the state of all of the child blocks of the given block (which are
* connected to inputs) to the given state object.
* @param {!Block} block The block to serialize the input blocks of.
* @param {!State} state The state object to append to.
*/
const saveInputBlocks = function(block, state) {
const inputs = Object.create(null);
for (let i = 0; i < block.inputList.length; i++) {
const input = block.inputList[i];
if (input.type === inputTypes.DUMMY) {
continue;
}
const connectionState =
saveConnection(/** @type {!Connection} */ (input.connection));
if (connectionState) {
inputs[input.name] = connectionState;
}
}
if (Object.keys(inputs).length) {
state['inputs'] = inputs;
}
};
/**
* Adds the state of all of the next blocks of the given block to the given
* state object.
* @param {!Block} block The block to serialize the next blocks of.
* @param {!State} state The state object to append to.
*/
const saveNextBlocks = function(block, state) {
if (!block.nextConnection) {
return;
}
const connectionState = saveConnection(block.nextConnection);
if (connectionState) {
state['next'] = connectionState;
}
};
/**
* Returns the state of the given connection (ie the state of any connected
* shadow or real blocks).
* @param {!Connection} connection The connection to serialize the connected
* blocks of.
* @return {?ConnectionState} An object containing the state of any connected
* shadow block, or any connected real block.
*/
const saveConnection = function(connection) {
const shadow = connection.getShadowDom();
const child = connection.targetBlock();
if (!shadow && !child) {
return null;
}
var state = Object.create(null);
if (shadow) {
state['shadow'] = Xml.domToText(shadow)
.replace('xmlns="https://developers.google.com/blockly/xml"', '');
}
if (child) {
state['block'] = save(child);
}
return state;
};

View File

@@ -196,7 +196,7 @@ goog.addDependency('../../core/renderers/zelos/renderer.js', ['Blockly.zelos.Ren
goog.addDependency('../../core/requires.js', ['Blockly.requires'], ['Blockly', 'Blockly.Comment', 'Blockly.ContextMenuItems', 'Blockly.FieldAngle', 'Blockly.FieldCheckbox', 'Blockly.FieldColour', 'Blockly.FieldDropdown', 'Blockly.FieldImage', 'Blockly.FieldLabelSerializable', 'Blockly.FieldMultilineInput', 'Blockly.FieldNumber', 'Blockly.FieldTextInput', 'Blockly.FieldVariable', 'Blockly.FlyoutButton', 'Blockly.Generator', 'Blockly.HorizontalFlyout', 'Blockly.Mutator', 'Blockly.ShortcutItems', 'Blockly.Themes.Classic', 'Blockly.Toolbox', 'Blockly.Trashcan', 'Blockly.VariablesDynamic', 'Blockly.VerticalFlyout', 'Blockly.Warning', 'Blockly.ZoomControls', 'Blockly.geras.Renderer', 'Blockly.serialization.blocks', 'Blockly.thrasos.Renderer', 'Blockly.zelos.Renderer']);
goog.addDependency('../../core/scrollbar.js', ['Blockly.Scrollbar'], ['Blockly.Touch', 'Blockly.browserEvents', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Svg', 'Blockly.utils.dom'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/scrollbar_pair.js', ['Blockly.ScrollbarPair'], ['Blockly.Events', 'Blockly.Scrollbar', 'Blockly.utils.Svg', 'Blockly.utils.dom'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/serialization/blocks.js', ['Blockly.serialization.blocks'], [], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/serialization/blocks.js', ['Blockly.serialization.blocks'], ['Blockly.Xml', 'Blockly.inputTypes'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/serialization/serialization.js', ['Blockly.serialization'], [], {'module': 'goog'});
goog.addDependency('../../core/shortcut_items.js', ['Blockly.ShortcutItems'], ['Blockly.Gesture', 'Blockly.ShortcutRegistry', 'Blockly.clipboard', 'Blockly.common', 'Blockly.utils.KeyCodes'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/shortcut_registry.js', ['Blockly.ShortcutRegistry'], ['Blockly.utils.KeyCodes', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'});

View File

@@ -343,5 +343,295 @@ suite('JSO', function() {
assertProperty(jso, 'fields', {'FIELD': ['state1', 42, true]});
});
});
suite('Connected blocks', function() {
setup(function() {
this.assertInput = function(jso, name, value) {
chai.assert.deepInclude(jso['inputs'][name], value);
};
this.createBlockWithChild = function(blockType, inputName) {
const block = this.workspace.newBlock(blockType);
const childBlock = this.workspace.newBlock(blockType);
block.getInput(inputName).connection.connect(
childBlock.outputConnection || childBlock.previousConnection);
return block;
};
this.createBlockWithShadow = function(blockType, inputName) {
const block = this.workspace.newBlock(blockType);
block.getInput(inputName).connection.setShadowDom(
Blockly.Xml.textToDom(
'<block type="' + blockType + '" id="test"></block>'));
return block;
};
this.createBlockWithShadowAndChild = function(blockType, inputName) {
const block = this.workspace.newBlock(blockType);
const childBlock = this.workspace.newBlock(blockType);
block.getInput(inputName).connection.connect(
childBlock.outputConnection || childBlock.previousConnection);
block.getInput(inputName).connection.setShadowDom(
Blockly.Xml.textToDom(
'<block type="' + blockType + '" id="test"></block>'));
return block;
};
this.assertChild = function(blockType, inputName) {
const block = this.createBlockWithChild(blockType, inputName);
const jso = Blockly.serialization.blocks.save(block);
this.assertInput(
jso, inputName, {'block': { 'type': blockType, 'id': 'id2'}});
};
this.assertShadow = function(blockType, inputName) {
const block = this.createBlockWithShadow(blockType, inputName);
const jso = Blockly.serialization.blocks.save(block);
this.assertInput(
jso, inputName, {'shadow': { 'type': blockType, 'id': 'test'}});
};
this.assertOverwrittenShadow = function(blockType, inputName) {
const block =
this.createBlockWithShadowAndChild(blockType, inputName);
const jso = Blockly.serialization.blocks.save(block);
this.assertInput(
jso,
inputName,
{
'block': {
'type': blockType,
'id': 'id2'
},
'shadow': {
'type': blockType,
'id': 'test'
}
});
};
this.assertNoChild = function(blockType, inputName) {
const block = this.createBlockWithChild(blockType, inputName);
const jso =
Blockly.serialization.blocks.save(block, {addInputBlocks: false});
chai.assert.isUndefined(jso['inputs']);
};
this.assertNoShadow = function(blockType, inputName) {
const block = this.createBlockWithShadow(blockType, inputName);
const jso =
Blockly.serialization.blocks.save(block, {addInputBlocks: false});
chai.assert.isUndefined(jso['inputs']);
};
this.assertNoOverwrittenShadow = function(blockType, inputName) {
const block =
this.createBlockWithShadowAndChild(blockType, inputName);
const jso =
Blockly.serialization.blocks.save(block, {addInputBlocks: false});
chai.assert.isUndefined(jso['inputs']);
};
});
suite('Value', function() {
suite('With serialization', function() {
test('Child', function() {
this.assertChild('row_block', 'INPUT');
});
test.skip('Shadow', function() {
this.assertShadow('row_block', 'INPUT');
});
test.skip('Overwritten shadow', function() {
this.assertOverwrittenShadow('row_block', 'INPUT');
});
});
suite('Without serialization', function() {
test('Child', function() {
this.assertNoChild('row_block', 'INPUT');
});
test('Shadow', function() {
this.assertNoShadow('row_block', 'INPUT');
});
test('Overwritten shadow', function() {
this.assertNoOverwrittenShadow('row_block', 'INPUT');
});
});
});
suite('Statement', function() {
suite('With serialization', function() {
test('Child', function() {
this.assertChild('statement_block', 'NAME');
});
test.skip('Shadow', function() {
this.assertShadow('statement_block', 'NAME');
});
test.skip('Overwritten shadow', function() {
this.assertOverwrittenShadow('statement_block', 'NAME');
});
test('Child with next blocks', function() {
const block = this.workspace.newBlock('statement_block');
const childBlock = this.workspace.newBlock('stack_block');
const grandChildBlock = this.workspace.newBlock('stack_block');
block.getInput('NAME').connection
.connect(childBlock.previousConnection);
childBlock.nextConnection
.connect(grandChildBlock.previousConnection);
const jso = Blockly.serialization.blocks.save(block);
this.assertInput(
jso,
'NAME',
{
'block': {
'type': 'stack_block',
'id': 'id2',
'next': {
'block': {
'type': 'stack_block',
'id': 'id4'
}
}
}
}
);
});
});
suite('Without serialization', function() {
test('Child', function() {
this.assertNoChild('statement_block', 'NAME');
});
test('Shadow', function() {
this.assertNoShadow('statement_block', 'NAME');
});
test('Overwritten shadow', function() {
this.assertNoOverwrittenShadow('statement_block', 'NAME');
});
});
});
suite('Next', function() {
setup(function() {
this.createNextWithChild = function() {
const block = this.workspace.newBlock('stack_block');
const childBlock = this.workspace.newBlock('stack_block');
block.nextConnection.connect(childBlock.previousConnection);
return block;
};
this.createNextWithShadow = function() {
const block = this.workspace.newBlock('stack_block');
block.nextConnection.setShadowDom(
Blockly.Xml.textToDom(
'<block type="stack_block" id="test"></block>'));
return block;
};
this.createNextWithShadowAndChild = function() {
const block = this.workspace.newBlock('stack_block');
const childBlock = this.workspace.newBlock('stack_block');
block.nextConnection.connect(childBlock.previousConnection);
block.nextConnection.setShadowDom(
Blockly.Xml.textToDom(
'<block type="stack_block" id="test"></block>'));
return block;
};
});
suite('With serialization', function() {
test('Child', function() {
const block = this.createNextWithChild();
const jso =
Blockly.serialization.blocks.save(block);
chai.assert.deepInclude(
jso['next'], {'block': { 'type': 'stack_block', 'id': 'id2'}});
});
test.skip('Shadow', function() {
const block = this.createNextWithShadow();
const jso = Blockly.serialization.blocks.save(block);
chai.assert.deepInclude(
jso['next'], {'shadow': { 'type': 'stack_block', 'id': 'test'}});
});
test.skip('Overwritten shadow', function() {
const block = this.createNextWithShadowAndChild();
const jso = Blockly.serialization.blocks.save(block);
chai.assert.deepInclude(
jso['next'],
{
'block': {
'type': 'stack_block',
'id': 'id2'
},
'shadow': {
'type': 'stack_block',
'id': 'test'
}
});
});
test('Next block with inputs', function() {
const block = this.workspace.newBlock('stack_block');
const childBlock = this.workspace.newBlock('statement_block');
const grandChildBlock = this.workspace.newBlock('stack_block');
block.nextConnection.connect(childBlock.previousConnection);
childBlock.getInput('NAME').connection
.connect(grandChildBlock.previousConnection);
const jso = Blockly.serialization.blocks.save(block);
chai.assert.deepInclude(
jso['next'],
{
'block': {
'type': 'statement_block',
'id': 'id2',
'inputs': {
'NAME': {
'block': {
'type': 'stack_block',
'id': 'id4'
}
}
}
}
}
);
});
});
suite('Without serialization', function() {
test('Child', function() {
const block = this.createNextWithChild();
const jso = Blockly.serialization.blocks.save(
block, {addNextBlocks: false});
chai.assert.isUndefined(jso['next']);
});
test('Shadow', function() {
const block = this.createNextWithShadow();
const jso = Blockly.serialization.blocks.save(
block, {addNextBlocks: false});
chai.assert.isUndefined(jso['next']);
});
test('Overwritten shadow', function() {
const block = this.createNextWithShadowAndChild();
const jso = Blockly.serialization.blocks.save(
block, {addNextBlocks: false});
chai.assert.isUndefined(jso['next']);
});
});
});
});
});
});