Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 45 additions & 3 deletions core/serialization/blocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -262,19 +262,41 @@ const saveConnection = function(connection) {
* Loads the block represented by the given state into the given workspace.
* @param {!State} state The state of a block to deserialize into the workspace.
* @param {!Workspace} workspace The workspace to add the block to.
* @return {!Block} The block that was just loaded.
*/
const load = function(state, workspace) {
loadInternal(state, workspace);
// We only want to fire an event for the top block.
Blockly.Events.disable();

const block = loadInternal(state, workspace);

Blockly.Events.enable();
Blockly.Events.fire(
new (Blockly.Events.get(Blockly.Events.BLOCK_CREATE))(block));

// Adding connections to the connection db is expensive. This defers that
// operation to decrease load time.
if (block instanceof Blockly.BlockSvg) {
setTimeout(() => {
if (!block.disposed) {
block.setConnectionTracking(true);
}
}, 1);
}

return block;
};
exports.load = load;

/**
* Loads the block represented by the given state into the given workspace.
* This is defined internally so that the extra optional parameter doesn't
* clutter our external API.
* @param {!State} state The state of a block to deserialize into the workspace.
* @param {!Workspace} workspace The workspace to add the block to.
* @param {!Connection} parentConnection The optional parent connection to
* @param {!Connection=} parentConnection The optional parent connection to
* attach the block to.
* @return {!Block} The block that was just loaded.
*/
const loadInternal = function(state, workspace, parentConnection = undefined) {
const block = workspace.newBlock(state['type'], state['id']);
Expand All @@ -291,6 +313,8 @@ const loadInternal = function(state, workspace, parentConnection = undefined) {
loadFields(block, state);
loadInputBlocks(block, state);
loadNextBlocks(block, state);
initBlock(block);
return block;
};

/**
Expand Down Expand Up @@ -424,4 +448,22 @@ const loadConnection = function(connection, connectionState) {
connection);
}
};
exports.load = load;

// TODO(#5146): Remove this from the serialization system.
/**
* Initializes the give block, eg init the model, inits the svg, renders, etc.
* @param {!Block} block The block to initialize.
*/
const initBlock = function(block) {
if (block instanceof Blockly.BlockSvg) {
// Adding connections to the connection db is expensive. This defers that
// operation to decrease load time.
block.setConnectionTracking(false);

block.initSvg();
block.render(false);
block.updateDisabled();
} else {
block.initModel();
}
};
1 change: 1 addition & 0 deletions tests/mocha/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
<script src="input_test.js"></script>
<script src="insertion_marker_test.js"></script>
<script src="json_test.js"></script>
<script src="jso_deserialization_test.js"></script>
<script src="jso_serialization_test.js"></script>
<script src="shortcut_registry_test.js"></script>
<script src="keydown_test.js"></script>
Expand Down
157 changes: 157 additions & 0 deletions tests/mocha/jso_deserialization_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

suite('JSO Deserialization', function() {
setup(function() {
sharedTestSetup.call(this);
this.workspace = new Blockly.Workspace();
});

teardown(function() {
workspaceTeardown.call(this, this.workspace);
sharedTestTeardown.call(this);
});

suite('Events', function() {
test.skip('Finished loading', function() {
const state = {
'blocks': {
'blocks': [
{
'type': 'controls_if',
'id': 'testId',
'x': 42,
'y': 42
},
]
}
};
Blockly.serialization.workspaces.load(state, this.workspace);
assertEventFired(
this.eventsFireStub,
Blockly.Events.FinishedLoading,
{},
this.workspace.id);
});

suite('Var create', function() {
test('Just var', function() {
const state = {
'variables': [
{
'name': 'test',
'id': 'testId',
}
]
};
Blockly.serialization.workspaces.load(state, this.workspace);
assertEventFired(
this.eventsFireStub,
Blockly.Events.VarCreate,
{'varName': 'test', 'varId': 'testId', 'varType': ''},
this.workspace.id);
});

test('Only fire one event with var and var on block', function() {
const state = {
'variables': [
{
'name': 'test',
'id': 'testId',
}
],
'blocks': {
'blocks': [
{
'type': 'variables_get',
'id': 'blockId',
'x': 42,
'y': 42,
'fields': {
'VAR': 'testId'
}
},
]
}
};
Blockly.serialization.workspaces.load(state, this.workspace);
const calls = this.eventsFireStub.getCalls();
const count = calls.reduce((acc, call) => {
if (call.args[0] instanceof Blockly.Events.VarCreate) {
return acc + 1;
}
return acc;
}, 0);
chai.assert.equal(count, 1);
assertEventFired(
this.eventsFireStub,
Blockly.Events.VarCreate,
{'varName': 'test', 'varId': 'testId', 'varType': ''},
this.workspace.id);
});
});

suite('Block create', function() {
test('Simple', function() {
const state = {
'blocks': {
'blocks': [
{
'type': 'controls_if',
'id': 'testId',
'x': 42,
'y': 42
},
]
}
};
Blockly.serialization.workspaces.load(state, this.workspace);
assertEventFired(
this.eventsFireStub,
Blockly.Events.BlockCreate,
{},
this.workspace.id,
'testId');
});

test('Only fire event for top block', function() {
const state = {
'blocks': {
'blocks': [
{
'type': 'controls_if',
'id': 'id1',
'x': 42,
'y': 42,
'inputs': {
'DO0': {
'block': {
'type': 'controls_if',
'id': 'id2'
}
}
},
'next': {
'block': {
'type': 'controls_if',
'id': 'id3'
}
}
},
]
}
};
Blockly.serialization.workspaces.load(state, this.workspace);
assertEventFired(
this.eventsFireStub,
Blockly.Events.BlockCreate,
{},
this.workspace.id,
'id1');
});
});
});
});
2 changes: 1 addition & 1 deletion tests/mocha/jso_serialization_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

suite('JSO', function() {
suite('JSO Serialization', function() {
setup(function() {
sharedTestSetup.call(this);
this.workspace = new Blockly.Workspace();
Expand Down
59 changes: 42 additions & 17 deletions tests/playground.html
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@
}
taChange();
if (autoimport) {
fromXml();
load();
}
}

Expand Down Expand Up @@ -204,7 +204,7 @@
}
}

function toXml() {
function saveXml() {
var output = document.getElementById('importExport');
var xml = Blockly.Xml.workspaceToDom(workspace);
output.value = Blockly.Xml.domToPrettyText(xml);
Expand All @@ -213,13 +213,28 @@
taChange();
}

function fromXml() {
function saveJson() {
var output = document.getElementById('importExport');
var state = Blockly.serialization.workspaces.save(workspace);
output.value = JSON.stringify(state, null, 2);
output.focus();
output.select();
taChange();
}

function load() {
var input = document.getElementById('importExport');
if (!input.value) {
return;
}
var xml = Blockly.Xml.textToDom(input.value);
Blockly.Xml.domToWorkspace(xml, workspace);
var valid = saveIsValid(input.value);
if (valid.json) {
var state = JSON.parse(input.value);
Blockly.serialization.workspaces.load(state, workspace);
} else if (valid.xml) {
var xml = Blockly.Xml.textToDom(input.value);
Blockly.Xml.domToWorkspace(xml, workspace);
}
taChange();
}

Expand All @@ -229,20 +244,34 @@
taChange();
}

// Disable the "Import from XML" button if the XML is invalid.
// Disable the "Load" button if the save state is invalid.
// Preserve text between page reloads.
function taChange() {
var textarea = document.getElementById('importExport');
if (sessionStorage) {
sessionStorage.setItem('textarea', textarea.value);
}
var valid = true;
var valid = saveIsValid(textarea.value);
document.getElementById('import').disabled = !valid.json && !valid.xml;
}

function saveIsValid(save) {
var validJson = true;
try {
JSON.parse(save);
} catch (e) {
validJson = false;
}
var validXml = true
try {
Blockly.Xml.textToDom(textarea.value);
Blockly.Xml.textToDom(save);
} catch (e) {
valid = false;
validXml = false;
}
return {
json: validJson,
xml: validXml
}
document.getElementById('import').disabled = !valid;
}

function logEvents(state) {
Expand Down Expand Up @@ -423,18 +452,14 @@ <h1>Blockly Playground</h1>
</select>
</form>
<p>
<input type="button" value="Export to XML" onclick="toXml()">
&nbsp;
<input type="button" value="Import from XML" onclick="fromXml()" id="import">
<input type="button" value="Save JSON" onclick="saveJson()">
<input type="button" value="Save XML" onclick="saveXml()">
<input type="button" value="Load" onclick="load()" id="import">
<br>
<input type="button" value="To JavaScript" onclick="toCode('JavaScript')">
&nbsp;
<input type="button" value="To Python" onclick="toCode('Python')">
&nbsp;
<input type="button" value="To PHP" onclick="toCode('PHP')">
&nbsp;
<input type="button" value="To Lua" onclick="toCode('Lua')">
&nbsp;
<input type="button" value="To Dart" onclick="toCode('Dart')">
<br>
<textarea id="importExport" style="width: 26%; height: 12em"
Expand Down