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
20 changes: 18 additions & 2 deletions core/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -328,18 +328,34 @@ Blockly.Block.prototype.onchange;

/**
* An optional serialization method for defining how to serialize the
* mutation state. This must be coupled with defining `domToMutation`.
* mutation state to XML. This must be coupled with defining `domToMutation`.
* @type {?function(...):!Element}
*/
Blockly.Block.prototype.mutationToDom;

/**
* An optional deserialization method for defining how to deserialize the
* mutation state. This must be coupled with defining `mutationToDom`.
* mutation state from XML. This must be coupled with defining `mutationToDom`.
* @type {?function(!Element)}
*/
Blockly.Block.prototype.domToMutation;

/**
* An optional serialization method for defining how to serialize the block's
* extra state (eg mutation state) to something JSON compatible. This must be
* coupled with defining `loadExtraState`.
* @type {?function(): *}
*/
Blockly.Block.prototype.saveExtraState;

/**
* An optional serialization method for defining how to deserialize the block's
* extra state (eg mutation state) from something JSON compatible. This must be
* coupled with defining `saveExtraState`.
* @type {?function(*)}
*/
Blockly.Block.prototype.loadExtraState;

/**
* An optional property for suppressing adding STATEMENT_PREFIX and
* STATEMENT_SUFFIX to generated code.
Expand Down
128 changes: 87 additions & 41 deletions core/extensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,11 @@ Blockly.Extensions.registerMutator = function(name, mixinObj, opt_helperFn,
opt_blockList) {
var errorPrefix = 'Error when registering mutator "' + name + '": ';

// Sanity check the mixin object before registering it.
Blockly.Extensions.checkHasFunction_(
errorPrefix, mixinObj.domToMutation, 'domToMutation');
Blockly.Extensions.checkHasFunction_(
errorPrefix, mixinObj.mutationToDom, 'mutationToDom');

Blockly.Extensions.checkHasMutatorProperties_(errorPrefix, mixinObj);
var hasMutatorDialog =
Blockly.Extensions.checkMutatorDialog_(mixinObj, errorPrefix);

if (opt_helperFn && (typeof opt_helperFn != 'function')) {
throw Error('Extension "' + name + '" is not a function');
throw Error(errorPrefix + 'Extension "' + name + '" is not a function');
}

// Sanity checks passed.
Expand Down Expand Up @@ -154,7 +148,7 @@ Blockly.Extensions.apply = function(name, block, isMutator) {

if (isMutator) {
var errorPrefix = 'Error after applying mutator "' + name + '": ';
Blockly.Extensions.checkBlockHasMutatorProperties_(errorPrefix, block);
Blockly.Extensions.checkHasMutatorProperties_(errorPrefix, block);
} else {
if (!Blockly.Extensions.mutatorPropertiesMatch_(
/** @type {!Array<Object>} */ (mutatorProperties), block)) {
Expand Down Expand Up @@ -203,54 +197,100 @@ Blockly.Extensions.checkNoMutatorProperties_ = function(mutationName, block) {
};

/**
* Check that the given object has both or neither of the functions required
* to have a mutator dialog.
* These functions are 'compose' and 'decompose'. If a block has one, it must
* have both.
* Checks if the given object has both the 'mutationToDom' and 'domToMutation'
* functions.
* @param {!Object} object The object to check.
* @param {string} errorPrefix The string to prepend to any error message.
* @return {boolean} True if the object has both functions. False if it has
* neither function.
* @throws {Error} if the object has only one of the functions, or either is
* not actually a function.
* @private
*/
Blockly.Extensions.checkXmlHooks_ = function(object, errorPrefix) {
return Blockly.Extensions.checkHasFunctionPair_(
object, 'mutationToDom', 'domToMutation', errorPrefix);
};

/**
* Checks if the given object has both the 'saveExtraState' and 'loadExtraState'
* functions.
* @param {!Object} object The object to check.
* @param {string} errorPrefix The string to prepend to any error message.
* @return {boolean} True if the object has both functions. False if it has
* neither function.
* @throws {Error} if the object has only one of the functions.
* @throws {Error} if the object has only one of the functions, or either is
* not actually a function.
* @private
*/
Blockly.Extensions.checkJsonHooks_ = function(object, errorPrefix) {
return Blockly.Extensions.checkHasFunctionPair_(
object, 'saveExtraState', 'loadExtraState', errorPrefix);
};

/**
* Checks if the given object has both the 'compose' and 'decompose' functions.
* @param {!Object} object The object to check.
* @param {string} errorPrefix The string to prepend to any error message.
* @return {boolean} True if the object has both functions. False if it has
* neither function.
* @throws {Error} if the object has only one of the functions, or either is
* not actually a function.
* @private
*/
Blockly.Extensions.checkMutatorDialog_ = function(object, errorPrefix) {
var hasCompose = object.compose !== undefined;
var hasDecompose = object.decompose !== undefined;

if (hasCompose && hasDecompose) {
if (typeof object.compose != 'function') {
throw Error(errorPrefix + 'compose must be a function.');
} else if (typeof object.decompose != 'function') {
throw Error(errorPrefix + 'decompose must be a function.');
}
return true;
} else if (!hasCompose && !hasDecompose) {
return false;
}
throw Error(errorPrefix +
'Must have both or neither of "compose" and "decompose"');
return Blockly.Extensions
.checkHasFunctionPair_(object, 'compose', 'decompose', errorPrefix);
};

/**
* Check that a block has required mutator properties. This should be called
* after applying a mutation extension.
* Checks that the given object has both or neither of the given functions, and
* that they are indeed functions.
* @param {!Object} object The object to check.
* @param {string} name1 The name of the first function in the pair.
* @param {string} name2 The name of the second function in the pair.
* @param {string} errorPrefix The string to prepend to any error message.
* @param {!Blockly.Block} block The block to inspect.
* @return {boolean} True if the object has both functions. False if it has
* neither function.
* @throws {Error} If the object has only one of the functions, or either is
* not actually a function.
* @private
*/
Blockly.Extensions.checkBlockHasMutatorProperties_ = function(errorPrefix,
block) {
if (typeof block.domToMutation != 'function') {
throw Error(errorPrefix + 'Applying a mutator didn\'t add "domToMutation"');
}
if (typeof block.mutationToDom != 'function') {
throw Error(errorPrefix + 'Applying a mutator didn\'t add "mutationToDom"');
}
Blockly.Extensions.checkHasFunctionPair_ =
function(object, name1, name2, errorPrefix) {
var has1 = object[name1] !== undefined;
var has2 = object[name2] !== undefined;

if (has1 && has2) {
if (typeof object[name1] != 'function') {
throw Error(errorPrefix + name1 + ' must be a function.');
} else if (typeof object[name2] != 'function') {
throw Error(errorPrefix + name2 + ' must be a function.');
}
return true;
} else if (!has1 && !has2) {
return false;
}
throw Error(errorPrefix +
'Must have both or neither of "' + name1 + '" and "' + name2 + '"');
};

/**
* Checks that the given object required mutator properties.
* @param {string} errorPrefix The string to prepend to any error message.
* @param {!Object} object The object to inspect.
* @private
*/
Blockly.Extensions.checkHasMutatorProperties_ = function(errorPrefix, object) {
var hasXmlHooks = Blockly.Extensions.checkXmlHooks_(object, errorPrefix);
var hasJsonHooks = Blockly.Extensions.checkJsonHooks_(object, errorPrefix);
if (!hasXmlHooks && !hasJsonHooks) {
throw Error(errorPrefix +
'Mutations must contain either XML hooks, or JSON hooks, or both');
}
// A block with a mutator isn't required to have a mutation dialog, but
// it should still have both or neither of compose and decompose.
Blockly.Extensions.checkMutatorDialog_(block, errorPrefix);
Blockly.Extensions.checkMutatorDialog_(object, errorPrefix);
};

/**
Expand All @@ -270,6 +310,12 @@ Blockly.Extensions.getMutatorProperties_ = function(block) {
if (block.mutationToDom !== undefined) {
result.push(block.mutationToDom);
}
if (block.saveExtraState !== undefined) {
result.push(block.saveExtraState);
}
if (block.loadExtraState !== undefined) {
result.push(block.loadExtraState);
}
if (block.compose !== undefined) {
result.push(block.compose);
}
Expand Down
20 changes: 20 additions & 0 deletions core/field.js
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,26 @@ Blockly.Field.prototype.toXml = function(fieldElement) {
return fieldElement;
};

/**
* Saves this fields value as something which can be serialized to JSON. Should
* only be called by the serialization system.
* @return {*} JSON serializable state.
* @package
*/
Blockly.Field.prototype.saveState = function() {
return this.getValue();
};

/**
* Sets the field's state based on the given state value. Should only be called
* by the serialization system.
* @param {*} state The state we want to apply to the field.
* @package
*/
Blockly.Field.prototype.loadState = function(state) {
this.setValue(state);
};

/**
* Dispose of all DOM objects and events belonging to this editable field.
* @package
Expand Down
51 changes: 51 additions & 0 deletions tests/mocha/extensions_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,57 @@ suite('Extensions', function() {
}, /mutationToDom/);
});

test('No saveExtraState', function() {
this.extensionsCleanup_.push('mutator_test');
chai.assert.throws(function() {
Blockly.Extensions.registerMutator('mutator_test',
{
loadExtraState: function() {
return 'loadExtraState';
},
compose: function() {
return 'composeFn';
},
decompose: function() {
return 'decomposeFn';
}
});
}, /saveExtraState/);
});

test('No loadExtraState', function() {
this.extensionsCleanup_.push('mutator_test');
chai.assert.throws(function() {
Blockly.Extensions.registerMutator('mutator_test',
{
saveExtraState: function() {
return 'saveExtraState';
},
compose: function() {
return 'composeFn';
},
decompose: function() {
return 'decomposeFn';
}
});
}, /loadExtraState/);
});

test('No serialization hooks', function() {
this.extensionsCleanup_.push('mutator_test');
chai.assert.throws(function() {
Blockly.Extensions.registerMutator('mutator_test',
{
compose: function() {
return 'composeFn';
},
decompose: function() {
return 'decomposeFn';
}
});
}, 'Mutations must contain either XML hooks, or JSON hooks, or both');
});

test('Has decompose but no compose', function() {
this.extensionsCleanup_.push('mutator_test');
chai.assert.throws(function() {
Expand Down