Skip to content
187 changes: 158 additions & 29 deletions core/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ goog.require('Blockly.Events.BlockMove');
goog.require('Blockly.IASTNodeLocationWithBlock');
goog.require('Blockly.utils.deprecation');
goog.require('Blockly.Xml');
goog.require('Blockly.serialization.blocks');

goog.requireType('Blockly.Block');
goog.requireType('Blockly.IConnectionChecker');
Expand Down Expand Up @@ -117,16 +118,15 @@ Blockly.Connection.prototype.connect_ = function(childConnection) {
// Make sure the parentConnection is available.
var orphan;
if (parentConnection.isConnected()) {
var shadowDom = parentConnection.getShadowDom(true);
parentConnection.shadowDom_ = null; // Set to null so it doesn't respawn.
let shadowState = parentConnection.stashShadowState_();
var target = parentConnection.targetBlock();
if (target.isShadow()) {
target.dispose(false);
} else {
parentConnection.disconnect();
orphan = target;
}
parentConnection.shadowDom_ = shadowDom;
parentConnection.applyShadowState_(shadowState);
}

// Connect the new connection to the parent.
Expand Down Expand Up @@ -161,11 +161,10 @@ Blockly.Connection.prototype.connect_ = function(childConnection) {
* @package
*/
Blockly.Connection.prototype.dispose = function() {

// isConnected returns true for shadows and non-shadows.
if (this.isConnected()) {
// Destroy the attached shadow block & its children (if it exists).
this.setShadowDom(null);
this.setShadowStateInternal_();

var targetBlock = this.targetBlock();
if (targetBlock) {
Expand Down Expand Up @@ -472,18 +471,8 @@ Blockly.Connection.prototype.disconnectInternal_ = function(parentBlock,
* @protected
*/
Blockly.Connection.prototype.respawnShadow_ = function() {
var parentBlock = this.getSourceBlock();
var shadow = this.getShadowDom();
if (parentBlock.workspace && shadow) {
var blockShadow = Blockly.Xml.domToBlock(shadow, parentBlock.workspace);
if (blockShadow.outputConnection) {
this.connect(blockShadow.outputConnection);
} else if (blockShadow.previousConnection) {
this.connect(blockShadow.previousConnection);
} else {
throw Error('Child block does not have output or previous statement.');
}
}
// Have to keep respawnShadow_ for backwards compatibility.
this.createShadowBlock_(true);
};

/**
Expand Down Expand Up @@ -581,18 +570,10 @@ Blockly.Connection.prototype.getCheck = function() {

/**
* Changes the connection's shadow block.
* @param {?Element} shadow DOM representation of a block or null.
*/
Blockly.Connection.prototype.setShadowDom = function(shadow) {
this.shadowDom_ = shadow;
var target = this.targetBlock();
if (!target) {
this.respawnShadow_();
} else if (target.isShadow()) {
// The disconnect from dispose will automatically generate the new shadow.
target.dispose(false);
this.respawnShadow_();
}
* @param {?Element} shadowDom DOM representation of a block or null.
*/
Blockly.Connection.prototype.setShadowDom = function(shadowDom) {
this.setShadowStateInternal_({shadowDom: shadowDom});
};

/**
Expand All @@ -610,6 +591,33 @@ Blockly.Connection.prototype.getShadowDom = function(returnCurrent) {
this.shadowDom_;
};

/**
* Changes the connection's shadow block.
* @param {?Blockly.serialization.blocks.State} shadowState An state
* represetation of the block or null.
*/
Blockly.Connection.prototype.setShadowState = function(shadowState) {
this.setShadowStateInternal_({shadowState: shadowState});
};

/**
* Returns the serialized object representation of the connection's shadow
* block.
* @param {boolean=} returnCurrent If true, and the shadow block is currently
* attached to this connection, this serializes the state of that block
* and returns it (so that field values are correct). Otherwise the saved
* state is just returned.
* @return {?Blockly.serialization.blocks.State} Serialized object
* representation of the block, or null.
*/
Blockly.Connection.prototype.getShadowState = function(returnCurrent) {
if (returnCurrent && this.targetBlock() && this.targetBlock().isShadow()) {
return Blockly.serialization.blocks.save(
/** @type {!Blockly.Block} */ (this.targetBlock()));
}
return this.shadowState_;
};

/**
* Find all nearby compatible connections to this connection.
* Type checking does not apply, since this function is used for bumping.
Expand Down Expand Up @@ -678,3 +686,124 @@ Blockly.Connection.prototype.toString = function() {
}
return msg + block.toDevString();
};

/**
* Returns the state of the shadowDom_ and shadowState_ properties, then
* temporarily sets those properties to null so no shadow respawns.
* @return {{shadowDom: ?Element,
* shadowState: ?Blockly.serialization.blocks.State}} The state of both the
* shadowDom_ and shadowState_ properties.
* @private
*/
Blockly.Connection.prototype.stashShadowState_ = function() {
const shadowDom = this.getShadowDom(true);
const shadowState = this.getShadowState(true);
// Set to null so it doesn't respawn.
this.shadowDom_ = null;
this.shadowState_ = null;
return {shadowDom, shadowState};
};

/**
* Reapplies the stashed state of the shadowDom_ and shadowState_ properties.
* @param {{shadowDom: ?Element,
* shadowState: ?Blockly.serialization.blocks.State}} param0 The state to
* reapply to the shadowDom_ and shadowState_ properties.
* @private
*/
Blockly.Connection.prototype.applyShadowState_ =
function({shadowDom, shadowState}) {
this.shadowDom_ = shadowDom;
this.shadowState_ = shadowState;
};

/**
* Sets the state of the shadow of this connection.
* @param {{shadowDom: (?Element|undefined),
* shadowState: (?Blockly.serialization.blocks.State|undefined)}=} param0
* The state to set the shadow of this connection to.
* @private
*/
Blockly.Connection.prototype.setShadowStateInternal_ =
function({shadowDom = null, shadowState = null} = {}) {
// One or both of these should always be null.
// If neither is null, the shadowState will get priority.
this.shadowDom_ = shadowDom;
this.shadowState_ = shadowState;

var target = this.targetBlock();
if (!target) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain these 3 different cases? Not necessarily in code comments yet but just I don't understand why each case does what it does.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can give it my best shot haha

  1. !target means that there is no connected block (no shadow and no real). In this case we want to try to respawn a shadow. If one is created, we serialize it.
  2. target.isShadow() means there is a connected shadow block. In this case we want to dispose of that and try to respawn a new shadow. If one is created, we serialize it.
  3. Otherwise we know we have a target block (b/c !target was false) and we know it's not a shadow, so we must have a connected real block. In this case we just want to create the shadow block (without trying to connect it) and serialize it.

The serializing is to make sure that our shadowDom_ and shadowState_ properties both have the correct value so that both serialization systems work properly.

Tell me if there's anything I didn't explain well. Cuz honestly all of this logic is a bit of a (hopefully necessary?) mess.

And if there's anything I can do to clean this up, definitely tell me that as well.

this.respawnShadow_();
if (this.targetBlock() && this.targetBlock().isShadow()) {
this.serializeShadow_(this.targetBlock());
}
} else if (target.isShadow()) {
target.dispose(false);
this.respawnShadow_();
if (this.targetBlock() && this.targetBlock().isShadow()) {
this.serializeShadow_(this.targetBlock());
}
} else {
var shadow = this.createShadowBlock_(false);
this.serializeShadow_(shadow);
if (shadow) {
shadow.dispose(false);
}
}
};

/**
* Creates a shadow block based on the current shadowState_ or shadowDom_.
* shadowState_ gets priority.
* @param {boolean} attemptToConnect Whether to try to connect the shadow block
* to this connection or not.
* @return {?Blockly.Block} The shadow block that was created, or null if both
* the shadowState_ and shadowDom_ are null.
* @private
*/
Blockly.Connection.prototype.createShadowBlock_ = function(attemptToConnect) {
var parentBlock = this.getSourceBlock();
var shadowState = this.getShadowState();
var shadowDom = this.getShadowDom();
if (!parentBlock.workspace || (!shadowState && !shadowDom)) {
return null;
}

if (shadowState) {
var blockShadow = Blockly.serialization.blocks.loadInternal(
shadowState,
parentBlock.workspace,
attemptToConnect ? this : undefined,
true);
return blockShadow;
}

if (shadowDom) {
blockShadow = Blockly.Xml.domToBlock(shadowDom, parentBlock.workspace);
if (attemptToConnect) {
if (blockShadow.outputConnection) {
this.connect(blockShadow.outputConnection);
} else if (blockShadow.previousConnection) {
this.connect(blockShadow.previousConnection);
} else {
throw Error('Shadow block does not have output or previous statement.');
}
}
return blockShadow;
}
return null;
};

/**
* Saves the given shadow block to both the shadowDom_ and shadowState_
* properties, in their respective serialized forms.
* @param {?Blockly.Block} shadow The shadow to serialize, or null.
* @private
*/
Blockly.Connection.prototype.serializeShadow_ = function(shadow) {
if (!shadow) {
return;
}
this.shadowDom_ = /** @type {!Element} */ (Blockly.Xml.blockToDom(shadow));
this.shadowState_ = Blockly.serialization.blocks.save(shadow);
};
1 change: 0 additions & 1 deletion core/rendered_connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,6 @@ Blockly.RenderedConnection.prototype.respawnShadow_ = function() {
Blockly.RenderedConnection.superClass_.respawnShadow_.call(this);
var blockShadow = this.targetBlock();
if (!blockShadow) {
// This connection must not have a shadowDom_.
return;
}
blockShadow.initSvg();
Expand Down
23 changes: 15 additions & 8 deletions core/serialization/blocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,15 @@
goog.module('Blockly.serialization.blocks');
goog.module.declareLegacyNamespace();

const {BadConnectionCheck, MissingBlockType, MissingConnection, RealChildOfShadow} = goog.require('Blockly.serialization.exceptions');
// 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 Events = goog.require('Blockly.Events');
const {MissingBlockType, MissingConnection, BadConnectionCheck} =
goog.require('Blockly.serialization.exceptions');
const Size = goog.require('Blockly.utils.Size');
// eslint-disable-next-line no-unused-vars
const Workspace = goog.requireType('Blockly.Workspace');
const Xml = goog.require('Blockly.Xml');
const inputTypes = goog.require('Blockly.inputTypes');


Expand Down Expand Up @@ -266,15 +264,14 @@ const saveNextBlocks = function(block, state) {
* shadow block, or any connected real block.
*/
const saveConnection = function(connection) {
const shadow = connection.getShadowDom();
const shadow = connection.getShadowState();
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"', '');
state['shadow'] = shadow;
}
if (child && !child.isShadow()) {
state['block'] = save(child);
Expand Down Expand Up @@ -332,14 +329,18 @@ exports.load = load;
* @param {!Workspace} workspace The workspace to add the block to.
* @param {!Connection=} parentConnection The optional parent connection to
* attach the block to.
* @param {boolean} isShadow Whether the block we are loading is a shadow block
* or not.
* @return {!Block} The block that was just loaded.
*/
const loadInternal = function(state, workspace, parentConnection = undefined) {
const loadInternal = function(
state, workspace, parentConnection = undefined, isShadow = false) {
if (!state['type']) {
throw new MissingBlockType(state);
}

const block = workspace.newBlock(state['type'], state['id']);
block.setShadow(isShadow);
loadCoords(block, state);
loadAttributes(block, state);
loadExtraState(block, state);
Expand All @@ -351,6 +352,8 @@ const loadInternal = function(state, workspace, parentConnection = undefined) {
initBlock(block, workspace.rendered);
return block;
};
/** @package */
exports.loadInternal = loadInternal;

/**
* Applies any coordinate information available on the state object to the
Expand Down Expand Up @@ -417,6 +420,10 @@ const tryToConnectParent = function(parentConnection, child, state) {
if (!parentConnection) {
return;
}

if (parentConnection.getSourceBlock().isShadow() && !child.isShadow()) {
throw new RealChildOfShadow(state);
}

let connected = false;
let childConnection;
Expand Down Expand Up @@ -542,7 +549,7 @@ const loadNextBlocks = function(block, state) {
*/
const loadConnection = function(connection, connectionState) {
if (connectionState['shadow']) {
connection.setShadowDom(Blockly.Xml.textToDom(connectionState['shadow']));
connection.setShadowState(connectionState['shadow']);
}
if (connectionState['block']) {
loadInternal(
Expand Down
Loading