mirror of
https://github.com/google/blockly.git
synced 2026-01-06 16:40:07 +01:00
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:
committed by
alschmiedt
parent
8faa360b74
commit
56d3cb6c8f
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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'});
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user