diff --git a/accessible/app.component.js b/accessible/app.component.js index 775f5411f..216520498 100644 --- a/accessible/app.component.js +++ b/accessible/app.component.js @@ -38,6 +38,7 @@ blocklyApp.AppComponent = ng.core.Component({ + @@ -52,6 +53,7 @@ blocklyApp.AppComponent = ng.core.Component({ blocklyApp.BlockOptionsModalComponent, blocklyApp.SidebarComponent, blocklyApp.ToolboxModalComponent, + blocklyApp.VariableAddModalComponent, blocklyApp.VariableRenameModalComponent, blocklyApp.VariableRemoveModalComponent, blocklyApp.WorkspaceComponent diff --git a/accessible/field-segment.component.js b/accessible/field-segment.component.js index 4d528286b..d0020a7fa 100644 --- a/accessible/field-segment.component.js +++ b/accessible/field-segment.component.js @@ -55,7 +55,7 @@ blocklyApp.FieldSegmentComponent = ng.core.Component({ (keydown.enter)="selectOption()" tabindex="-1"> diff --git a/accessible/sidebar.component.js b/accessible/sidebar.component.js index de5ba6ebc..016fac979 100644 --- a/accessible/sidebar.component.js +++ b/accessible/sidebar.component.js @@ -52,6 +52,11 @@ blocklyApp.SidebarComponent = ng.core.Component({ class="blocklySidebarButton"> {{'ERASE_WORKSPACE'|translate}} + `, pipes: [blocklyApp.TranslatePipe] @@ -62,9 +67,10 @@ blocklyApp.SidebarComponent = ng.core.Component({ blocklyApp.ToolboxModalService, blocklyApp.TreeService, blocklyApp.UtilsService, + blocklyApp.VariableModalService, function( blockConnectionService, toolboxModalService, treeService, - utilsService) { + utilsService, variableService) { // ACCESSIBLE_GLOBALS is a global variable defined by the containing // page. It should contain a key, customSidebarButtons, describing // additional buttons that should be displayed after the default ones. @@ -77,6 +83,7 @@ blocklyApp.SidebarComponent = ng.core.Component({ this.toolboxModalService = toolboxModalService; this.treeService = treeService; this.utilsService = utilsService; + this.variableModalService = variableService; this.ID_FOR_ATTACH_TO_LINK_BUTTON = 'blocklyAttachToLinkBtn'; this.ID_FOR_CREATE_NEW_GROUP_BUTTON = 'blocklyCreateNewGroupBtn'; @@ -88,6 +95,9 @@ blocklyApp.SidebarComponent = ng.core.Component({ isWorkspaceEmpty: function() { return this.utilsService.isWorkspaceEmpty(); }, + hasVariableCategory: function() { + return this.toolboxModalService.toolboxHasVariableCategory(); + }, clearWorkspace: function() { blocklyApp.workspace.clear(); this.treeService.clearAllActiveDescs(); @@ -104,5 +114,8 @@ blocklyApp.SidebarComponent = ng.core.Component({ showToolboxModalForCreateNewGroup: function() { this.toolboxModalService.showToolboxModalForCreateNewGroup( this.ID_FOR_CREATE_NEW_GROUP_BUTTON); + }, + showAddVariableModal: function() { + this.variableModalService.showAddModal_("item"); } }); diff --git a/accessible/toolbox-modal.service.js b/accessible/toolbox-modal.service.js index 4103309f0..66211b505 100644 --- a/accessible/toolbox-modal.service.js +++ b/accessible/toolbox-modal.service.js @@ -42,6 +42,7 @@ blocklyApp.ToolboxModalService = ng.core.Class({ this.selectedToolboxCategories = null; this.onSelectBlockCallback = null; this.onDismissCallback = null; + this.hasVariableCategory = null; // The aim of the pre-show hook is to populate the modal component with // the information it needs to display the modal (e.g., which categories // and blocks to display). @@ -119,6 +120,26 @@ blocklyApp.ToolboxModalService = ng.core.Class({ isModalShown: function() { return this.modalIsShown; }, + toolboxHasVariableCategory: function() { + if (this.hasVariableCategory === null) { + var toolboxXmlElt = document.getElementById('blockly-toolbox-xml'); + var toolboxCategoryElts = toolboxXmlElt.getElementsByTagName('category'); + var that = this; + Array.from(toolboxCategoryElts).forEach( + function(categoryElt) { + var custom = categoryElt.attributes.custom; + if (custom && custom.value == Blockly.VARIABLE_CATEGORY_NAME) { + that.hasVariableCategory = true; + } + }); + + if (this.hasVariableCategory === null) { + this.hasVariableCategory = false; + } + } + + return this.hasVariableCategory; + }, showModal_: function( selectedToolboxCategories, onSelectBlockCallback, onDismissCallback) { this.selectedToolboxCategories = selectedToolboxCategories; diff --git a/accessible/variable-add-modal.component.js b/accessible/variable-add-modal.component.js new file mode 100644 index 000000000..1ea6eddfd --- /dev/null +++ b/accessible/variable-add-modal.component.js @@ -0,0 +1,162 @@ +/** + * 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 rename modal. + * + * @author corydiers@google.com (Cory Diers) + */ + +blocklyApp.VariableAddModalComponent = ng.core.Component({ + selector: 'blockly-add-variable-modal', + template: ` +
+ +
+
+

New Variable Name: + +

+
+ + +
+
+
+ `, + 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; + + var that = this; + this.variableModalService.registerPreAddShowHook( + function() { + 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.createVariable(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 index 812deb96d..720afde7f 100644 --- a/accessible/variable-modal.service.js +++ b/accessible/variable-modal.service.js @@ -29,12 +29,19 @@ blocklyApp.VariableModalService = ng.core.Class({ this.modalIsShown = false; } ], - // Registers a hook to be called before the modal is shown. + // Registers a hook to be called before the add modal is shown. + registerPreAddShowHook: function(preShowHook) { + this.preAddShowHook = function() { + preShowHook(); + }; + }, + // Registers a hook to be called before the rename modal is shown. registerPreRenameShowHook: function(preShowHook) { this.preRenameShowHook = function(oldName) { preShowHook(oldName); }; }, + // Registers a hook to be called before the remove modal is shown. registerPreRemoveShowHook: function(preShowHook) { this.preRemoveShowHook = function(oldName, count) { preShowHook(oldName, count); @@ -44,12 +51,17 @@ blocklyApp.VariableModalService = ng.core.Class({ isModalShown: function() { return this.modalIsShown; }, - // Show the variable modal. + // Show the add variable modal. + showAddModal_: function() { + this.preAddShowHook(); + this.modalIsShown = true; + }, + // Show the rename variable modal. showRenameModal_: function(oldName) { this.preRenameShowHook(oldName); this.modalIsShown = true; }, - // Show the variable modal. + // Show the remove variable modal. showRemoveModal_: function(oldName) { var count = blocklyApp.workspace.getVariableUses(oldName).length; if (count > 1) { diff --git a/demos/accessible/index.html b/demos/accessible/index.html index 5a60b82a4..2a187360e 100644 --- a/demos/accessible/index.html +++ b/demos/accessible/index.html @@ -32,6 +32,7 @@ + @@ -351,6 +352,7 @@ +