From 54f0e06e2104b733f42f23d6e4a528b3574e3c28 Mon Sep 17 00:00:00 2001 From: Mark Gibson Date: Wed, 14 Feb 2018 18:14:59 +0000 Subject: [PATCH] Customising field types using a register of fields #1584 (#1594) Implement #1584 - Fields now registered by their JSON type name, allowing new custom fields and overriding of the standard fields. Replaces the manual switch statement for loading fields from JSON block definitions. --- core/block.js | 49 ++++++++++---------------------------- core/field.js | 46 +++++++++++++++++++++++++++++++++++ core/field_angle.js | 2 ++ core/field_checkbox.js | 2 ++ core/field_colour.js | 2 ++ core/field_date.js | 2 ++ core/field_dropdown.js | 2 ++ core/field_image.js | 2 ++ core/field_label.js | 2 ++ core/field_number.js | 2 ++ core/field_textinput.js | 2 ++ core/field_variable.js | 2 ++ tests/jsunit/field_test.js | 27 +++++++++++++++++++++ 13 files changed, 106 insertions(+), 36 deletions(-) diff --git a/core/block.js b/core/block.js index 23670666f..1c6008fb3 100644 --- a/core/block.js +++ b/core/block.js @@ -1231,44 +1231,21 @@ Blockly.Block.prototype.interpolate_ = function(message, args, lastDummyAlign) { case 'input_dummy': input = this.appendDummyInput(element['name']); break; - case 'field_label': - field = Blockly.FieldLabel.fromJson(element); - break; - case 'field_input': - field = Blockly.FieldTextInput.fromJson(element); - break; - case 'field_angle': - field = Blockly.FieldAngle.fromJson(element); - break; - case 'field_checkbox': - field = Blockly.FieldCheckbox.fromJson(element); - break; - case 'field_colour': - field = Blockly.FieldColour.fromJson(element); - break; - case 'field_variable': - field = Blockly.FieldVariable.fromJson(element); - break; - case 'field_dropdown': - field = Blockly.FieldDropdown.fromJson(element); - break; - case 'field_image': - field = Blockly.FieldImage.fromJson(element); - break; - case 'field_number': - field = Blockly.FieldNumber.fromJson(element); - break; - case 'field_date': - if (Blockly.FieldDate) { - field = Blockly.FieldDate.fromJson(element); - break; - } - // Fall through if FieldDate is not compiled in. + default: + field = Blockly.Field.fromJson(element); + // Unknown field. - if (element['alt']) { - element = element['alt']; - altRepeat = true; + if (!field) { + if (element['alt']) { + element = element['alt']; + altRepeat = true; + } else { + console.warn('Blockly could not create a field of type ' + + element['type'] + + '. You may need to register your custom field. See ' + + 'github.com/google/blockly/issues/1584'); + } } } } diff --git a/core/field.js b/core/field.js index 66f457a3a..469592539 100644 --- a/core/field.js +++ b/core/field.js @@ -52,6 +52,52 @@ Blockly.Field = function(text, opt_validator) { this.setValidator(opt_validator); }; +/** + * The set of all registered fields, keyed by field type as used in the JSON + * definition of a block. + * @type {!Object} + * @private + */ +Blockly.Field.TYPE_MAP_ = {}; + +/** + * Registers a field type. May also override an existing field type. + * Blockly.Field.fromJson uses this registry to find the appropriate field. + * @param {!string} type The field type name as used in the JSON definition. + * @param {!{fromJson: Function}} fieldClass The field class containing a + * fromJson function that can construct an instance of the field. + * @throws {Error} if the type name is empty, or the fieldClass is not an + * object containing a fromJson function. + */ +Blockly.Field.register = function(type, fieldClass) { + if (!goog.isString(type) || goog.string.isEmptyOrWhitespace(type)) { + throw new Error('Invalid field type "' + type + '"'); + } + if (!goog.isObject(fieldClass) || !goog.isFunction(fieldClass.fromJson)) { + throw new Error('Field "' + fieldClass + + '" must have a fromJson function'); + } + Blockly.Field.TYPE_MAP_[type] = fieldClass; +}; + +/** + * Construct a Field from a JSON arg object. + * Finds the appropriate registered field by the type name as registered using + * Blockly.Field.register. + * @param {!Object} options A JSON object with a type and options specific + * to the field type. + * @returns {?Blockly.Field} The new field instance or null if a field wasn't + * found with the given type name + * @package + */ +Blockly.Field.fromJson = function(options) { + var fieldClass = Blockly.Field.TYPE_MAP_[options['type']]; + if (fieldClass) { + return fieldClass.fromJson(options); + } + return null; +}; + /** * Temporary cache of text widths. * @type {Object} diff --git a/core/field_angle.js b/core/field_angle.js index 59384d08b..104e746f5 100644 --- a/core/field_angle.js +++ b/core/field_angle.js @@ -327,3 +327,5 @@ Blockly.FieldAngle.prototype.classValidator = function(text) { } return String(n); }; + +Blockly.Field.register('field_angle', Blockly.FieldAngle); diff --git a/core/field_checkbox.js b/core/field_checkbox.js index d723bab1f..2d3a3c4be 100644 --- a/core/field_checkbox.js +++ b/core/field_checkbox.js @@ -127,3 +127,5 @@ Blockly.FieldCheckbox.prototype.showEditor_ = function() { this.setValue(String(newState).toUpperCase()); } }; + +Blockly.Field.register('field_checkbox', Blockly.FieldCheckbox); diff --git a/core/field_colour.js b/core/field_colour.js index 77632c184..4ba161a84 100644 --- a/core/field_colour.js +++ b/core/field_colour.js @@ -234,3 +234,5 @@ Blockly.FieldColour.widgetDispose_ = function() { } Blockly.Events.setGroup(false); }; + +Blockly.Field.register('field_colour', Blockly.FieldColour); diff --git a/core/field_date.js b/core/field_date.js index 53ced0e14..662198107 100644 --- a/core/field_date.js +++ b/core/field_date.js @@ -347,3 +347,5 @@ Blockly.FieldDate.CSS = [ ' color: #fff;', '}' ]; + +Blockly.Field.register('field_date', Blockly.FieldDate); diff --git a/core/field_dropdown.js b/core/field_dropdown.js index 3ecca22ba..79fdbf76a 100644 --- a/core/field_dropdown.js +++ b/core/field_dropdown.js @@ -563,3 +563,5 @@ Blockly.FieldDropdown.prototype.dispose = function() { Blockly.WidgetDiv.hideIfOwner(this); Blockly.FieldDropdown.superClass_.dispose.call(this); }; + +Blockly.Field.register('field_dropdown', Blockly.FieldDropdown); diff --git a/core/field_image.js b/core/field_image.js index eaa8e9995..18b832730 100644 --- a/core/field_image.js +++ b/core/field_image.js @@ -216,3 +216,5 @@ Blockly.FieldImage.prototype.showEditor_ = function() { this.clickHandler_(this); } }; + +Blockly.Field.register('field_image', Blockly.FieldImage); diff --git a/core/field_label.js b/core/field_label.js index 039f73a75..53a13e695 100644 --- a/core/field_label.js +++ b/core/field_label.js @@ -114,3 +114,5 @@ Blockly.FieldLabel.prototype.getSvgRoot = function() { Blockly.FieldLabel.prototype.setTooltip = function(newTip) { this.textElement_.tooltip = newTip; }; + +Blockly.Field.register('field_label', Blockly.FieldLabel); diff --git a/core/field_number.js b/core/field_number.js index ca90d4d2c..9bc27cc5a 100644 --- a/core/field_number.js +++ b/core/field_number.js @@ -114,3 +114,5 @@ Blockly.FieldNumber.prototype.classValidator = function(text) { n = goog.math.clamp(n, this.min_, this.max_); return String(n); }; + +Blockly.Field.register('field_number', Blockly.FieldNumber); diff --git a/core/field_textinput.js b/core/field_textinput.js index a8490ab18..db1c04bf5 100644 --- a/core/field_textinput.js +++ b/core/field_textinput.js @@ -421,3 +421,5 @@ Blockly.FieldTextInput.nonnegativeIntegerValidator = function(text) { } return n; }; + +Blockly.Field.register('field_input', Blockly.FieldTextInput); diff --git a/core/field_variable.js b/core/field_variable.js index 333fee2d6..7a3d6a37c 100644 --- a/core/field_variable.js +++ b/core/field_variable.js @@ -352,3 +352,5 @@ Blockly.FieldVariable.prototype.onItemSelected = function(menu, menuItem) { } this.setValue(id); }; + +Blockly.Field.register('field_variable', Blockly.FieldVariable); diff --git a/tests/jsunit/field_test.js b/tests/jsunit/field_test.js index 6a0faebc8..2da04834d 100644 --- a/tests/jsunit/field_test.js +++ b/tests/jsunit/field_test.js @@ -97,3 +97,30 @@ function test_field_isEditable_nonEditableBlock_false() { assertFalse('Non-editable field with non-editable block is not editable', field.isCurrentlyEditable()); } + +function test_field_register_with_custom_field() { + var CustomFieldType = function(value) { + CustomFieldType.superClass_.constructor.call(this, value); + }; + goog.inherits(CustomFieldType, Blockly.Field); + + CustomFieldType.fromJson = function(options) { + return new CustomFieldType(options['value']); + }; + + var json = { + type: 'field_custom_test', + value: 'ok' + }; + + // before registering + var field = Blockly.Field.fromJson(json); + assertNull(field); + + Blockly.Field.register('field_custom_test', CustomFieldType); + + // after registering + field = Blockly.Field.fromJson(json); + assertNotNull(field); + assertEquals(field.getValue(), 'ok'); +}