Add serialization of basic block attributes to JSOs (#5053)

* Add basic attribute tests

* Add basic block serialization

* Add more attribute tests

* Change save to use options dictionary

* Add obeying save options

* Add test for data attribute

* Add saving data tag

* Move to ES6 syntax

* Fixup move to es6 syntax

* Declare module

* Format and lint

* Add docs

* Add returning null on insertion markers

* Fixup for move to module

* Switch to other function declarations

* Cleanup for finalized style

* Fix lint and types

* Export State def

* Switch disabled=true to enabled=false
This commit is contained in:
Beka Westberg
2021-07-23 15:03:11 -07:00
committed by alschmiedt
parent 2fa0e9801a
commit ceeda333dc
7 changed files with 383 additions and 2 deletions

View File

@@ -81,3 +81,5 @@ goog.require('Blockly.zelos.Renderer');
// Blockly Themes.
// Classic is the default theme.
goog.require('Blockly.Themes.Classic');
goog.require('Blockly.serialization.blocks');

View File

@@ -0,0 +1,114 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview Handles serializing blocks to plain JavaScript objects only
* containing state.
*/
'use strict';
goog.module('Blockly.serialization.blocks');
goog.module.declareLegacyNamespace();
// TODO: Remove this once lint is fixed.
/* eslint-disable no-use-before-define */
/**
* Represents the state of a given block.
* @typedef {{
* type: string,
* id: string,
* x: (number|undefined),
* y: (number|undefined),
* collapsed: (boolean|undefined),
* disabled: (boolean|undefined),
* editable: (boolean|undefined),
* deletable: (boolean|undefined),
* movable: (boolean|undefined),
* inline: (boolean|undefined),
* data: (string|undefined),
* }}
*/
var State;
exports.State = State;
/**
* Returns the state of the given block as a plain JavaScript object.
* @param {!Blockly.Block} block The block to serialize.
* @param {{addCoordinates: (boolean|undefined)}=} param1
* addCoordinates: If true the coordinates of the block are added to the
* serialized state. False 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} = {}) {
if (block.isInsertionMarker()) {
return null;
}
const state = {
'type': block.type,
'id': block.id
};
if (addCoordinates) {
addCoords(block, state);
}
addAttributes(block, state);
return state;
};
exports.save = save;
/**
* Adds attributes to the given state object based on the state of the block.
* Eg collapsed, disabled, editable, etc.
* @param {!Blockly.Block} block The block to base the attributes on.
* @param {!State} state The state object to append
* to.
*/
const addAttributes = function(block, state) {
if (block.isCollapsed()) {
state['collapsed'] = true;
}
if (!block.isEnabled()) {
state['enabled'] = false;
}
if (!block.isEditable()) {
state['editable'] = false;
}
if (!block.isDeletable() && !block.isShadow()) {
state['deletable'] = false;
}
if (!block.isMovable() && !block.isShadow()) {
state['movable'] = false;
}
if (block.inputsInline !== undefined &&
block.inputsInline !== block.inputsInlineDefault) {
state['inline'] = block.inputsInline;
}
// Data is a nullable string, so we don't need to worry about falsy values.
if (block.data) {
state['data'] = block.data;
}
};
/**
* Adds the coordinates of the given block to the given state object.
* @param {!Blockly.Block} block The block to base the coordinates on
* @param {!State} state The state object to append
* to
*/
const addCoords = function(block, state) {
const workspace = block.workspace;
const xy = block.getRelativeToSurfaceXY();
state['x'] = Math.round(workspace.RTL ? workspace.getWidth() - xy.x : xy.x);
state['y'] = Math.round(xy.y);
};

View File

@@ -0,0 +1,18 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview Contains top-level functions for serialization of the workspace
* to JavaScript objects.
*/
'use strict';
/**
* The top level namespace for JavaScript Object serialization.
* @namespace Blockly.serialization
*/
goog.module('Blockly.serialization');
goog.module.declareLegacyNamespace();

View File

@@ -193,9 +193,11 @@ goog.addDependency('../../core/renderers/zelos/measurables/row_elements.js', ['B
goog.addDependency('../../core/renderers/zelos/measurables/top_row.js', ['Blockly.zelos.TopRow'], ['Blockly.blockRendering.TopRow', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/renderers/zelos/path_object.js', ['Blockly.zelos.PathObject'], ['Blockly.blockRendering.PathObject', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/renderers/zelos/renderer.js', ['Blockly.zelos.Renderer'], ['Blockly.InsertionMarkerManager', 'Blockly.blockRendering', 'Blockly.blockRendering.Renderer', 'Blockly.connectionTypes', 'Blockly.utils.object', 'Blockly.zelos.ConstantProvider', 'Blockly.zelos.Drawer', 'Blockly.zelos.MarkerSvg', 'Blockly.zelos.PathObject', 'Blockly.zelos.RenderInfo'], {'lang': 'es6', 'module': 'goog'});
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.thrasos.Renderer', 'Blockly.zelos.Renderer']);
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/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'});
goog.addDependency('../../core/theme.js', ['Blockly.Theme'], ['Blockly.registry', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'});

View File

@@ -193,9 +193,11 @@ goog.addDependency('../../core/renderers/zelos/measurables/row_elements.js', ['B
goog.addDependency('../../core/renderers/zelos/measurables/top_row.js', ['Blockly.zelos.TopRow'], ['Blockly.blockRendering.TopRow', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/renderers/zelos/path_object.js', ['Blockly.zelos.PathObject'], ['Blockly.blockRendering.PathObject', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/renderers/zelos/renderer.js', ['Blockly.zelos.Renderer'], ['Blockly.InsertionMarkerManager', 'Blockly.blockRendering', 'Blockly.blockRendering.Renderer', 'Blockly.connectionTypes', 'Blockly.utils.object', 'Blockly.zelos.ConstantProvider', 'Blockly.zelos.Drawer', 'Blockly.zelos.MarkerSvg', 'Blockly.zelos.PathObject', 'Blockly.zelos.RenderInfo'], {'lang': 'es6', 'module': 'goog'});
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.thrasos.Renderer', 'Blockly.zelos.Renderer']);
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/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'});
goog.addDependency('../../core/theme.js', ['Blockly.Theme'], ['Blockly.registry', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'});
@@ -328,6 +330,7 @@ goog.addDependency('../../tests/mocha/generator_test.js', ['Blockly.test.generat
goog.addDependency('../../tests/mocha/gesture_test.js', ['Blockly.test.gesture'], ['Blockly.test.helpers'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../tests/mocha/input_test.js', ['Blockly.test.input'], ['Blockly.test.helpers'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../tests/mocha/insertion_marker_test.js', ['Blockly.test.insertionMarker'], ['Blockly.test.helpers'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../tests/mocha/jso_serialization_test.js', ['Blockly.test.jsoSerialization'], ['Blockly.test.helpers'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../tests/mocha/json_test.js', ['Blockly.test.json'], ['Blockly.test.helpers'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../tests/mocha/keydown_test.js', ['Blockly.test.keydown'], ['Blockly.test.helpers'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../tests/mocha/logic_ternary_test.js', ['Blockly.test.logicTernary'], ['Blockly.test.helpers'], {'lang': 'es6', 'module': 'goog'});

View File

@@ -82,6 +82,7 @@
goog.require('Blockly.test.gesture');
goog.require('Blockly.test.input');
goog.require('Blockly.test.insertionMarker');
goog.require('Blockly.test.jsoSerialization');
goog.require('Blockly.test.json');
goog.require('Blockly.test.keydown');
goog.require('Blockly.test.logicTernary');

View File

@@ -0,0 +1,241 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.module('Blockly.test.jsoSerialization');
const {defineStackBlock, defineRowBlock, defineStatementBlock, createGenUidStubWithReturns, sharedTestSetup, sharedTestTeardown, workspaceTeardown} = goog.require('Blockly.test.helpers');
suite('JSO', function() {
setup(function() {
sharedTestSetup.call(this);
this.workspace = new Blockly.Workspace();
defineStackBlock();
defineRowBlock();
defineStatementBlock();
createGenUidStubWithReturns(new Array(10).fill().map((_, i) => 'id' + i));
});
teardown(function() {
workspaceTeardown.call(this, this.workspace);
sharedTestTeardown.call(this);
});
suite('Blocks', function() {
test('Null on insertionMarkers', function() {
const block = this.workspace.newBlock('row_block');
block.setInsertionMarker(true);
const jso = Blockly.serialization.blocks.save(block);
chai.assert.isNull(jso);
});
suite('Save Single Block', function() {
function assertProperty(obj, property, value) {
chai.assert.equal(obj[property], value);
}
function assertNoProperty(obj, property) {
assertProperty(obj, property, undefined);
}
test('Basic', function() {
const block = this.workspace.newBlock('row_block');
const jso = Blockly.serialization.blocks.save(block);
assertProperty(jso, 'type', 'row_block');
assertProperty(jso, 'id', 'id0');
});
suite('Attributes', function() {
suite('Collapsed', function() {
test('True', function() {
const block = this.workspace.newBlock('row_block');
block.setCollapsed(true);
const jso = Blockly.serialization.blocks.save(block);
assertProperty(jso, 'collapsed', true);
});
test('False', function() {
const block = this.workspace.newBlock('row_block');
block.setCollapsed(false);
const jso = Blockly.serialization.blocks.save(block);
assertNoProperty(jso, 'collapsed');
});
});
suite('Enabled', function() {
test('False', function() {
const block = this.workspace.newBlock('row_block');
block.setEnabled(false);
const jso = Blockly.serialization.blocks.save(block);
assertProperty(jso, 'enabled', false);
});
test('True', function() {
const block = this.workspace.newBlock('row_block');
block.setEnabled(true);
const jso = Blockly.serialization.blocks.save(block);
assertNoProperty(jso, 'enabled');
});
});
suite('Deletable', function() {
test('False', function() {
const block = this.workspace.newBlock('row_block');
block.setDeletable(false);
const jso = Blockly.serialization.blocks.save(block);
assertProperty(jso, 'deletable', false);
});
test('True', function() {
const block = this.workspace.newBlock('row_block');
block.setDeletable(true);
const jso = Blockly.serialization.blocks.save(block);
assertNoProperty(jso, 'deletable');
});
test('False and Shadow', function() {
const block = this.workspace.newBlock('row_block');
block.setDeletable(false);
block.setShadow(true);
const jso = Blockly.serialization.blocks.save(block);
assertNoProperty(jso, 'deletable');
});
});
suite('Movable', function() {
test('False', function() {
const block = this.workspace.newBlock('row_block');
block.setMovable(false);
const jso = Blockly.serialization.blocks.save(block);
assertProperty(jso, 'movable', false);
});
test('True', function() {
const block = this.workspace.newBlock('row_block');
block.setMovable(true);
const jso = Blockly.serialization.blocks.save(block);
assertNoProperty(jso, 'movable');
});
test('False and Shadow', function() {
const block = this.workspace.newBlock('row_block');
block.setMovable(false);
block.setShadow(true);
const jso = Blockly.serialization.blocks.save(block);
assertNoProperty(jso, 'movable');
});
});
suite('Editable', function() {
test('False', function() {
const block = this.workspace.newBlock('row_block');
block.setEditable(false);
const jso = Blockly.serialization.blocks.save(block);
assertProperty(jso, 'editable', false);
});
test('True', function() {
const block = this.workspace.newBlock('row_block');
block.setEditable(true);
const jso = Blockly.serialization.blocks.save(block);
assertNoProperty(jso, 'editable');
});
});
suite('Inline', function() {
test('True', function() {
const block = this.workspace.newBlock('statement_block');
block.setInputsInline(true);
const jso = Blockly.serialization.blocks.save(block);
assertProperty(jso, 'inline', true);
});
test('False', function() {
const block = this.workspace.newBlock('statement_block');
block.setInputsInline(false);
const jso = Blockly.serialization.blocks.save(block);
assertProperty(jso, 'inline', false);
});
test('undefined', function() {
const block = this.workspace.newBlock('statement_block');
const jso = Blockly.serialization.blocks.save(block);
assertNoProperty(jso, 'inline');
});
test('True, matching default', function() {
const block = this.workspace.newBlock('statement_block');
block.setInputsInline(true);
block.inputsInlineDefault = true;
const jso = Blockly.serialization.blocks.save(block);
assertNoProperty(jso, 'inline');
});
test('False, matching default', function() {
const block = this.workspace.newBlock('statement_block');
block.setInputsInline(false);
block.inputsInlineDefault = false;
const jso = Blockly.serialization.blocks.save(block);
assertNoProperty(jso, 'inline');
});
});
suite('Data', function() {
test('No data', function() {
const block = this.workspace.newBlock('row_block');
const jso = Blockly.serialization.blocks.save(block);
assertNoProperty(jso, 'data');
});
test('With data', function() {
const block = this.workspace.newBlock('row_block');
block.data = 'some data';
const jso = Blockly.serialization.blocks.save(block);
assertProperty(jso, 'data', 'some data');
});
});
});
suite('Coords', function() {
test('No coordinates', function() {
const block = this.workspace.newBlock('row_block');
const jso = Blockly.serialization.blocks.save(block);
assertNoProperty(jso, 'x');
assertNoProperty(jso, 'y');
});
test('Simple', function() {
const block = this.workspace.newBlock('row_block');
block.moveBy(42, 42);
const jso =
Blockly.serialization.blocks.save(block, {addCoordinates: true});
assertProperty(jso, 'x', 42);
assertProperty(jso, 'y', 42);
});
test('Negative', function() {
const block = this.workspace.newBlock('row_block');
block.moveBy(-42, -42);
const jso =
Blockly.serialization.blocks.save(block, {addCoordinates: true});
assertProperty(jso, 'x', -42);
assertProperty(jso, 'y', -42);
});
test('Zero', function() {
const block = this.workspace.newBlock('row_block');
const jso =
Blockly.serialization.blocks.save(block, {addCoordinates: true});
assertProperty(jso, 'x', 0);
assertProperty(jso, 'y', 0);
});
});
});
});
});