diff --git a/accessible/app.component.js b/accessible/app.component.js index c69a54ae0..737d39267 100644 --- a/accessible/app.component.js +++ b/accessible/app.component.js @@ -38,6 +38,7 @@ blocklyApp.AppComponent = ng.core.Component({ + {{'BUTTON'|translate}} @@ -50,6 +51,7 @@ blocklyApp.AppComponent = ng.core.Component({ blocklyApp.BlockOptionsModalComponent, blocklyApp.SidebarComponent, blocklyApp.ToolboxModalComponent, + blocklyApp.VariableModalComponent, blocklyApp.WorkspaceComponent ], pipes: [blocklyApp.TranslatePipe], @@ -64,7 +66,8 @@ blocklyApp.AppComponent = ng.core.Component({ blocklyApp.NotificationsService, blocklyApp.ToolboxModalService, blocklyApp.TreeService, - blocklyApp.UtilsService + blocklyApp.UtilsService, + blocklyApp.VariableModalService ] }) .Class({ diff --git a/accessible/field-segment.component.js b/accessible/field-segment.component.js index d14e52303..290a2a3bd 100644 --- a/accessible/field-segment.component.js +++ b/accessible/field-segment.component.js @@ -52,6 +52,7 @@ blocklyApp.FieldSegmentComponent = ng.core.Component({ {{getPrefixText()}} @@ -66,14 +67,19 @@ blocklyApp.FieldSegmentComponent = ng.core.Component({ }) .Class({ constructor: [ - blocklyApp.NotificationsService, function(notificationsService) { + blocklyApp.NotificationsService, + blocklyApp.VariableModalService, + function(notificationsService, variableModalService) { this.notificationsService = notificationsService; + this.variableModalService = variableModalService; this.dropdownOptions = []; + this.rawOptions = []; }], ngDoCheck: function() { - if (this.isDropdown()) { - var rawOptions = this.mainField.getOptions_(); - this.dropdownOptions = rawOptions.map(function(valueAndText) { + if (this.isDropdown() && this.shouldBreakCache()) { + this.optionValue = this.mainField.getValue(); + this.rawOptions = this.mainField.getOptions(); + this.dropdownOptions = this.rawOptions.map(function(valueAndText) { return { text: valueAndText[0], value: valueAndText[1] @@ -81,6 +87,21 @@ blocklyApp.FieldSegmentComponent = ng.core.Component({ }); } }, + shouldBreakCache: function() { + var newOptions = this.mainField.getOptions(); + if (newOptions.length != this.rawOptions.length) { + return true; + } + + for (var i = 0; i < this.rawOptions.length; i++) { + // Compare the value of the cached options with the values in the field. + if (newOptions[i][1] != this.rawOptions[i][1]) { + return true; + } + } + + return false; + }, getPrefixText: function() { var prefixTexts = this.prefixFields.map(function(prefixField) { return prefixField.getText(); @@ -111,12 +132,21 @@ blocklyApp.FieldSegmentComponent = ng.core.Component({ // Do not permit a residual value of NaN after a backspace event. this.mainField.setValue(newValue || 0); }, + selectOption: function() { + if (this.optionValue == Blockly.Msg.RENAME_VARIABLE) { + this.variableModalService.showModal_(this.mainField.getValue()); + } + }, setDropdownValue: function(optionValue) { - if (optionValue == 'NO_ACTION') { + this.optionValue = optionValue + if (this.optionValue == 'NO_ACTION') { return; } - this.mainField.setValue(optionValue); + if (this.optionValue != Blockly.Msg.RENAME_VARIABLE) { + this.mainField.setValue(this.optionValue); + } + var optionText = undefined; for (var i = 0; i < this.dropdownOptions.length; i++) { if (this.dropdownOptions[i].value == optionValue) { @@ -128,7 +158,7 @@ blocklyApp.FieldSegmentComponent = ng.core.Component({ if (!optionText) { throw Error( 'There is no option text corresponding to the value: ' + - optionValue); + this.optionValue); } this.notificationsService.speak('Selected option ' + optionText); diff --git a/accessible/variable-modal.component.js b/accessible/variable-modal.component.js new file mode 100644 index 000000000..d6a08d226 --- /dev/null +++ b/accessible/variable-modal.component.js @@ -0,0 +1,163 @@ +/** + * 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 modal. + * + * @author corydiers@google.com (Cory Diers) + */ + +blocklyApp.VariableModalComponent = ng.core.Component({ + selector: 'blockly-variable-modal', + template: ` + + + + + New Variable Name: + + + + + SUBMIT + + + CANCEL + + + + + `, + 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 = ""; + + var that = this; + this.variableModalService.registerPreShowHook( + function(oldName) { + that.currentVariableName = oldName; + 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('mainFieldId').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() { + this.workspace.renameVariable(this.currentVariableName, this.variableName); + this.hideModal_(); + }, + // Dismisses and closes the modal. + dismissModal: function() { + this.hideModal_(); + } +}) diff --git a/accessible/variable-modal.service.js b/accessible/variable-modal.service.js new file mode 100644 index 000000000..97c067557 --- /dev/null +++ b/accessible/variable-modal.service.js @@ -0,0 +1,47 @@ +/** + * 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 Angular2 Service for the variable modal. + * + * @author corydiers@google.com (Cory Diers) + */ + +blocklyApp.VariableModalService = ng.core.Class({ + constructor: [ + function() { + this.modalIsShown = false; + } + ], + registerPreShowHook: function(preShowHook) { + this.preShowHook = function(oldName) { + preShowHook(oldName); + }; + }, + isModalShown: function() { + return this.modalIsShown; + }, + showModal_: function(oldName) { + this.preShowHook(oldName); + this.modalIsShown = true; + }, + hideModal: function() { + this.modalIsShown = false; + } +}); diff --git a/demos/accessible/index.html b/demos/accessible/index.html index 9c07986b6..f7eef34d8 100644 --- a/demos/accessible/index.html +++ b/demos/accessible/index.html @@ -27,10 +27,12 @@ + + diff --git a/tests/blocks/index.html b/tests/blocks/index.html index 254065ada..b528a85f5 100644 --- a/tests/blocks/index.html +++ b/tests/blocks/index.html @@ -3,7 +3,7 @@ Unit Tests for Blockly Blocks - +
New Variable Name: + +