diff --git a/blocks/math.js b/blocks/math.js index 9fbdde134..755d38dcb 100644 --- a/blocks/math.js +++ b/blocks/math.js @@ -467,7 +467,7 @@ Blockly.Extensions.register("math_is_divisibleby_mutator", * @this {Blockly.Block} */ function() { - goog.mixin(this, Blockly.Blocks.math.IS_DIVISIBLEBY_MUTATOR_MIXIN_); + this.mixin(Blockly.Blocks.math.IS_DIVISIBLEBY_MUTATOR_MIXIN_); this.getField('PROPERTY').setValidator(function(option) { var divisorInput = (option == 'DIVISIBLE_BY'); this.sourceBlock_.updateShape_(divisorInput); @@ -526,9 +526,8 @@ Blockly.Extensions.register("math_modes_of_list_mutator", * @this {Blockly.Block} */ function() { - var thisBlock = this; - goog.mixin(this, Blockly.Blocks.math.LIST_MODES_MUTATOR_MIXIN_); + this.mixin(Blockly.Blocks.math.LIST_MODES_MUTATOR_MIXIN_); this.getField('OP').setValidator(function(newOp) { - thisBlock.updateType_(newOp); - }); + this.updateType_(newOp); + }.bind(this)); }); diff --git a/core/block.js b/core/block.js index 42cfaa74e..ff27cf152 100644 --- a/core/block.js +++ b/core/block.js @@ -1042,6 +1042,34 @@ Blockly.Block.prototype.jsonInit = function(json) { } }; +/** + * Add key/values from mixinObj to this block object. By default, this method + * will check that the keys in mixinObj will not overwrite existing values in + * the block, including prototype values. This provides some insurance against + * mixin / extension incompatibilities with future block features. This check + * can be disabled by passing true as the second argument. + * @param {!Object} mixinObj The key/values pairs to add to this block object. + * @param {boolean=} opt_disableCheck Option flag to disable overwrite checks. + */ +Blockly.Block.prototype.mixin = function(mixinObj, opt_disableCheck) { + if (goog.isDef(opt_disableCheck) && !goog.isBoolean(opt_disableCheck)) { + throw new Error("opt_disableCheck must be a boolean if provided"); + } + if (!opt_disableCheck) { + var overwrites = []; + for (var key in mixinObj) { + if (this[key] !== undefined) { + overwrites.push(key); + } + } + if (overwrites.length) { + throw new Error('Mixin will overwrite block members: ' + + JSON.stringify(overwrites)); + } + } + goog.mixin(this, mixinObj); +}; + /** * Interpolate a message description onto the block. * @param {string} message Text contains interpolation tokens (%1, %2, ...) diff --git a/core/extensions.js b/core/extensions.js index 5259925bd..7ed26f3d7 100644 --- a/core/extensions.js +++ b/core/extensions.js @@ -63,6 +63,19 @@ Blockly.Extensions.register = function(name, initFn) { Blockly.Extensions.ALL_[name] = initFn; }; +/** + * Registers a new extension function that adds all key/value of mixinObj. + * @param {string} name The name of this extension. + * @param {!Object} mixinObj The values to mix in. + * @throws {Error} if the extension name is empty or the extension is already + * registered. + */ +Blockly.Extensions.registerMixin = function(name, mixinObj) { + Blockly.Extensions.register(name, function() { + this.mixin(mixinObj); + }); +}; + /** * Applies an extension method to a block. This should only be called during * block construction. diff --git a/tests/jsunit/extensions_test.js b/tests/jsunit/extensions_test.js index caef7c992..5924f8e05 100644 --- a/tests/jsunit/extensions_test.js +++ b/tests/jsunit/extensions_test.js @@ -193,4 +193,114 @@ function test_parent_tooltip_when_inline() { delete Blockly.Blocks['test_parent_tooltip_when_inline']; delete Blockly.Blocks['test_parent']; } -} \ No newline at end of file +} + +function test_mixin_extension() { + var TEST_MIXIN = { + field: 'FIELD', + method: function() { + console.log('TEXT_MIXIN method()'); + } + }; + + var workspace = new Blockly.Workspace(); + var block; + try { + assertUndefined(Blockly.Extensions.ALL_['mixin_test']); + + // Extension defined before the block type is defined. + Blockly.Extensions.registerMixin('mixin_test', TEST_MIXIN); + assert(goog.isFunction(Blockly.Extensions.ALL_['mixin_test'])); + + Blockly.defineBlocksWithJsonArray([{ + "type": "test_block_mixin", + "message0": "test_block_mixin", + "extensions": ["mixin_test"] + }]); + + block = new Blockly.Block(workspace, 'test_block_mixin'); + + assertEquals(TEST_MIXIN.field, block.field); + assertEquals(TEST_MIXIN.method, block.method); + } finally { + block && block.dispose(); + workspace.dispose(); + + delete Blockly.Extensions.ALL_['mixin_test']; + delete Blockly.Blocks['test_block_mixin']; + } +} + +function test_bad_mixin_overwrites_local_value() { + var TEST_MIXIN_BAD_INPUTLIST = { + inputList: 'bad inputList' // Defined in constructor + }; + + var workspace = new Blockly.Workspace(); + var block; + try { + assertUndefined(Blockly.Extensions.ALL_['mixin_bad_inputList']); + + // Extension defined before the block type is defined. + Blockly.Extensions.registerMixin('mixin_bad_inputList', TEST_MIXIN_BAD_INPUTLIST); + assert(goog.isFunction(Blockly.Extensions.ALL_['mixin_bad_inputList'])); + + Blockly.defineBlocksWithJsonArray([{ + "type": "test_block_bad_inputList", + "message0": "test_block_bad_inputList", + "extensions": ["mixin_bad_inputList"] + }]); + + try { + block = new Blockly.Block(workspace, 'test_block_bad_inputList'); + } catch (e) { + // Expected Error + assert(e.message.indexOf('inputList') >= 0); // Reference the conflict + return; + } + fail('Expected error when constructing block'); + } finally { + block && block.dispose(); + workspace.dispose(); + + delete Blockly.Extensions.ALL_['mixin_bad_inputList']; + delete Blockly.Blocks['test_block_bad_inputList']; + } +} + +function test_bad_mixin_overwrites_prototype() { + var TEST_MIXIN_BAD_COLOUR = { + colour_: 'bad colour_' // Defined on prototype + }; + + var workspace = new Blockly.Workspace(); + var block; + try { + assertUndefined(Blockly.Extensions.ALL_['mixin_bad_colour_']); + + // Extension defined before the block type is defined. + Blockly.Extensions.registerMixin('mixin_bad_colour_', TEST_MIXIN_BAD_COLOUR); + assert(goog.isFunction(Blockly.Extensions.ALL_['mixin_bad_colour_'])); + + Blockly.defineBlocksWithJsonArray([{ + "type": "test_block_bad_colour", + "message0": "test_block_bad_colour", + "extensions": ["mixin_bad_colour_"] + }]); + + try { + block = new Blockly.Block(workspace, 'test_block_bad_colour'); + } catch (e) { + // Expected Error + assert(e.message.indexOf('colour_') >= 0); // Reference the conflict + return; + } + fail('Expected error when constructing block'); + } finally { + block && block.dispose(); + workspace.dispose(); + + delete Blockly.Extensions.ALL_['mixin_bad_colour_']; + delete Blockly.Blocks['test_block_bad_colour']; + } +}