mirror of
https://github.com/google/blockly.git
synced 2026-01-04 23:50:12 +01:00
985 lines
33 KiB
JavaScript
985 lines
33 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2012 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/**
|
|
* @fileoverview XML reader and writer.
|
|
* @author fraser@google.com (Neil Fraser)
|
|
*/
|
|
'use strict';
|
|
|
|
/**
|
|
* @name Blockly.Xml
|
|
* @namespace
|
|
*/
|
|
goog.provide('Blockly.Xml');
|
|
|
|
goog.require('Blockly.constants');
|
|
goog.require('Blockly.Events');
|
|
goog.require('Blockly.utils');
|
|
goog.require('Blockly.utils.dom');
|
|
goog.require('Blockly.utils.global');
|
|
goog.require('Blockly.utils.Size');
|
|
goog.require('Blockly.utils.xml');
|
|
|
|
goog.requireType('Blockly.Block');
|
|
goog.requireType('Blockly.Comment');
|
|
goog.requireType('Blockly.Connection');
|
|
goog.requireType('Blockly.Field');
|
|
goog.requireType('Blockly.VariableModel');
|
|
goog.requireType('Blockly.Workspace');
|
|
|
|
|
|
/**
|
|
* Encode a block tree as XML.
|
|
* @param {!Blockly.Workspace} workspace The workspace containing blocks.
|
|
* @param {boolean=} opt_noId True if the encoder should skip the block IDs.
|
|
* @return {!Element} XML DOM element.
|
|
*/
|
|
Blockly.Xml.workspaceToDom = function(workspace, opt_noId) {
|
|
var xml = Blockly.utils.xml.createElement('xml');
|
|
var variablesElement = Blockly.Xml.variablesToDom(
|
|
Blockly.Variables.allUsedVarModels(workspace));
|
|
if (variablesElement.hasChildNodes()) {
|
|
xml.appendChild(variablesElement);
|
|
}
|
|
var comments = workspace.getTopComments(true);
|
|
for (var i = 0, comment; (comment = comments[i]); i++) {
|
|
xml.appendChild(comment.toXmlWithXY(opt_noId));
|
|
}
|
|
var blocks = workspace.getTopBlocks(true);
|
|
for (var i = 0, block; (block = blocks[i]); i++) {
|
|
xml.appendChild(Blockly.Xml.blockToDomWithXY(block, opt_noId));
|
|
}
|
|
return xml;
|
|
};
|
|
|
|
/**
|
|
* Encode a list of variables as XML.
|
|
* @param {!Array.<!Blockly.VariableModel>} variableList List of all variable
|
|
* models.
|
|
* @return {!Element} Tree of XML elements.
|
|
*/
|
|
Blockly.Xml.variablesToDom = function(variableList) {
|
|
var variables = Blockly.utils.xml.createElement('variables');
|
|
for (var i = 0, variable; (variable = variableList[i]); i++) {
|
|
var element = Blockly.utils.xml.createElement('variable');
|
|
element.appendChild(Blockly.utils.xml.createTextNode(variable.name));
|
|
if (variable.type) {
|
|
element.setAttribute('type', variable.type);
|
|
}
|
|
element.id = variable.getId();
|
|
variables.appendChild(element);
|
|
}
|
|
return variables;
|
|
};
|
|
|
|
/**
|
|
* Encode a block subtree as XML with XY coordinates.
|
|
* @param {!Blockly.Block} block The root block to encode.
|
|
* @param {boolean=} opt_noId True if the encoder should skip the block ID.
|
|
* @return {!Element|!DocumentFragment} Tree of XML elements or an empty document
|
|
* fragment if the block was an insertion marker.
|
|
*/
|
|
Blockly.Xml.blockToDomWithXY = function(block, opt_noId) {
|
|
if (block.isInsertionMarker()) { // Skip over insertion markers.
|
|
block = block.getChildren(false)[0];
|
|
if (!block) {
|
|
// Disappears when appended.
|
|
return new DocumentFragment();
|
|
}
|
|
}
|
|
|
|
var width; // Not used in LTR.
|
|
if (block.workspace.RTL) {
|
|
width = block.workspace.getWidth();
|
|
}
|
|
|
|
var element = Blockly.Xml.blockToDom(block, opt_noId);
|
|
var xy = block.getRelativeToSurfaceXY();
|
|
element.setAttribute('x',
|
|
Math.round(block.workspace.RTL ? width - xy.x : xy.x));
|
|
element.setAttribute('y', Math.round(xy.y));
|
|
return element;
|
|
};
|
|
|
|
/**
|
|
* Encode a field as XML.
|
|
* @param {!Blockly.Field} field The field to encode.
|
|
* @return {Element} XML element, or null if the field did not need to be
|
|
* serialized.
|
|
* @private
|
|
*/
|
|
Blockly.Xml.fieldToDom_ = function(field) {
|
|
if (field.isSerializable()) {
|
|
var container = Blockly.utils.xml.createElement('field');
|
|
container.setAttribute('name', field.name || '');
|
|
return field.toXml(container);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Encode all of a block's fields as XML and attach them to the given tree of
|
|
* XML elements.
|
|
* @param {!Blockly.Block} block A block with fields to be encoded.
|
|
* @param {!Element} element The XML element to which the field DOM should be
|
|
* attached.
|
|
* @private
|
|
*/
|
|
Blockly.Xml.allFieldsToDom_ = function(block, element) {
|
|
for (var i = 0, input; (input = block.inputList[i]); i++) {
|
|
for (var j = 0, field; (field = input.fieldRow[j]); j++) {
|
|
var fieldDom = Blockly.Xml.fieldToDom_(field);
|
|
if (fieldDom) {
|
|
element.appendChild(fieldDom);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Encode a block subtree as XML.
|
|
* @param {!Blockly.Block} block The root block to encode.
|
|
* @param {boolean=} opt_noId True if the encoder should skip the block ID.
|
|
* @return {!Element|!DocumentFragment} Tree of XML elements or an empty document
|
|
* fragment if the block was an insertion marker.
|
|
*/
|
|
Blockly.Xml.blockToDom = function(block, opt_noId) {
|
|
// Skip over insertion markers.
|
|
if (block.isInsertionMarker()) {
|
|
var child = block.getChildren(false)[0];
|
|
if (child) {
|
|
return Blockly.Xml.blockToDom(child);
|
|
} else {
|
|
// Disappears when appended.
|
|
return new DocumentFragment();
|
|
}
|
|
}
|
|
|
|
|
|
var element =
|
|
Blockly.utils.xml.createElement(block.isShadow() ? 'shadow' : 'block');
|
|
element.setAttribute('type', block.type);
|
|
if (!opt_noId) {
|
|
// It's important to use setAttribute here otherwise IE11 won't serialize
|
|
// the block's id when domToText is called.
|
|
element.setAttribute('id', block.id);
|
|
}
|
|
if (block.mutationToDom) {
|
|
// Custom data for an advanced block.
|
|
var mutation = block.mutationToDom();
|
|
if (mutation && (mutation.hasChildNodes() || mutation.hasAttributes())) {
|
|
element.appendChild(mutation);
|
|
}
|
|
}
|
|
|
|
Blockly.Xml.allFieldsToDom_(block, element);
|
|
|
|
var commentText = block.getCommentText();
|
|
if (commentText) {
|
|
var size = block.commentModel.size;
|
|
var pinned = block.commentModel.pinned;
|
|
|
|
var commentElement = Blockly.utils.xml.createElement('comment');
|
|
commentElement.appendChild(Blockly.utils.xml.createTextNode(commentText));
|
|
commentElement.setAttribute('pinned', pinned);
|
|
commentElement.setAttribute('h', size.height);
|
|
commentElement.setAttribute('w', size.width);
|
|
|
|
element.appendChild(commentElement);
|
|
}
|
|
|
|
if (block.data) {
|
|
var dataElement = Blockly.utils.xml.createElement('data');
|
|
dataElement.appendChild(Blockly.utils.xml.createTextNode(block.data));
|
|
element.appendChild(dataElement);
|
|
}
|
|
|
|
for (var i = 0, input; (input = block.inputList[i]); i++) {
|
|
var container;
|
|
var empty = true;
|
|
if (input.type == Blockly.DUMMY_INPUT) {
|
|
continue;
|
|
} else {
|
|
var childBlock = input.connection.targetBlock();
|
|
if (input.type == Blockly.INPUT_VALUE) {
|
|
container = Blockly.utils.xml.createElement('value');
|
|
} else if (input.type == Blockly.NEXT_STATEMENT) {
|
|
container = Blockly.utils.xml.createElement('statement');
|
|
}
|
|
var shadow = input.connection.getShadowDom();
|
|
if (shadow && (!childBlock || !childBlock.isShadow())) {
|
|
container.appendChild(Blockly.Xml.cloneShadow_(shadow, opt_noId));
|
|
}
|
|
if (childBlock) {
|
|
var elem = Blockly.Xml.blockToDom(childBlock, opt_noId);
|
|
if (elem.nodeType == Blockly.utils.dom.NodeType.ELEMENT_NODE) {
|
|
container.appendChild(elem);
|
|
empty = false;
|
|
}
|
|
}
|
|
}
|
|
container.setAttribute('name', input.name);
|
|
if (!empty) {
|
|
element.appendChild(container);
|
|
}
|
|
}
|
|
if (block.inputsInline != undefined &&
|
|
block.inputsInline != block.inputsInlineDefault) {
|
|
element.setAttribute('inline', block.inputsInline);
|
|
}
|
|
if (block.isCollapsed()) {
|
|
element.setAttribute('collapsed', true);
|
|
}
|
|
if (!block.isEnabled()) {
|
|
element.setAttribute('disabled', true);
|
|
}
|
|
if (!block.isDeletable() && !block.isShadow()) {
|
|
element.setAttribute('deletable', false);
|
|
}
|
|
if (!block.isMovable() && !block.isShadow()) {
|
|
element.setAttribute('movable', false);
|
|
}
|
|
if (!block.isEditable()) {
|
|
element.setAttribute('editable', false);
|
|
}
|
|
|
|
var nextBlock = block.getNextBlock();
|
|
if (nextBlock) {
|
|
var elem = Blockly.Xml.blockToDom(nextBlock, opt_noId);
|
|
if (elem.nodeType == Blockly.utils.dom.NodeType.ELEMENT_NODE) {
|
|
var container = Blockly.utils.xml.createElement('next');
|
|
container.appendChild(elem);
|
|
element.appendChild(container);
|
|
}
|
|
}
|
|
var shadow = block.nextConnection && block.nextConnection.getShadowDom();
|
|
if (shadow && (!nextBlock || !nextBlock.isShadow())) {
|
|
container.appendChild(Blockly.Xml.cloneShadow_(shadow, opt_noId));
|
|
}
|
|
|
|
return element;
|
|
};
|
|
|
|
/**
|
|
* Deeply clone the shadow's DOM so that changes don't back-wash to the block.
|
|
* @param {!Element} shadow A tree of XML elements.
|
|
* @param {boolean=} opt_noId True if the encoder should skip the block ID.
|
|
* @return {!Element} A tree of XML elements.
|
|
* @private
|
|
*/
|
|
Blockly.Xml.cloneShadow_ = function(shadow, opt_noId) {
|
|
shadow = shadow.cloneNode(true);
|
|
// Walk the tree looking for whitespace. Don't prune whitespace in a tag.
|
|
var node = shadow;
|
|
var textNode;
|
|
while (node) {
|
|
if (opt_noId && node.nodeName == 'shadow') {
|
|
// Strip off IDs from shadow blocks. There should never be a 'block' as
|
|
// a child of a 'shadow', so no need to check that.
|
|
node.removeAttribute('id');
|
|
}
|
|
if (node.firstChild) {
|
|
node = node.firstChild;
|
|
} else {
|
|
while (node && !node.nextSibling) {
|
|
textNode = node;
|
|
node = node.parentNode;
|
|
if (textNode.nodeType == Blockly.utils.dom.NodeType.TEXT_NODE &&
|
|
textNode.data.trim() == '' && node.firstChild != textNode) {
|
|
// Prune whitespace after a tag.
|
|
Blockly.utils.dom.removeNode(textNode);
|
|
}
|
|
}
|
|
if (node) {
|
|
textNode = node;
|
|
node = node.nextSibling;
|
|
if (textNode.nodeType == Blockly.utils.dom.NodeType.TEXT_NODE &&
|
|
textNode.data.trim() == '') {
|
|
// Prune whitespace before a tag.
|
|
Blockly.utils.dom.removeNode(textNode);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return shadow;
|
|
};
|
|
|
|
/**
|
|
* Converts a DOM structure into plain text.
|
|
* Currently the text format is fairly ugly: all one line with no whitespace,
|
|
* unless the DOM itself has whitespace built-in.
|
|
* @param {!Node} dom A tree of XML nodes.
|
|
* @return {string} Text representation.
|
|
*/
|
|
Blockly.Xml.domToText = function(dom) {
|
|
var text = Blockly.utils.xml.domToText(dom);
|
|
// Unpack self-closing tags. These tags fail when embedded in HTML.
|
|
// <block name="foo"/> -> <block name="foo"></block>
|
|
return text.replace(/<(\w+)([^<]*)\/>/g, '<$1$2></$1>');
|
|
};
|
|
|
|
/**
|
|
* Converts a DOM structure into properly indented text.
|
|
* @param {!Node} dom A tree of XML elements.
|
|
* @return {string} Text representation.
|
|
*/
|
|
Blockly.Xml.domToPrettyText = function(dom) {
|
|
// This function is not guaranteed to be correct for all XML.
|
|
// But it handles the XML that Blockly generates.
|
|
var blob = Blockly.Xml.domToText(dom);
|
|
// Place every open and close tag on its own line.
|
|
var lines = blob.split('<');
|
|
// Indent every line.
|
|
var indent = '';
|
|
for (var i = 1; i < lines.length; i++) {
|
|
var line = lines[i];
|
|
if (line[0] == '/') {
|
|
indent = indent.substring(2);
|
|
}
|
|
lines[i] = indent + '<' + line;
|
|
if (line[0] != '/' && line.slice(-2) != '/>') {
|
|
indent += ' ';
|
|
}
|
|
}
|
|
// Pull simple tags back together.
|
|
// E.g. <foo></foo>
|
|
var text = lines.join('\n');
|
|
text = text.replace(/(<(\w+)\b[^>]*>[^\n]*)\n *<\/\2>/g, '$1</$2>');
|
|
// Trim leading blank line.
|
|
return text.replace(/^\n/, '');
|
|
};
|
|
|
|
/**
|
|
* Converts an XML string into a DOM structure.
|
|
* @param {string} text An XML string.
|
|
* @return {!Element} A DOM object representing the singular child of the
|
|
* document element.
|
|
* @throws if the text doesn't parse.
|
|
*/
|
|
Blockly.Xml.textToDom = function(text) {
|
|
var doc = Blockly.utils.xml.textToDomDocument(text);
|
|
if (!doc || !doc.documentElement ||
|
|
doc.getElementsByTagName('parsererror').length) {
|
|
throw Error('textToDom was unable to parse: ' + text);
|
|
}
|
|
return doc.documentElement;
|
|
};
|
|
|
|
/**
|
|
* Clear the given workspace then decode an XML DOM and
|
|
* create blocks on the workspace.
|
|
* @param {!Element} xml XML DOM.
|
|
* @param {!Blockly.Workspace} workspace The workspace.
|
|
* @return {Array.<string>} An array containing new block ids.
|
|
*/
|
|
Blockly.Xml.clearWorkspaceAndLoadFromXml = function(xml, workspace) {
|
|
workspace.setResizesEnabled(false);
|
|
workspace.clear();
|
|
var blockIds = Blockly.Xml.domToWorkspace(xml, workspace);
|
|
workspace.setResizesEnabled(true);
|
|
return blockIds;
|
|
};
|
|
|
|
/**
|
|
* Decode an XML DOM and create blocks on the workspace.
|
|
* @param {!Element} xml XML DOM.
|
|
* @param {!Blockly.Workspace} workspace The workspace.
|
|
* @return {!Array.<string>} An array containing new block IDs.
|
|
* @suppress {strictModuleDepCheck} Suppress module check while workspace
|
|
* comments are not bundled in.
|
|
*/
|
|
Blockly.Xml.domToWorkspace = function(xml, workspace) {
|
|
if (xml instanceof Blockly.Workspace) {
|
|
var swap = xml;
|
|
// Closure Compiler complains here because the arguments are reversed.
|
|
/** @suppress {checkTypes} */
|
|
xml = workspace;
|
|
workspace = swap;
|
|
console.warn('Deprecated call to Blockly.Xml.domToWorkspace, ' +
|
|
'swap the arguments.');
|
|
}
|
|
|
|
var width; // Not used in LTR.
|
|
if (workspace.RTL) {
|
|
width = workspace.getWidth();
|
|
}
|
|
var newBlockIds = []; // A list of block IDs added by this call.
|
|
Blockly.utils.dom.startTextWidthCache();
|
|
var existingGroup = Blockly.Events.getGroup();
|
|
if (!existingGroup) {
|
|
Blockly.Events.setGroup(true);
|
|
}
|
|
|
|
// Disable workspace resizes as an optimization.
|
|
if (workspace.setResizesEnabled) {
|
|
workspace.setResizesEnabled(false);
|
|
}
|
|
var variablesFirst = true;
|
|
try {
|
|
for (var i = 0, xmlChild; (xmlChild = xml.childNodes[i]); i++) {
|
|
var name = xmlChild.nodeName.toLowerCase();
|
|
var xmlChildElement = /** @type {!Element} */ (xmlChild);
|
|
if (name == 'block' ||
|
|
(name == 'shadow' && !Blockly.Events.recordUndo)) {
|
|
// Allow top-level shadow blocks if recordUndo is disabled since
|
|
// that means an undo is in progress. Such a block is expected
|
|
// to be moved to a nested destination in the next operation.
|
|
var block = Blockly.Xml.domToBlock(xmlChildElement, workspace);
|
|
newBlockIds.push(block.id);
|
|
var blockX = xmlChildElement.hasAttribute('x') ?
|
|
parseInt(xmlChildElement.getAttribute('x'), 10) : 10;
|
|
var blockY = xmlChildElement.hasAttribute('y') ?
|
|
parseInt(xmlChildElement.getAttribute('y'), 10) : 10;
|
|
if (!isNaN(blockX) && !isNaN(blockY)) {
|
|
block.moveBy(workspace.RTL ? width - blockX : blockX, blockY);
|
|
}
|
|
variablesFirst = false;
|
|
} else if (name == 'shadow') {
|
|
throw TypeError('Shadow block cannot be a top-level block.');
|
|
} else if (name == 'comment') {
|
|
if (workspace.rendered) {
|
|
if (!Blockly.WorkspaceCommentSvg) {
|
|
console.warn('Missing require for Blockly.WorkspaceCommentSvg, ' +
|
|
'ignoring workspace comment.');
|
|
} else {
|
|
Blockly.WorkspaceCommentSvg.fromXml(
|
|
xmlChildElement, workspace, width);
|
|
}
|
|
} else {
|
|
if (!Blockly.WorkspaceComment) {
|
|
console.warn('Missing require for Blockly.WorkspaceComment, ' +
|
|
'ignoring workspace comment.');
|
|
} else {
|
|
Blockly.WorkspaceComment.fromXml(xmlChildElement, workspace);
|
|
}
|
|
}
|
|
} else if (name == 'variables') {
|
|
if (variablesFirst) {
|
|
Blockly.Xml.domToVariables(xmlChildElement, workspace);
|
|
} else {
|
|
throw Error('\'variables\' tag must exist once before block and ' +
|
|
'shadow tag elements in the workspace XML, but it was found in ' +
|
|
'another location.');
|
|
}
|
|
variablesFirst = false;
|
|
}
|
|
}
|
|
} finally {
|
|
if (!existingGroup) {
|
|
Blockly.Events.setGroup(false);
|
|
}
|
|
Blockly.utils.dom.stopTextWidthCache();
|
|
}
|
|
// Re-enable workspace resizing.
|
|
if (workspace.setResizesEnabled) {
|
|
workspace.setResizesEnabled(true);
|
|
}
|
|
Blockly.Events.fire(new (Blockly.Events.get(Blockly.Events.FINISHED_LOADING))(
|
|
workspace));
|
|
return newBlockIds;
|
|
};
|
|
|
|
/**
|
|
* Decode an XML DOM and create blocks on the workspace. Position the new
|
|
* blocks immediately below prior blocks, aligned by their starting edge.
|
|
* @param {!Element} xml The XML DOM.
|
|
* @param {!Blockly.Workspace} workspace The workspace to add to.
|
|
* @return {Array.<string>} An array containing new block IDs.
|
|
*/
|
|
Blockly.Xml.appendDomToWorkspace = function(xml, workspace) {
|
|
var bbox; // Bounding box of the current blocks.
|
|
// First check if we have a workspaceSvg, otherwise the blocks have no shape
|
|
// and the position does not matter.
|
|
if (Object.prototype.hasOwnProperty.call(workspace, 'scale')) {
|
|
bbox = workspace.getBlocksBoundingBox();
|
|
}
|
|
// Load the new blocks into the workspace and get the IDs of the new blocks.
|
|
var newBlockIds = Blockly.Xml.domToWorkspace(xml, workspace);
|
|
if (bbox && bbox.top != bbox.bottom) { // check if any previous block
|
|
var offsetY = 0; // offset to add to y of the new block
|
|
var offsetX = 0;
|
|
var farY = bbox.bottom; // bottom position
|
|
var topX = workspace.RTL ? bbox.right : bbox.left; // x of bounding box
|
|
// Check position of the new blocks.
|
|
var newLeftX = Infinity; // x of top left corner
|
|
var newRightX = -Infinity; // x of top right corner
|
|
var newY = Infinity; // y of top corner
|
|
var ySeparation = 10;
|
|
for (var i = 0; i < newBlockIds.length; i++) {
|
|
var blockXY =
|
|
workspace.getBlockById(newBlockIds[i]).getRelativeToSurfaceXY();
|
|
if (blockXY.y < newY) {
|
|
newY = blockXY.y;
|
|
}
|
|
if (blockXY.x < newLeftX) { // if we left align also on x
|
|
newLeftX = blockXY.x;
|
|
}
|
|
if (blockXY.x > newRightX) { // if we right align also on x
|
|
newRightX = blockXY.x;
|
|
}
|
|
}
|
|
offsetY = farY - newY + ySeparation;
|
|
offsetX = workspace.RTL ? topX - newRightX : topX - newLeftX;
|
|
for (var i = 0; i < newBlockIds.length; i++) {
|
|
var block = workspace.getBlockById(newBlockIds[i]);
|
|
block.moveBy(offsetX, offsetY);
|
|
}
|
|
}
|
|
return newBlockIds;
|
|
};
|
|
|
|
/**
|
|
* Decode an XML block tag and create a block (and possibly sub blocks) on the
|
|
* workspace.
|
|
* @param {!Element} xmlBlock XML block element.
|
|
* @param {!Blockly.Workspace} workspace The workspace.
|
|
* @return {!Blockly.Block} The root block created.
|
|
*/
|
|
Blockly.Xml.domToBlock = function(xmlBlock, workspace) {
|
|
if (xmlBlock instanceof Blockly.Workspace) {
|
|
var swap = xmlBlock;
|
|
// Closure Compiler complains here because the arguments are reversed.
|
|
/** @suppress {checkTypes} */
|
|
xmlBlock = /** @type {!Element} */ (workspace);
|
|
workspace = swap;
|
|
console.warn('Deprecated call to Blockly.Xml.domToBlock, ' +
|
|
'swap the arguments.');
|
|
}
|
|
// Create top-level block.
|
|
Blockly.Events.disable();
|
|
var variablesBeforeCreation = workspace.getAllVariables();
|
|
try {
|
|
var topBlock = Blockly.Xml.domToBlockHeadless_(xmlBlock, workspace);
|
|
// Generate list of all blocks.
|
|
var blocks = topBlock.getDescendants(false);
|
|
if (workspace.rendered) {
|
|
// Wait to track connections to speed up assembly.
|
|
topBlock.setConnectionTracking(false);
|
|
// Render each block.
|
|
for (var i = blocks.length - 1; i >= 0; i--) {
|
|
blocks[i].initSvg();
|
|
}
|
|
for (var i = blocks.length - 1; i >= 0; i--) {
|
|
blocks[i].render(false);
|
|
}
|
|
// Populating the connection database may be deferred until after the
|
|
// blocks have rendered.
|
|
setTimeout(function() {
|
|
if (!topBlock.disposed) {
|
|
topBlock.setConnectionTracking(true);
|
|
}
|
|
}, 1);
|
|
topBlock.updateDisabled();
|
|
// Allow the scrollbars to resize and move based on the new contents.
|
|
// TODO(@picklesrus): #387. Remove when domToBlock avoids resizing.
|
|
workspace.resizeContents();
|
|
} else {
|
|
for (var i = blocks.length - 1; i >= 0; i--) {
|
|
blocks[i].initModel();
|
|
}
|
|
}
|
|
} finally {
|
|
Blockly.Events.enable();
|
|
}
|
|
if (Blockly.Events.isEnabled()) {
|
|
var newVariables = Blockly.Variables.getAddedVariables(workspace,
|
|
variablesBeforeCreation);
|
|
// Fire a VarCreate event for each (if any) new variable created.
|
|
for (var i = 0; i < newVariables.length; i++) {
|
|
var thisVariable = newVariables[i];
|
|
Blockly.Events.fire(new (Blockly.Events.get(Blockly.Events.VAR_CREATE))(
|
|
thisVariable));
|
|
}
|
|
// Block events come after var events, in case they refer to newly created
|
|
// variables.
|
|
Blockly.Events.fire(new (Blockly.Events.get(Blockly.Events.CREATE))(
|
|
topBlock));
|
|
}
|
|
return topBlock;
|
|
};
|
|
|
|
|
|
/**
|
|
* Decode an XML list of variables and add the variables to the workspace.
|
|
* @param {!Element} xmlVariables List of XML variable elements.
|
|
* @param {!Blockly.Workspace} workspace The workspace to which the variable
|
|
* should be added.
|
|
*/
|
|
Blockly.Xml.domToVariables = function(xmlVariables, workspace) {
|
|
for (var i = 0, xmlChild; (xmlChild = xmlVariables.childNodes[i]); i++) {
|
|
if (xmlChild.nodeType != Blockly.utils.dom.NodeType.ELEMENT_NODE) {
|
|
continue; // Skip text nodes.
|
|
}
|
|
var type = xmlChild.getAttribute('type');
|
|
var id = xmlChild.getAttribute('id');
|
|
var name = xmlChild.textContent;
|
|
|
|
workspace.createVariable(name, type, id);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* A mapping of nodeName to node for child nodes of xmlBlock.
|
|
* @typedef {{
|
|
* mutation: !Array<!Element>,
|
|
* comment: !Array<!Element>,
|
|
* data: !Array<!Element>,
|
|
* field: !Array<!Element>,
|
|
* input: !Array<!Element>,
|
|
* next: !Array<!Element>
|
|
* }}
|
|
*/
|
|
Blockly.Xml.childNodeTagMap;
|
|
|
|
/**
|
|
* Creates a mapping of childNodes for each supported xml tag for the provided
|
|
* xmlBlock. Logs a warning for any encountered unsupported tags.
|
|
* @param {!Element} xmlBlock XML block element.
|
|
* @return {Blockly.Xml.childNodeTagMap} The childNode map from nodeName to
|
|
* node.
|
|
*/
|
|
Blockly.Xml.mapSupportedXmlTags_ = function(xmlBlock) {
|
|
var childNodeMap = {
|
|
mutation: [], comment: [], data: [], field: [], input: [],
|
|
next: []
|
|
};
|
|
for (var i = 0, xmlChild; (xmlChild = xmlBlock.childNodes[i]); i++) {
|
|
if (xmlChild.nodeType == Blockly.utils.dom.NodeType.TEXT_NODE) {
|
|
// Ignore any text at the <block> level. It's all whitespace anyway.
|
|
continue;
|
|
}
|
|
switch (xmlChild.nodeName.toLowerCase()) {
|
|
case 'mutation':
|
|
childNodeMap.mutation.push(xmlChild);
|
|
break;
|
|
case 'comment':
|
|
if (!Blockly.Comment) {
|
|
console.warn('Missing require for Blockly.Comment, ' +
|
|
'ignoring block comment.');
|
|
break;
|
|
}
|
|
childNodeMap.comment.push(xmlChild);
|
|
break;
|
|
case 'data':
|
|
childNodeMap.data.push(xmlChild);
|
|
break;
|
|
case 'title':
|
|
// Titles were renamed to field in December 2013.
|
|
// Fall through.
|
|
case 'field':
|
|
childNodeMap.field.push(xmlChild);
|
|
break;
|
|
case 'value':
|
|
case 'statement':
|
|
childNodeMap.input.push(xmlChild);
|
|
break;
|
|
case 'next':
|
|
childNodeMap.next.push(xmlChild);
|
|
break;
|
|
default:
|
|
// Unknown tag; ignore. Same principle as HTML parsers.
|
|
console.warn('Ignoring unknown tag: ' + xmlChild.nodeName);
|
|
}
|
|
}
|
|
return childNodeMap;
|
|
};
|
|
|
|
/**
|
|
* Applies mutation tag child nodes to the given block.
|
|
* @param {Array<!Element>} xmlChildren Child nodes.
|
|
* @param {!Blockly.Block} block The block to apply the child nodes on.
|
|
* @return {boolean} True if mutation may have added some elements that need
|
|
* initialization (requiring initSvg call).
|
|
* @private
|
|
*/
|
|
Blockly.Xml.applyMutationTagNodes_ = function(xmlChildren, block) {
|
|
var shouldCallInitSvg = false;
|
|
for (var i = 0, xmlChild; (xmlChild = xmlChildren[i]); i++) {
|
|
// Custom data for an advanced block.
|
|
if (block.domToMutation) {
|
|
block.domToMutation(xmlChild);
|
|
if (block.initSvg) {
|
|
// Mutation may have added some elements that need initializing.
|
|
shouldCallInitSvg = true;
|
|
}
|
|
}
|
|
}
|
|
return shouldCallInitSvg;
|
|
};
|
|
|
|
/**
|
|
* Applies comment tag child nodes to the given block.
|
|
* @param {Array<!Element>} xmlChildren Child nodes.
|
|
* @param {!Blockly.Block} block The block to apply the child nodes on.
|
|
* @private
|
|
*/
|
|
Blockly.Xml.applyCommentTagNodes_ = function(xmlChildren, block) {
|
|
for (var i = 0, xmlChild; (xmlChild = xmlChildren[i]); i++) {
|
|
var text = xmlChild.textContent;
|
|
var pinned = xmlChild.getAttribute('pinned') == 'true';
|
|
var width = parseInt(xmlChild.getAttribute('w'), 10);
|
|
var height = parseInt(xmlChild.getAttribute('h'), 10);
|
|
|
|
block.setCommentText(text);
|
|
block.commentModel.pinned = pinned;
|
|
if (!isNaN(width) && !isNaN(height)) {
|
|
block.commentModel.size = new Blockly.utils.Size(width, height);
|
|
}
|
|
|
|
if (pinned && block.getCommentIcon && !block.isInFlyout) {
|
|
setTimeout(function() {
|
|
block.getCommentIcon().setVisible(true);
|
|
}, 1);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Applies data tag child nodes to the given block.
|
|
* @param {Array<!Element>} xmlChildren Child nodes.
|
|
* @param {!Blockly.Block} block The block to apply the child nodes on.
|
|
* @private
|
|
*/
|
|
Blockly.Xml.applyDataTagNodes_ = function(xmlChildren, block) {
|
|
for (var i = 0, xmlChild; (xmlChild = xmlChildren[i]); i++) {
|
|
block.data = xmlChild.textContent;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Applies field tag child nodes to the given block.
|
|
* @param {Array<!Element>} xmlChildren Child nodes.
|
|
* @param {!Blockly.Block} block The block to apply the child nodes on.
|
|
* @private
|
|
*/
|
|
Blockly.Xml.applyFieldTagNodes_ = function(xmlChildren, block) {
|
|
for (var i = 0, xmlChild; (xmlChild = xmlChildren[i]); i++) {
|
|
var nodeName = xmlChild.getAttribute('name');
|
|
Blockly.Xml.domToField_(block, nodeName, xmlChild);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Finds any enclosed blocks or shadows within this xml node.
|
|
* @param {!Element} xmlNode The xml node to extract child block info from.
|
|
* @return {{childBlockElement: ?Element, childShadowElement: ?Element}} Any
|
|
* found child block.
|
|
* @private
|
|
*/
|
|
Blockly.Xml.findChildBlocks_ = function(xmlNode) {
|
|
var childBlockInfo = {childBlockElement: null, childShadowElement: null};
|
|
for (var i = 0, xmlChild; (xmlChild = xmlNode.childNodes[i]); i++) {
|
|
if (xmlChild.nodeType == Blockly.utils.dom.NodeType.ELEMENT_NODE) {
|
|
if (xmlChild.nodeName.toLowerCase() == 'block') {
|
|
childBlockInfo.childBlockElement = /** @type {!Element} */ (xmlChild);
|
|
} else if (xmlChild.nodeName.toLowerCase() == 'shadow') {
|
|
childBlockInfo.childShadowElement = /** @type {!Element} */ (xmlChild);
|
|
}
|
|
}
|
|
}
|
|
return childBlockInfo;
|
|
};
|
|
|
|
/**
|
|
* Applies input child nodes (value or statement) to the given block.
|
|
* @param {Array<!Element>} xmlChildren Child nodes.
|
|
* @param {!Blockly.Workspace} workspace The workspace containing the given
|
|
* block.
|
|
* @param {!Blockly.Block} block The block to apply the child nodes on.
|
|
* @param {string} prototypeName The prototype name of the block.
|
|
* @private
|
|
*/
|
|
Blockly.Xml.applyInputTagNodes_ = function(xmlChildren, workspace, block,
|
|
prototypeName) {
|
|
for (var i = 0, xmlChild; (xmlChild = xmlChildren[i]); i++) {
|
|
var nodeName = xmlChild.getAttribute('name');
|
|
var input = block.getInput(nodeName);
|
|
if (!input) {
|
|
console.warn('Ignoring non-existent input ' + nodeName + ' in block ' +
|
|
prototypeName);
|
|
break;
|
|
}
|
|
var childBlockInfo = Blockly.Xml.findChildBlocks_(xmlChild);
|
|
if (childBlockInfo.childBlockElement) {
|
|
if (!input.connection) {
|
|
throw TypeError('Input connection does not exist.');
|
|
}
|
|
Blockly.Xml.domToBlockHeadless_(childBlockInfo.childBlockElement,
|
|
workspace, input.connection, false);
|
|
}
|
|
// Set shadow after so we don't create a shadow we delete immediately.
|
|
if (childBlockInfo.childShadowElement) {
|
|
input.connection.setShadowDom(childBlockInfo.childShadowElement);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Applies next child nodes to the given block.
|
|
* @param {Array<!Element>} xmlChildren Child nodes.
|
|
* @param {!Blockly.Workspace} workspace The workspace containing the given
|
|
* block.
|
|
* @param {!Blockly.Block} block The block to apply the child nodes on.
|
|
* @private
|
|
*/
|
|
Blockly.Xml.applyNextTagNodes_ = function(xmlChildren, workspace, block) {
|
|
for (var i = 0, xmlChild; (xmlChild = xmlChildren[i]); i++) {
|
|
var childBlockInfo = Blockly.Xml.findChildBlocks_(xmlChild);
|
|
if (childBlockInfo.childBlockElement) {
|
|
if (!block.nextConnection) {
|
|
throw TypeError('Next statement does not exist.');
|
|
}
|
|
// If there is more than one XML 'next' tag.
|
|
if (block.nextConnection.isConnected()) {
|
|
throw TypeError('Next statement is already connected.');
|
|
}
|
|
// Create child block.
|
|
Blockly.Xml.domToBlockHeadless_(childBlockInfo.childBlockElement,
|
|
workspace, block.nextConnection,
|
|
true);
|
|
}
|
|
// Set shadow after so we don't create a shadow we delete immediately.
|
|
if (childBlockInfo.childShadowElement && block.nextConnection) {
|
|
block.nextConnection.setShadowDom(childBlockInfo.childShadowElement);
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Decode an XML block tag and create a block (and possibly sub blocks) on the
|
|
* workspace.
|
|
* @param {!Element} xmlBlock XML block element.
|
|
* @param {!Blockly.Workspace} workspace The workspace.
|
|
* @param {!Blockly.Connection=} parentConnection The parent connection to
|
|
* to connect this block to after instantiating.
|
|
* @param {boolean=} connectedToParentNext Whether the provided parent connection
|
|
* is a next connection, rather than output or statement.
|
|
* @return {!Blockly.Block} The root block created.
|
|
* @private
|
|
*/
|
|
Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace,
|
|
parentConnection, connectedToParentNext) {
|
|
var block = null;
|
|
var prototypeName = xmlBlock.getAttribute('type');
|
|
if (!prototypeName) {
|
|
throw TypeError('Block type unspecified: ' + xmlBlock.outerHTML);
|
|
}
|
|
var id = xmlBlock.getAttribute('id');
|
|
block = workspace.newBlock(prototypeName, id);
|
|
|
|
// Preprocess childNodes so tags can be processed in a consistent order.
|
|
var xmlChildNameMap = Blockly.Xml.mapSupportedXmlTags_(xmlBlock);
|
|
|
|
var shouldCallInitSvg =
|
|
Blockly.Xml.applyMutationTagNodes_(xmlChildNameMap.mutation, block);
|
|
Blockly.Xml.applyCommentTagNodes_(xmlChildNameMap.comment, block);
|
|
Blockly.Xml.applyDataTagNodes_(xmlChildNameMap.data, block);
|
|
|
|
// Connect parent after processing mutation and before setting fields.
|
|
if (parentConnection) {
|
|
if (connectedToParentNext) {
|
|
if (block.previousConnection) {
|
|
parentConnection.connect(block.previousConnection);
|
|
} else {
|
|
throw TypeError(
|
|
'Next block does not have previous statement.');
|
|
}
|
|
} else {
|
|
if (block.outputConnection) {
|
|
parentConnection.connect(block.outputConnection);
|
|
} else if (block.previousConnection) {
|
|
parentConnection.connect(block.previousConnection);
|
|
} else {
|
|
throw TypeError(
|
|
'Child block does not have output or previous statement.');
|
|
}
|
|
}
|
|
}
|
|
|
|
Blockly.Xml.applyFieldTagNodes_(xmlChildNameMap.field, block);
|
|
Blockly.Xml.applyInputTagNodes_(
|
|
xmlChildNameMap.input, workspace, block, prototypeName);
|
|
Blockly.Xml.applyNextTagNodes_(xmlChildNameMap.next, workspace, block);
|
|
|
|
if (shouldCallInitSvg) {
|
|
// InitSvg needs to be called after variable fields are loaded.
|
|
block.initSvg();
|
|
}
|
|
|
|
var inline = xmlBlock.getAttribute('inline');
|
|
if (inline) {
|
|
block.setInputsInline(inline == 'true');
|
|
}
|
|
var disabled = xmlBlock.getAttribute('disabled');
|
|
if (disabled) {
|
|
block.setEnabled(disabled != 'true' && disabled != 'disabled');
|
|
}
|
|
var deletable = xmlBlock.getAttribute('deletable');
|
|
if (deletable) {
|
|
block.setDeletable(deletable == 'true');
|
|
}
|
|
var movable = xmlBlock.getAttribute('movable');
|
|
if (movable) {
|
|
block.setMovable(movable == 'true');
|
|
}
|
|
var editable = xmlBlock.getAttribute('editable');
|
|
if (editable) {
|
|
block.setEditable(editable == 'true');
|
|
}
|
|
var collapsed = xmlBlock.getAttribute('collapsed');
|
|
if (collapsed) {
|
|
block.setCollapsed(collapsed == 'true');
|
|
}
|
|
if (xmlBlock.nodeName.toLowerCase() == 'shadow') {
|
|
// Ensure all children are also shadows.
|
|
var children = block.getChildren(false);
|
|
for (var i = 0, child; (child = children[i]); i++) {
|
|
if (!child.isShadow()) {
|
|
throw TypeError('Shadow block not allowed non-shadow child.');
|
|
}
|
|
}
|
|
// Ensure this block doesn't have any variable inputs.
|
|
if (block.getVarModels().length) {
|
|
throw TypeError('Shadow blocks cannot have variable references.');
|
|
}
|
|
block.setShadow(true);
|
|
}
|
|
return block;
|
|
};
|
|
|
|
/**
|
|
* Decode an XML field tag and set the value of that field on the given block.
|
|
* @param {!Blockly.Block} block The block that is currently being deserialized.
|
|
* @param {string} fieldName The name of the field on the block.
|
|
* @param {!Element} xml The field tag to decode.
|
|
* @private
|
|
*/
|
|
Blockly.Xml.domToField_ = function(block, fieldName, xml) {
|
|
var field = block.getField(fieldName);
|
|
if (!field) {
|
|
console.warn('Ignoring non-existent field ' + fieldName + ' in block ' +
|
|
block.type);
|
|
return;
|
|
}
|
|
field.fromXml(xml);
|
|
};
|
|
|
|
/**
|
|
* Remove any 'next' block (statements in a stack).
|
|
* @param {!Element|!DocumentFragment} xmlBlock XML block element or an empty
|
|
* DocumentFragment if the block was an insertion marker.
|
|
*/
|
|
Blockly.Xml.deleteNext = function(xmlBlock) {
|
|
for (var i = 0, child; (child = xmlBlock.childNodes[i]); i++) {
|
|
if (child.nodeName.toLowerCase() == 'next') {
|
|
xmlBlock.removeChild(child);
|
|
break;
|
|
}
|
|
}
|
|
};
|