From e1e3f273d2a0887dd60b4b00049caaee79b33b52 Mon Sep 17 00:00:00 2001 From: Andrew n marshall Date: Tue, 13 Feb 2018 15:01:33 -0800 Subject: [PATCH 001/115] Remove forceRerender() from FieldDropDown. Errantly forced whole block to rerender, including fields that had not yet been initialized. Fixes #1609 --- core/field_dropdown.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/field_dropdown.js b/core/field_dropdown.js index 367e338b7..3ecca22ba 100644 --- a/core/field_dropdown.js +++ b/core/field_dropdown.js @@ -131,8 +131,6 @@ Blockly.FieldDropdown.prototype.init = function() { ' ' + Blockly.FieldDropdown.ARROW_CHAR)); Blockly.FieldDropdown.superClass_.init.call(this); - // Make sure the arrow gets rendered. - this.forceRerender(); }; /** From 54f0e06e2104b733f42f23d6e4a528b3574e3c28 Mon Sep 17 00:00:00 2001 From: Mark Gibson Date: Wed, 14 Feb 2018 18:14:59 +0000 Subject: [PATCH 002/115] 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'); +} From 2a00055e5743b8a334aec67586bec5e3e0ae296d Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Wed, 14 Feb 2018 10:42:27 -0800 Subject: [PATCH 003/115] Don't register mouse events if pointer events are supported --- core/blockly.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/core/blockly.js b/core/blockly.js index 427d45199..892ee152e 100644 --- a/core/blockly.js +++ b/core/blockly.js @@ -450,10 +450,14 @@ Blockly.bindEventWithChecks_ = function(node, name, thisObject, func, } }; - node.addEventListener(name, wrapFunc, false); - var bindData = [[node, name, wrapFunc]]; + var bindData = []; + // Don't register the mouse event if an equivalent pointer event is supported. + if (!window.PointerEvent || !(name in Blockly.Touch.TOUCH_MAP)) { + node.addEventListener(name, wrapFunc, false); + bindData.push([node, name, wrapFunc]); + } - // Add equivalent touch event. + // Add equivalent touch or pointer event. if (name in Blockly.Touch.TOUCH_MAP) { var touchWrapFunc = function(e) { wrapFunc(e); @@ -495,10 +499,13 @@ Blockly.bindEvent_ = function(node, name, thisObject, func) { } }; - node.addEventListener(name, wrapFunc, false); - var bindData = [[node, name, wrapFunc]]; - - // Add equivalent touch event. + var bindData = []; + // Don't register the mouse event if an equivalent pointer event is supported. + if (!window.PointerEvent || !(name in Blockly.Touch.TOUCH_MAP)) { + node.addEventListener(name, wrapFunc, false); + bindData.push([node, name, wrapFunc]); + } + // Add equivalent touch or pointer event. if (name in Blockly.Touch.TOUCH_MAP) { var touchWrapFunc = function(e) { // Punt on multitouch events. From 551b05b4f1b8d9931b0b99ed6d694f8e45826136 Mon Sep 17 00:00:00 2001 From: RoboErikG Date: Wed, 14 Feb 2018 10:52:49 -0800 Subject: [PATCH 004/115] Fix typo in BKY message key (#1616) We had {% instead of %{ Fixes #1613 --- blocks/text.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blocks/text.js b/blocks/text.js index 2252815a6..b0dc9cb3b 100644 --- a/blocks/text.js +++ b/blocks/text.js @@ -89,7 +89,7 @@ Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT "previousStatement": null, "nextStatement": null, "colour": "%{BKY_TEXTS_HUE}", - "tooltip": "{%BKY_TEXT_CREATE_JOIN_ITEM_TOOLTIP}", + "tooltip": "%{BKY_TEXT_CREATE_JOIN_ITEM_TOOLTIP}", "enableContextMenu": false }, { From 73417b831725262c4a226fc3784a3ac7f5a23562 Mon Sep 17 00:00:00 2001 From: Neil Fraser Date: Wed, 14 Feb 2018 11:22:55 -0800 Subject: [PATCH 005/115] Reduce size of XML by omitting empty variable tag. --- core/xml.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/xml.js b/core/xml.js index e99a95a58..baf575b75 100644 --- a/core/xml.js +++ b/core/xml.js @@ -42,8 +42,10 @@ goog.require('goog.dom'); */ Blockly.Xml.workspaceToDom = function(workspace, opt_noId) { var xml = goog.dom.createDom('xml'); - xml.appendChild(Blockly.Xml.variablesToDom( - Blockly.Variables.allUsedVarModels(workspace))); + var variables = Blockly.Variables.allUsedVarModels(workspace); + if (variables.length) { + xml.appendChild(Blockly.Xml.variablesToDom(variables)); + } var blocks = workspace.getTopBlocks(true); for (var i = 0, block; block = blocks[i]; i++) { xml.appendChild(Blockly.Xml.blockToDomWithXY(block, opt_noId)); From 446c1bbce473989ee3d5c3e30fa3bd2cb0071365 Mon Sep 17 00:00:00 2001 From: Andrew n marshall Date: Wed, 14 Feb 2018 16:15:43 -0800 Subject: [PATCH 006/115] Updateing Closure to 20180204.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f7c1192d6..d843adfd5 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "unused": true }, "dependencies": { - "google-closure-library": "^20171203.0.0", + "google-closure-library": "^20180204.0.0", "install": "^0.8.8", "npm": "^4.4.4", "webdriverio": "^4.6.2" From e5a2ef30f2bf06833b3b336a3a1919aa54ba49d5 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Thu, 15 Feb 2018 11:26:06 -0800 Subject: [PATCH 007/115] Switch to goog.events.BrowserFeature.POINTER_EVENTS --- core/blockly.js | 8 ++++++-- core/touch.js | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/core/blockly.js b/core/blockly.js index 892ee152e..d4a7a094b 100644 --- a/core/blockly.js +++ b/core/blockly.js @@ -53,7 +53,9 @@ goog.require('Blockly.WorkspaceSvg'); goog.require('Blockly.constants'); goog.require('Blockly.inject'); goog.require('Blockly.utils'); + goog.require('goog.color'); +goog.require('goog.events.BrowserFeature'); goog.require('goog.userAgent'); @@ -452,7 +454,8 @@ Blockly.bindEventWithChecks_ = function(node, name, thisObject, func, var bindData = []; // Don't register the mouse event if an equivalent pointer event is supported. - if (!window.PointerEvent || !(name in Blockly.Touch.TOUCH_MAP)) { + if (!goog.events.BrowserFeature.POINTER_EVENTS || + !(name in Blockly.Touch.TOUCH_MAP)) { node.addEventListener(name, wrapFunc, false); bindData.push([node, name, wrapFunc]); } @@ -501,7 +504,8 @@ Blockly.bindEvent_ = function(node, name, thisObject, func) { var bindData = []; // Don't register the mouse event if an equivalent pointer event is supported. - if (!window.PointerEvent || !(name in Blockly.Touch.TOUCH_MAP)) { + if (!goog.events.BrowserFeature.POINTER_EVENTS || + !(name in Blockly.Touch.TOUCH_MAP)) { node.addEventListener(name, wrapFunc, false); bindData.push([node, name, wrapFunc]); } diff --git a/core/touch.js b/core/touch.js index 8aad46b08..7a883c241 100644 --- a/core/touch.js +++ b/core/touch.js @@ -48,7 +48,7 @@ Blockly.Touch.touchIdentifier_ = null; * @type {Object} */ Blockly.Touch.TOUCH_MAP = {}; -if (window.PointerEvent) { +if (goog.events.BrowserFeature.POINTER_EVENTS) { Blockly.Touch.TOUCH_MAP = { 'mousedown': ['pointerdown'], 'mousemove': ['pointermove'], From f3a4664dea4e03d09bc1d818e489f42b52cfd7f6 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Thu, 15 Feb 2018 17:18:23 -0800 Subject: [PATCH 008/115] Delete extra variables at the end of a procedure argument edit --- blocks/procedures.js | 70 ++++++++++++++++++++++++++++++-------------- core/mutator.js | 24 +++++++++++++++ 2 files changed, 72 insertions(+), 22 deletions(-) diff --git a/blocks/procedures.js b/blocks/procedures.js index 8e890a767..c69b38d3c 100644 --- a/blocks/procedures.js +++ b/blocks/procedures.js @@ -217,8 +217,7 @@ Blockly.Blocks['procedures_defnoreturn'] = { while (paramBlock) { var varName = paramBlock.getFieldValue('NAME'); this.arguments_.push(varName); - var variable = Blockly.Variables.getOrCreateVariablePackage( - this.workspace, null, varName, ''); + var variable = this.workspace.getVariable(varName, ''); this.argumentVarModels_.push(variable); this.paramIds_.push(paramBlock.id); paramBlock = paramBlock.nextConnection && @@ -469,6 +468,15 @@ Blockly.Blocks['procedures_mutatorarg'] = { */ init: function() { var field = new Blockly.FieldTextInput('x', this.validator_); + // Hack: override showEditor to do just a little bit more work. + // We don't have a good place to hook into the start of a text edit. + var oldShowEditorFn = field.showEditor_.bind(field); + var newShowEditorFn = function() { + this.createdVariables_ = []; + oldShowEditorFn(); + }; + field.showEditor_ = newShowEditorFn.bind(field); + this.appendDummyInput() .appendField(Blockly.Msg.PROCEDURES_MUTATORARG_TITLE) .appendField(field, 'NAME'); @@ -478,43 +486,61 @@ Blockly.Blocks['procedures_mutatorarg'] = { this.setTooltip(Blockly.Msg.PROCEDURES_MUTATORARG_TOOLTIP); this.contextMenu = false; + // Get a handle on the parent workspace. + // TODO: Do I need to delete this during dispose? + this.outerWs_ = Blockly.Mutator.findParentWs(this.workspace); // Create the default variable when we drag the block in from the flyout. // Have to do this after installing the field on the block. - field.onFinishEditing_ = this.createNewVar_; + field.onFinishEditing_ = this.deleteIntermediateVars_; + // Create an empty list so onFinishEditing_ has something to look at, even + // though the editor was never opened. + field.createdVariables_ = []; field.onFinishEditing_('x'); }, /** - * Obtain a valid name for the procedure. + * Obtain a valid name for the procedure argument. Create a variable if + * necessary. * Merge runs of whitespace. Strip leading and trailing whitespace. * Beyond this, all names are legal. - * @param {string} newVar User-supplied name. + * @param {string} varName User-supplied name. * @return {?string} Valid name, or null if a name was not specified. * @private - * @this Blockly.Block + * @this Blockly.FieldTextInput */ - validator_: function(newVar) { - newVar = newVar.replace(/[\s\xa0]+/g, ' ').replace(/^ | $/g, ''); - return newVar || null; + validator_: function(varName) { + varName = varName.replace(/[\s\xa0]+/g, ' ').replace(/^ | $/g, ''); + if (!varName) { + return null; + } + var model = this.sourceBlock_.outerWs_.getVariable(varName, ''); + if (model && model.name != varName) { + // Rename the variable (case change) + this.outerWs_.renameVarById(model.getId(), varName); + } + if (!model) { + model = this.sourceBlock_.outerWs_.createVariable(varName, ''); + if (model && this.createdVariables_) { + this.createdVariables_.push(model); + } + } + return varName; }, /** * Called when focusing away from the text field. - * Creates a new variable with this name. + * Deletes all variables that were created as the user typed their intended + * variable name. * @param {string} newText The new variable name. * @private * @this Blockly.FieldTextInput */ - createNewVar_: function(newText) { - var source = this.sourceBlock_; - if (source && source.workspace && source.workspace.options && - source.workspace.options.parentWorkspace) { - var workspace = source.workspace.options.parentWorkspace; - var variableType = ''; - var variable = workspace.getVariable(newText, variableType); - // If there is a case change, rename the variable. - if (variable && variable.name !== newText) { - workspace.renameVariableById(variable.getId(), newText); - } else { - workspace.createVariable(newText, variableType); + deleteIntermediateVars_: function(newText) { + if (!this.sourceBlock_.outerWs_) { + return; + } + for (var i = 0; i < this.createdVariables_.length; i++) { + var model = this.createdVariables_[i]; + if (model.name != newText) { + this.sourceBlock_.outerWs_.deleteVariableById(model.getId()); } } } diff --git a/core/mutator.js b/core/mutator.js index 1446c48ab..a57727ae2 100644 --- a/core/mutator.js +++ b/core/mutator.js @@ -415,6 +415,30 @@ Blockly.Mutator.reconnect = function(connectionChild, block, inputName) { return false; }; +/** + * Get the parent workspace of a workspace that is inside a mutator, taking into + * account whether it is a flyout. + * @param {?Blockly.Workspace} workspace The workspace that is inside a mutator. + * @return {?Blockly.Workspace} The mutator's parent workspace or null. + * @package + */ +Blockly.Mutator.findParentWs = function(workspace) { + var outerWs = null; + if (workspace && workspace.options) { + var parent = workspace.options.parentWorkspace; + // If we were in a flyout in a mutator, need to go up two levels to find + // the actual parent. + if (workspace.isFlyout) { + if (parent && parent.options) { + outerWs = parent.options.parentWorkspace; + } + } else if (parent) { + outerWs = parent; + } + } + return outerWs; +}; + // Export symbols that would otherwise be renamed by Closure compiler. if (!goog.global['Blockly']) { goog.global['Blockly'] = {}; From af59e27d960fa08783956b8c1f7e26d6395a5d9b Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Fri, 16 Feb 2018 11:02:03 -0800 Subject: [PATCH 009/115] Cleanup --- blocks/procedures.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/blocks/procedures.js b/blocks/procedures.js index c69b38d3c..1874ee1d0 100644 --- a/blocks/procedures.js +++ b/blocks/procedures.js @@ -470,12 +470,12 @@ Blockly.Blocks['procedures_mutatorarg'] = { var field = new Blockly.FieldTextInput('x', this.validator_); // Hack: override showEditor to do just a little bit more work. // We don't have a good place to hook into the start of a text edit. - var oldShowEditorFn = field.showEditor_.bind(field); + field.oldShowEditorFn_ = field.showEditor_; var newShowEditorFn = function() { this.createdVariables_ = []; - oldShowEditorFn(); + this.oldShowEditorFn_(); }; - field.showEditor_ = newShowEditorFn.bind(field); + field.showEditor_ = newShowEditorFn; this.appendDummyInput() .appendField(Blockly.Msg.PROCEDURES_MUTATORARG_TITLE) @@ -486,9 +486,6 @@ Blockly.Blocks['procedures_mutatorarg'] = { this.setTooltip(Blockly.Msg.PROCEDURES_MUTATORARG_TOOLTIP); this.contextMenu = false; - // Get a handle on the parent workspace. - // TODO: Do I need to delete this during dispose? - this.outerWs_ = Blockly.Mutator.findParentWs(this.workspace); // Create the default variable when we drag the block in from the flyout. // Have to do this after installing the field on the block. field.onFinishEditing_ = this.deleteIntermediateVars_; @@ -508,17 +505,18 @@ Blockly.Blocks['procedures_mutatorarg'] = { * @this Blockly.FieldTextInput */ validator_: function(varName) { + var outerWs = Blockly.Mutator.findParentWs(this.sourceBlock_.workspace); varName = varName.replace(/[\s\xa0]+/g, ' ').replace(/^ | $/g, ''); if (!varName) { return null; } - var model = this.sourceBlock_.outerWs_.getVariable(varName, ''); + var model = outerWs.getVariable(varName, ''); if (model && model.name != varName) { // Rename the variable (case change) - this.outerWs_.renameVarById(model.getId(), varName); + outerWs.renameVarById(model.getId(), varName); } if (!model) { - model = this.sourceBlock_.outerWs_.createVariable(varName, ''); + model = outerWs.createVariable(varName, ''); if (model && this.createdVariables_) { this.createdVariables_.push(model); } @@ -534,13 +532,14 @@ Blockly.Blocks['procedures_mutatorarg'] = { * @this Blockly.FieldTextInput */ deleteIntermediateVars_: function(newText) { - if (!this.sourceBlock_.outerWs_) { + var outerWs = Blockly.Mutator.findParentWs(this.sourceBlock_.workspace); + if (!outerWs) { return; } for (var i = 0; i < this.createdVariables_.length; i++) { var model = this.createdVariables_[i]; if (model.name != newText) { - this.sourceBlock_.outerWs_.deleteVariableById(model.getId()); + outerWs.deleteVariableById(model.getId()); } } } From d9a7f84b98dbb5ed306cdf8ba84ecae2c90e5147 Mon Sep 17 00:00:00 2001 From: Andrew n marshall Date: Fri, 16 Feb 2018 11:14:40 -0800 Subject: [PATCH 010/115] Adding test blocks into the playground (#1629) The new toolbox option includes a number of test blocks largely pulled from the android project, testing various configurations and edge cases. --- media/test_200px.png | Bin 0 -> 2165 bytes media/test_30px.png | Bin 0 -> 1127 bytes media/test_50px.png | Bin 0 -> 1590 bytes media/test_a.png | Bin 0 -> 778 bytes media/test_b.png | Bin 0 -> 769 bytes media/test_c.png | Bin 0 -> 856 bytes media/test_d.png | Bin 0 -> 738 bytes media/test_e.png | Bin 0 -> 393 bytes media/test_f.png | Bin 0 -> 343 bytes media/test_g.png | Bin 0 -> 1169 bytes media/test_h.png | Bin 0 -> 313 bytes media/test_i.png | Bin 0 -> 200 bytes media/test_j.png | Bin 0 -> 453 bytes media/test_k.png | Bin 0 -> 612 bytes media/test_l.png | Bin 0 -> 254 bytes media/test_m.png | Bin 0 -> 1044 bytes tests/blocks/test_blocks.js | 493 ++++++++++++++++++++++++++++++++++++ tests/playground.html | 44 ++++ 18 files changed, 537 insertions(+) create mode 100644 media/test_200px.png create mode 100644 media/test_30px.png create mode 100644 media/test_50px.png create mode 100644 media/test_a.png create mode 100644 media/test_b.png create mode 100644 media/test_c.png create mode 100644 media/test_d.png create mode 100644 media/test_e.png create mode 100644 media/test_f.png create mode 100644 media/test_g.png create mode 100644 media/test_h.png create mode 100644 media/test_i.png create mode 100644 media/test_j.png create mode 100644 media/test_k.png create mode 100644 media/test_l.png create mode 100644 media/test_m.png create mode 100644 tests/blocks/test_blocks.js diff --git a/media/test_200px.png b/media/test_200px.png new file mode 100644 index 0000000000000000000000000000000000000000..a08e2fa6854355fe5008f404c00475076506d08b GIT binary patch literal 2165 zcmb_edpOgJ8~<+0C6}pRVY%d1ITZ?LA<89UE^~>+nCc{jY%Y=OY$^9fbo4VMwQ@T) z&f3Ou*-{Y)=P=`7OvFKqgfz|1d7j_zoaa2hzs~cV_jxbR=b!g|-p?PO_s#S`Ix5Pk z$pHYMh;Xv^+O8eHhphB=uJxrp+%7w!?GWCw+m<97l(CI>MmYIJ1AzRV-vb1YOJD#X ztBSC<^^Pl95V%)rUftKejDsJ#%RqZGLx-NytKkz@N_|c^`AVyebJ3&1gb)Y)~E{|btGw(W^pUiR`u7Q^iSw27>2T~B zwv@U5uabkp706)u%dxh#i0cc%_-h>WFRdilbF&Y6$C z9__ynLo;3)V=rh`s&o_5JIaO?l=IciSUv)Yg^&Y`;QmG0y(9z}*UlytZGw!0oKOfQ z$M8I44avlR0tULNU5vfhQXbnYvC`Kt(FQ+u2kPIE=mfeE9ivmAw^SYZH(K{ z`Xn7=jnFGacHf!v5XE^V5bCjW)81p;sfjxZ>1rW+@^Q-5k!s`Ii08}?$@R=HQcIVF z>6~H{QRy?w&M<_GP>fi&K@sK&N6pkJw1h!oKSx9y2U$b#4(u9%Ljk!Lq6J3Aj;0mb z6u9T>qp8tm5wN)1(-js4!!De4VlIdl=GRtG6eE6H{w7Li!>(;LKylP@CDAB+IGfY_ zZL4*pd!12RIUjltS~AD|bopaSGYweO7wi*R!UmW~xrs4Xw?sl2(pMsaL1S*5n!UAo zVD|Z((Z`7f&CjNmS~;O&y`_7u;faJ}AZrkQrxg%T?00tV^?MF$-)Y#unw!@yN>P2; zCmv=tB4-5AS3vZNJj$WpS8`yK>IyV?do?VQP9CMmk0%>GNK}rKGq?Q`wk0E^ZQ|Ie zW&mzHV9SjoLtO8}=iQwlDgnLE1CZol{BTNC$)RKcyR~hND%AqE8jLEgaJp0=9c{5d zwdeuL4hfEz@UcQ<-yl7liuf|b_mxfqjgmi!2G6%}j4v)6%hIX8HDpQN0 z>fU`Dvm=iDDRT)o+#4k>>V8k=-3V)oaV`z8r56)atE^$?ogmtqdUVnW`Y8T6_sdyI zab~0CqSw{+{tpG0z0BA0)6^55a!iJG~Zm{Z)lGkK!BBg+lRz|J>dLy8?Ul$ib3TX#QD-!eN{ zOO6eCeTs9zWF$T=Yt?0ok-h0*vF=^ySH8jW(I;9uY08VO7B)hIU(7^+53UM#EfZri z7m{oC|72haHUi2bSyiv!{q&96iO@BQ@vRGYqHFW10u-^2D&MOKdpKTo^`D0k5tEOGGK4V)q zroY~FUD3kMmKLFr0|qw@=mA<{AINV}XrIA3cFfen7nLNJ?-RH3a=C#8RqtPu-?K|l z3;`|r(iqM5OHxpr_sYuAF#KuMDIlt?P*UY*(cBT^6F&*Sc?*M$$3>tG&)oTwfh#Mc z&6z1zB+UcTvyu)ENl|?B;^8)(wHj#;Gf?n|O3ZwRp+8)n&c_Zg@vz|5P;GyG&ULZ; z^}gCC3m{Yif4JHRL@XVrp?6`RbzXEnvJbBPGP0iBp=nQVQJQI%Jf8lliy8L(i}d|t zApe@~&m|!hvO}|crbJOFW6pYXVimsox5sdO;ohG`_K&jpg?KPBy~Vx4hjt9}DrZ>R z+Hum(RD1T=(>cRc<9811y~nk^I0Ppm=BfO@A=!B7BHWkb{)b`*rcT9^T3&7L_*1;S zt~$6eJ=ugl1=*5sFdPNTKsN9_%Q?Zk@)KQ^&hG)sJR72+wQDHpsrtne`5Y->*>KeLdIQgwQq!U5WZA4(DJmeve%g> zuJa>DBJeL`w>OKunAqo?29>4cS;_r?V?KeA0x>8yK`LLKC{BRJE9y!qWc-&<5ae%z zPwkP_DtVyU+(~Z)sJ$xRM5Ds_&trVRux<-*#57Fa^Z(Ux`)>dc4oG{d-Njq~0oVH2 ACjbBd literal 0 HcmV?d00001 diff --git a/media/test_30px.png b/media/test_30px.png new file mode 100644 index 0000000000000000000000000000000000000000..bf2532b79776a92488d1cef9ba8ec828c1fcf21b GIT binary patch literal 1127 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 U@Wm>r`=G+y)78&qol`;+0ODY>x&QzG literal 0 HcmV?d00001 diff --git a/media/test_a.png b/media/test_a.png new file mode 100644 index 0000000000000000000000000000000000000000..b0706daac5fe77d8ed98cd8f76aea2cc7c4ccbd1 GIT binary patch literal 778 zcmV+l1NHogP)-4hrs2Fr=LRs1D*B6kh(sba^J1|W zN=oVC0kljJgpj&bzOifIKWmzh|va*6st`nY)7JRw(6|yW-Iw1twR`Krv^bPyF-yhsR zGo0?b3>Fs`sT`i?5ekG*&X>;*U}Q^(jz+<8oCUyp(|dN_7HmgqI|W5is60UsP)KX% z2hg^xk6tE|!De>T;`z*d#?lLsNCf@GzM9752vE+K5ekIB^E?%oBnj6abzAbiH#(6_ zCM|J72(*lIFF3vn3TX|(bF1wVLI4R!&d`f73}b6lq9{U=B$&MYORCB4lgu=#Xny0t z&d!b_c_SkpkK^^HSMc3oESl{WxV2rkqn15e0Gw=BmSu#)VR$^Ay2s??Bz0{qd$w%B z_m4WB-I~Je>@1ZS4u`ST`vGmi_PTeROQ_gW_rWl~ORgvihDL`l@N~er0#3H8swz_3 zDOZaFfcKskOG`^s%*mdq3LJbsK$MMQe0-crT3=sxY$ChytBIGFmo3_uFoxihpgq9e z`#l`#$F`FO#(Eq2*3;4j{K!{qwLj;weGLESxb**mD}XD2pN)1Usm9hrfdBvi07*qo IM6N<$g0Y2ULI3~& literal 0 HcmV?d00001 diff --git a/media/test_b.png b/media/test_b.png new file mode 100644 index 0000000000000000000000000000000000000000..3798fe746ca476ae27e057e1ea76fdd5b1866a41 GIT binary patch literal 769 zcmV+c1OEJpP)F|7{Gt(`ezMw7PT}(hr*}`HigN`Y7|?%bcn)=3hAIEB}EYgvF*W?An@Q}gCRA- zL(nm!NFufgGQz|LEl8n8A#|m5R7hNxwL`FAtGna6;l&T;^8NkszW2U2FCpJuyU039 z;(8X}006K=GWw@xS)31;PL`Bm#j+_aegMY?x>+6~OtVhikwFUo*rfDSY*h=onBLD>b0b1X^04FpzH;4S} zc00kZV^Q<1`4}dqSjZ~hi=rs8#jh0rAh-}f5QHTK3S~O8bXy^4+x|-N5)19|vFI-hGdr?l(x11c1Zg zKoA7Fsw+slc8bK5@N#i!L290j%#vaps1*b;DoW{rt^KmR zY$RuAVJ+NjDkkCz2nY9=E*`6_`@3KRU<2R}nY`d=Z%E2@00000NkvXXu0mjfLWFMK literal 0 HcmV?d00001 diff --git a/media/test_c.png b/media/test_c.png new file mode 100644 index 0000000000000000000000000000000000000000..86b5c834e12a15271d759fafbe414b84864e3d94 GIT binary patch literal 856 zcmV-e1E>6nP)g*F% zJEhVBtcJHWd`x?L`<6YLrV$E-0MKBRmt}gCOMaVyR z=-C5{ideZnO8l2!De(DdZEejS;L*f5AC{MF8SeM{3H0{iJW)@%T(RYR^7sdQKYTBH zfW?Zo`GG)yKyM$Vr%q#Yk`Gc!MuV63yO^Fa`&9 z76722wvLXD4wK*6*-2w{RYnlYk!j{`-?Xgf7RjEkt%^d$K5k#TMup&@rlw}ix7+P_ zJRTyy|AzYG53s8MTR|bVyl3Y^G7%>>HOZ1T#NpY08J}OE@ZI;e%*aW%lqL*=*-IC> z5crm+rY2K;Y;25RFi0pATKim1rxRJ0@p`>TlEld~ZI=5$T7XO-Qb|?SlmHo>0}fH7 zI%J?91h$3Z;KFEcGhyExG1tALlD?*V7TO_{3HYN=nq8%b(?!U3!3zqjY}{GK1r!&A|C^}#k} zl%kxA?*jlR<&0aKDN>1OL4~Re4aEaUC8D&%J{MFdG(OCMmQ!dBH&b({d2ay!H8C+! z5JVslAQ%kteEbOqPuTMUDe4S3olZtaN11;6inFF|mS^6vwVBG9ld}W6H9tR(&*wvu zBmmml+OS%!ba!{B#o25&Y&IJvj|Z=R43qs53Z+W6WjZS zzrbiT;_-Oc*w{#m@9*#DcGo#pre9~RSx^TgNupA3-I@3*Ljzh`*|MJI(d0+s@msjv zZY&l{8bmxEXKd~(+NRcwnnjrbg;Is#_({g*5)2IuW%%80H}RP_vhz^XA=Eat^6dQw zL{Ut$TPzm3E}tj4^rN%@0JSYP1VPBKx3{;mzVfs50IJ$L!r^d+-E20qnTVAhK&jTS zu&|JKe9-~cqf2yjbd(Pux%eH2!;xVR27_oCkChf6wHD(~cq-#&gn#dZ+GH*rKx!?< za_}jS2M5sUbZI`X*UO6T2uigkBd?&FP)^C^2+3dHXx{$Ky}3F-_OgbFVs7(q0;Je#^(eGg+h5PDoGNNNQ6Kjz@oxH%~2~2 zXD;TITOs!`Nv*|*ewxgy0D#J9qO$rRYLhwZS!a`M2UJ>p-aFW<%Kk6d2iOPr3+v9* U*kM)ZE&u=k07*qoM6N<$f<=B^VE_OC literal 0 HcmV?d00001 diff --git a/media/test_e.png b/media/test_e.png new file mode 100644 index 0000000000000000000000000000000000000000..2070b06b06c001c905a9928e1246f5a3f6eae9d8 GIT binary patch literal 393 zcmV;40e1e0P)E#K68mkkqk^&mhbrq}awP z>9i1o#iC2Ryg*`Zh9QsxQ_bQ0AM+oW8DlW|fke&99Nq!|fRe{FoxI^Y{wyBe(V(ac z)d60g9&yms3Iq5rI>^>;bpQb1oP$!Dd$?s;@E^vRzGieFO`hk$ahyLKdVK)C@8kUP z7JBzM)w8&k-J8+?Q50cLokwSBy=(-4CU<}kg0JZWAMekpo;P7pHr0oa zv1-CV7zglw!I2xe4d3@OXP)OF+V2W?G`j-; zz&S^fBx(Shb1YxiXqi^#yxkep3kZS$U8~{$0S(ZAQeS}(LJhz$C_X+l|0S+EKNKQ_U##okR#>U0~G~@(9prxe+v&Fzi=TDKJTZulq zK;t9j5ls7kpzF=&IeGG=X3ppH;qiEoB#GEc30awWtVI2k?#ZOHvlFM&iN#_`>WiX? z)oSJNp<~z|enYbpnjP>5Mmcn(jNaZ}P4J$c9=b&r%kwpqo7yNX+E1~tkFvt2(X*G~ zAFuPN^gcSBj{5rgq+KW!LZ74Kn{U6w@{nESQ*{U8@hCr!pJ8NlnA+M}P4KhtUBFg- zY|~2>js%cze#pq^FnxV}NRq_DpE;Bi?nf|M$<8utOyJJa1P!eT&Ye4_Qu6tHJlEL9 zQ?+kx8hj1@z$mxozo(+ODP_?0P~CyW-~{dMZ)=o19uJR}zmisP0JF)4*~=fhP+-G! z>h*eQZEa0{*VWbGbUHOp8U8&^U1joHm536F1c}TU+S=MwMmn92OBcVxw7Xj4P@k30 z${iEopd~p*uPv|?zOL~D(ACvNO_}3A-|Hs@qZLV#G)AH*Qu}0#<(0oQbJkN{=leaZarofuX5zICvZ3% zD*4>p9DRL#lovIqO9PlK230<{*=!s*aDe&XuY7UY!`)bva5O+@b(YZT zEDOQwjL&?+(T-o_JdQ1qNrJ=q9{m`#MIPO zQdU(}g~Q>{l;uby!tvwBkv^WFy0k^(Yr}>l90@Rf>mnVmzkti-QkB?rF*G!U$K&DV zZ$>ICjVX0D?TFysnNIt^rla#^+S}VTXL~;;Cnu34iNV1^rYAC}G(Wp(fB3%y;9i*y zE>E!-oM8U%E7qVcr2Xld)V&Sn3^Tw75+>dZ1Ks+AN$kXkF*JDd<`5Mw$ jS-RNqACm_JcAUUJrJ|!B-3!#?00000NkvXXu0mjf*my+Z literal 0 HcmV?d00001 diff --git a/media/test_h.png b/media/test_h.png new file mode 100644 index 0000000000000000000000000000000000000000..33cbb44c89a6265abb2d6653179b25c1ef964db6 GIT binary patch literal 313 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=hEVFfw?$IEF;DCi7g$`g`7>p&?U2 zSH6^i83?$|4)))VPY6A8@YLx8=MMQ^2nhO;TzX*N@5z#H-o72PNEkd_{an^L HB{Ts5e%F8& literal 0 HcmV?d00001 diff --git a/media/test_i.png b/media/test_i.png new file mode 100644 index 0000000000000000000000000000000000000000..cf4d90c8b90c9910995c9e8b42d054bc0cb7671d GIT binary patch literal 200 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJeoq(2kcie~pUZiF&l@x}WGd*& zmohK|0k`F>d$skAp>rlro!&UPb@n&Dh~j*s=DmAummTpHWGhWnzGdCe*w~o({@z}V zKg*V>wKbcspOka1VR>U9dyiGZ6~@JU5~T-LFuTbbykgiC?ZVD4Cu70BqDa~><;T>d tj@2A5FCJ&?<2v{x<1iPS4KG*=1B0!|3NfqdeL$x%c)I$ztaD0e0sswSM&JMd literal 0 HcmV?d00001 diff --git a/media/test_j.png b/media/test_j.png new file mode 100644 index 0000000000000000000000000000000000000000..81da54444ba9d9771108e7bff7a67f7fd19969ec GIT binary patch literal 453 zcmV;$0XqJPP)u9x4rWD?ZU$|c3)ZPsj0Y0DzWyl9fss zQc2SEIjv4u%&mro& zGfof$a2$v2^%%JRW#ZiffQjT|A}caZ9*-bA2^gCmRry3wg!_%g1T~31#{hBRngD(( v_xhl+NvGexRP#_uGU?W$=DG literal 0 HcmV?d00001 diff --git a/media/test_k.png b/media/test_k.png new file mode 100644 index 0000000000000000000000000000000000000000..5f80a823045ce992e41c6e610bfe98bb1dccaab4 GIT binary patch literal 612 zcmV-q0-ODbP)w^5oA&d-a{fg2)*ney?B_ELQgHc(2Lhz zcJrX2g&u@KKu{2bLLmNK$84H{VO^nmYFTXJmOt1_zqkBe@;;xyd%xt(?W{fhjVUHH zP54Xz0M6SLkB0+vjttueE=DQLhr*5%?wg1I1THUJK3$-5WZ?a^ueKB1{5(0yD2?xX z7~F_D!dW$jWt>4^lzP2xxlX6kj`zO%`4QK%w@@yZEp@jUNI!j(X`+r@k+jLigx7tfxfTCMt)@U#XN!wV4D zEVj3Dz9l>_!dQd~M8elDI4PkhiYE!*+b|ZP@F>0kLWm~`SB=Oit4j&A8X5=)09Zaw zqjtT$m4m=$5sOe~7icz{o+M1uw2UQ_Nfat2TZuWlgNfcZ(D<=u3tGQ=XW}hRrq$E% yq4UXc{KB~??#1tpe{5|9{%59sE|>|-1pWYwvI-0y4qI0M0000)daAGroLKXx_Wmwr$6Yui`vzPM@}vgH#^Ax3@a* zhp(?M&-HcrDz}T++8igaxyc&5Y6xWSu}ZkYxR_6(fPF%U?BeDxUrO4Zg|C-bm3HR9 zN;Us`^BkKd%P+TaRaDT^J7MgO+w&d+v04>5SU`njxgN@xNA&YNKE literal 0 HcmV?d00001 diff --git a/media/test_m.png b/media/test_m.png new file mode 100644 index 0000000000000000000000000000000000000000..b9b3385bd3b038ff5dec975900809aacfad2ba07 GIT binary patch literal 1044 zcmV+v1nc{WP)6vw|_!2&kaV1C$9q=qzp<|Y-3#bT%*{6TbqV&EVB1dEG{%*9iw6rR-7k(&d6TvL%srI@j3 zG>X5q6T(Lc_(?lKG#X_tkW41=wADm+#QB!mEyNNDgu`KGTo43QG;OqiMz;e&5SYmT z;C8!l``%sr(rhr}j}KLdqBt^N5Ckatm<}9vIwRX}*!uc99ye-~zoKghe0&v=NMvMg zFc`$K(@AuY5TNQh78Vx93J3;+h$RvzwOfS$=Xz~y^@a(8KwNV|z;DNGEH5wr=OUsg z;s>>2j$fRf;<;}I@pzn=cXV__A71nt^;ikF~WmDo0^^gd8e z7~~g=MZDt=pja#t{$UVJ0To?i(x1!azJ=mIBsm*!H;Scx9{DB+wC@E z*=!a!eSUatcI@x(6Kf8GXbPw@04$fwn6>f%fE)ffstf6K8lQdhHPY!cQT@aF4=_LN z!JORzS(Y(ugCLp${yL#wKv|Y?(dhyJT$pm=yX|jLtyYOStE;QT*r?uk@87f9P$(3L zIfEdY0$2bR3I+IhI{<*^INbF4kD_q(0SN8_fXF=b)l#eE}>06-6PcH*Csc zL8#G0CX*rjk|bft>u1g%W&r3P!(mtN3t$0QE|=lvc`6YAmb`wV3!|zx?22QOx{lFk z6bHv`SPTG%C&;o4fpG*zRo*|JV|RBKfj|I~Bq715-mr7_N$NoYfRe6bLOg%rWxme- zm2>O-+XT^rR?#&)Qu}YhMW+iNdAuV9l-eykJE}8d6KDU7&IJ7L1-t^{Q=oYq+tG6X O0000 + + + +

Outer Frame

+