diff --git a/accessible/app.component.js b/accessible/app.component.js index 737d39267..775f5411f 100644 --- a/accessible/app.component.js +++ b/accessible/app.component.js @@ -38,7 +38,8 @@ blocklyApp.AppComponent = ng.core.Component({ - + + {{'BUTTON'|translate}} @@ -51,7 +52,8 @@ blocklyApp.AppComponent = ng.core.Component({ blocklyApp.BlockOptionsModalComponent, blocklyApp.SidebarComponent, blocklyApp.ToolboxModalComponent, - blocklyApp.VariableModalComponent, + blocklyApp.VariableRenameModalComponent, + blocklyApp.VariableRemoveModalComponent, blocklyApp.WorkspaceComponent ], pipes: [blocklyApp.TranslatePipe], diff --git a/accessible/field-segment.component.js b/accessible/field-segment.component.js index 71a025d22..4d528286b 100644 --- a/accessible/field-segment.component.js +++ b/accessible/field-segment.component.js @@ -75,6 +75,12 @@ blocklyApp.FieldSegmentComponent = ng.core.Component({ this.dropdownOptions = []; this.rawOptions = []; }], + // Angular2 hook - called after initialization. + ngAfterContentInit: function() { + if (this.mainField) { + this.mainField.initModel(); + } + }, // Angular2 hook - called to check if the cached component needs an update. ngDoCheck: function() { if (this.isDropdown() && this.shouldBreakCache()) { @@ -144,7 +150,12 @@ blocklyApp.FieldSegmentComponent = ng.core.Component({ // Confirm a selection for dropdown fields. selectOption: function() { if (this.optionValue == Blockly.Msg.RENAME_VARIABLE) { - this.variableModalService.showModal_(this.mainField.getValue()); + this.variableModalService.showRenameModal_(this.mainField.getValue()); + } + + if (this.optionValue == + Blockly.Msg.DELETE_VARIABLE.replace('%1', this.mainField.getValue())) { + this.variableModalService.showRemoveModal_(this.mainField.getValue()); } }, // Sets the value on a dropdown input. @@ -154,7 +165,8 @@ blocklyApp.FieldSegmentComponent = ng.core.Component({ return; } - if (this.optionValue != Blockly.Msg.RENAME_VARIABLE) { + if (this.optionValue != Blockly.Msg.RENAME_VARIABLE && this.optionValue != + Blockly.Msg.DELETE_VARIABLE.replace('%1', this.mainField.getValue())) { this.mainField.setValue(this.optionValue); } diff --git a/accessible/variable-modal.service.js b/accessible/variable-modal.service.js index 86b45bf25..812deb96d 100644 --- a/accessible/variable-modal.service.js +++ b/accessible/variable-modal.service.js @@ -30,20 +30,35 @@ blocklyApp.VariableModalService = ng.core.Class({ } ], // Registers a hook to be called before the modal is shown. - registerPreShowHook: function(preShowHook) { - this.preShowHook = function(oldName) { + registerPreRenameShowHook: function(preShowHook) { + this.preRenameShowHook = function(oldName) { preShowHook(oldName); }; }, + registerPreRemoveShowHook: function(preShowHook) { + this.preRemoveShowHook = function(oldName, count) { + preShowHook(oldName, count); + }; + }, // Returns true if the variable modal is shown. isModalShown: function() { return this.modalIsShown; }, // Show the variable modal. - showModal_: function(oldName) { - this.preShowHook(oldName); + showRenameModal_: function(oldName) { + this.preRenameShowHook(oldName); this.modalIsShown = true; }, + // Show the variable modal. + showRemoveModal_: function(oldName) { + var count = blocklyApp.workspace.getVariableUses(oldName).length; + if (count > 1) { + this.preRemoveShowHook(oldName, count); + this.modalIsShown = true; + } else { + blocklyApp.workspace.deleteVariableInternal_(oldName); + } + }, // Hide the variable modal. hideModal: function() { this.modalIsShown = false; diff --git a/accessible/variable-remove-modal.component.js b/accessible/variable-remove-modal.component.js new file mode 100644 index 000000000..be5ad43f3 --- /dev/null +++ b/accessible/variable-remove-modal.component.js @@ -0,0 +1,164 @@ +/** + * AccessibleBlockly + * + * Copyright 2017 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Component representing the variable remove modal. + * + * @author corydiers@google.com (Cory Diers) + */ + +blocklyApp.VariableRemoveModalComponent = ng.core.Component({ + selector: 'blockly-remove-variable-modal', + template: ` + + + + + Remove {{count}} instances of + "{{currentVariableName}}" variable? + + + + YES + + + NO + + + + + `, + pipes: [blocklyApp.TranslatePipe] +}) +.Class({ + constructor: [ + blocklyApp.AudioService, blocklyApp.KeyboardInputService, blocklyApp.VariableModalService, + function(audioService, keyboardService, variableService) { + this.workspace = blocklyApp.workspace; + this.variableModalService = variableService; + this.audioService = audioService; + this.keyboardInputService = keyboardService + this.modalIsVisible = false; + this.activeButtonIndex = -1; + this.currentVariableName = ""; + this.count = 0; + + var that = this; + this.variableModalService.registerPreRemoveShowHook( + function(name, count) { + that.currentVariableName = name; + that.count = count + that.modalIsVisible = true; + + that.keyboardInputService.setOverride({ + // Tab key: navigates to the previous or next item in the list. + '9': function(evt) { + evt.preventDefault(); + evt.stopPropagation(); + + if (evt.shiftKey) { + // Move to the previous item in the list. + if (that.activeButtonIndex <= 0) { + that.activeActionButtonIndex = 0; + that.audioService.playOopsSound(); + } else { + that.activeButtonIndex--; + } + } else { + // Move to the next item in the list. + if (that.activeButtonIndex == that.numInteractiveElements() - 1) { + that.audioService.playOopsSound(); + } else { + that.activeButtonIndex++; + } + } + + that.focusOnOption(that.activeButtonIndex); + }, + // Escape key: closes the modal. + '27': function() { + that.dismissModal(); + }, + // Up key: no-op. + '38': function(evt) { + evt.preventDefault(); + }, + // Down key: no-op. + '40': function(evt) { + evt.preventDefault(); + } + }); + + setTimeout(function() { + document.getElementById('label').focus(); + }, 150); + } + ); + } + ], + // Caches the current text variable as the user types. + setTextValue: function(newValue) { + this.variableName = newValue; + }, + // Closes the modal (on both success and failure). + hideModal_: function() { + this.modalIsVisible = false; + this.keyboardInputService.clearOverride(); + }, + // Focuses on the button represented by the given index. + focusOnOption: function(index) { + var elements = this.getInteractiveElements(); + var button = elements[index]; + button.focus(); + }, + // Counts the number of interactive elements for the modal. + numInteractiveElements: function() { + var elements = this.getInteractiveElements(); + return elements.length; + }, + // Gets all the interactive elements for the modal. + getInteractiveElements: function() { + return Array.prototype.filter.call( + document.getElementById("varForm").elements, function(element) { + if (element.type === 'hidden') { + return false; + } + if (element.disabled) { + return false; + } + if (element.tabIndex < 0) { + return false; + } + return true; + }); + }, + // Submits the name change for the variable. + submit: function() { + blocklyApp.workspace.deleteVariableInternal_(this.currentVariableName); + this.hideModal_(); + }, + // Dismisses and closes the modal. + dismissModal: function() { + this.hideModal_(); + } +}) diff --git a/accessible/variable-modal.component.js b/accessible/variable-rename-modal.component.js similarity index 92% rename from accessible/variable-modal.component.js rename to accessible/variable-rename-modal.component.js index 43e882547..55d085108 100644 --- a/accessible/variable-modal.component.js +++ b/accessible/variable-rename-modal.component.js @@ -18,13 +18,13 @@ */ /** - * @fileoverview Component representing the variable modal. + * @fileoverview Component representing the variable rename modal. * * @author corydiers@google.com (Cory Diers) */ -blocklyApp.VariableModalComponent = ng.core.Component({ - selector: 'blockly-variable-modal', +blocklyApp.VariableRenameModalComponent = ng.core.Component({ + selector: 'blockly-rename-variable-modal', template: ` @@ -65,7 +65,7 @@ blocklyApp.VariableModalComponent = ng.core.Component({ this.currentVariableName = ""; var that = this; - this.variableModalService.registerPreShowHook( + this.variableModalService.registerPreRenameShowHook( function(oldName) { that.currentVariableName = oldName; that.modalIsVisible = true; diff --git a/core/field.js b/core/field.js index e815683b5..4114f1bae 100644 --- a/core/field.js +++ b/core/field.js @@ -159,6 +159,13 @@ Blockly.Field.prototype.init = function() { this.render_(); }; +/** + * Initializes the model of the field after it has been installed on a block. + * No-op by default. + */ +Blockly.Field.prototype.initModel = function() { +}; + /** * Dispose of all DOM objects belonging to this editable field. */ diff --git a/core/field_variable.js b/core/field_variable.js index be8a68033..553d2a554 100644 --- a/core/field_variable.js +++ b/core/field_variable.js @@ -73,6 +73,12 @@ Blockly.FieldVariable.prototype.init = function() { return; } Blockly.FieldVariable.superClass_.init.call(this); + + // TODO (1010): Change from init/initModel to initView/initModel + this.initModel(); +}; + +Blockly.FieldVariable.prototype.initModel = function() { if (!this.getValue()) { // Variables without names get uniquely named for this workspace. var workspace = diff --git a/core/workspace.js b/core/workspace.js index 95dc26c00..d5c355d3f 100644 --- a/core/workspace.js +++ b/core/workspace.js @@ -297,7 +297,8 @@ Blockly.Workspace.prototype.getVariableUses = function(name) { }; /** - * Delete a variables and all of its uses from this workspace. + * Delete a variables and all of its uses from this workspace. May prompt the + * user for confirmation. * @param {string} name Name of variable to delete. */ Blockly.Workspace.prototype.deleteVariable = function(name) { @@ -320,14 +321,6 @@ Blockly.Workspace.prototype.deleteVariable = function(name) { } var workspace = this; - function doDeletion() { - Blockly.Events.setGroup(true); - for (var i = 0; i < uses.length; i++) { - uses[i].dispose(true, false); - } - Blockly.Events.setGroup(false); - workspace.variableList.splice(variableIndex, 1); - } if (uses.length > 1) { // Confirm before deleting multiple blocks. Blockly.confirm( @@ -335,15 +328,31 @@ Blockly.Workspace.prototype.deleteVariable = function(name) { replace('%2', name), function(ok) { if (ok) { - doDeletion(); + workspace.deleteVariableInternal_(name); } }); } else { // No confirmation necessary for a single block. - doDeletion(); + this.deleteVariableInternal_(name); } }; +/** + * Deletes a variable and all of its uses from this workspace without asking the + * user for confirmation. + * @private + */ +Blockly.Workspace.prototype.deleteVariableInternal_ = function(name) { + var uses = this.getVariableUses(name); + var variableIndex = this.variableIndexOf(name); + Blockly.Events.setGroup(true); + for (var i = 0; i < uses.length; i++) { + uses[i].dispose(true, false); + } + Blockly.Events.setGroup(false); + this.variableList.splice(variableIndex, 1); +}; + /** * Check whether a variable exists with the given name. The check is * case-insensitive. diff --git a/demos/accessible/index.html b/demos/accessible/index.html index f7eef34d8..5a60b82a4 100644 --- a/demos/accessible/index.html +++ b/demos/accessible/index.html @@ -32,7 +32,8 @@ - + +
Remove {{count}} instances of + "{{currentVariableName}}" variable? +