From b1109f47f03b66b7dd1d178e0940a4af3096d50c Mon Sep 17 00:00:00 2001 From: Sean Lip Date: Mon, 14 Nov 2016 18:31:36 -0800 Subject: [PATCH] Add correct focus behavior for the modal. Update boundary sounds. --- accessible/app.component.js | 70 +- accessible/audio.service.js | 67 +- accessible/block-options-modal.component.js | 179 +++ accessible/clipboard.service.js | 307 +++-- accessible/field-segment.component.js | 271 ++-- accessible/keyboard-input.service.js | 53 + accessible/media/oops.mp3 | Bin 0 -> 6846 bytes accessible/media/oops.ogg | Bin 0 -> 6063 bytes accessible/media/oops.wav | Bin 0 -> 31292 bytes accessible/messages.js | 4 +- accessible/modal.service.js | 59 + accessible/notifications.service.js | 39 +- accessible/toolbox-tree.component.js | 277 ++-- accessible/toolbox.component.js | 204 +-- accessible/translate.pipe.js | 19 +- accessible/tree.service.js | 1299 ++++++++++--------- accessible/utils.service.js | 95 +- accessible/workspace-tree.component.js | 456 +++---- accessible/workspace.component.js | 141 +- demos/accessible/index.html | 3 + 20 files changed, 1919 insertions(+), 1624 deletions(-) create mode 100644 accessible/block-options-modal.component.js create mode 100644 accessible/keyboard-input.service.js create mode 100644 accessible/media/oops.mp3 create mode 100644 accessible/media/oops.ogg create mode 100644 accessible/media/oops.wav create mode 100644 accessible/modal.service.js diff --git a/accessible/app.component.js b/accessible/app.component.js index 7ea38278b..e4d8f2635 100644 --- a/accessible/app.component.js +++ b/accessible/app.component.js @@ -25,39 +25,45 @@ blocklyApp.workspace = new Blockly.Workspace(); -blocklyApp.AppView = ng.core - .Component({ - selector: 'blockly-app', - template: ` - +blocklyApp.AppView = ng.core.Component({ + selector: 'blockly-app', + template: ` + -
- - -
+ - - - - - `, - directives: [blocklyApp.ToolboxComponent, blocklyApp.WorkspaceComponent], - pipes: [blocklyApp.TranslatePipe], - // All services are declared here, so that all components in the - // application use the same instance of the service. - // https://www.sitepoint.com/angular-2-components-providers-classes-factories-values/ - providers: [ - blocklyApp.ClipboardService, blocklyApp.NotificationsService, - blocklyApp.TreeService, blocklyApp.UtilsService, - blocklyApp.AudioService] - }) - .Class({ - constructor: [blocklyApp.NotificationsService, function(_notificationsService) { +
+ + +
+ + + + + + `, + directives: [ + blocklyApp.ToolboxComponent, blocklyApp.WorkspaceComponent, + blocklyApp.BlockOptionsModalComponent], + pipes: [blocklyApp.TranslatePipe], + // All services are declared here, so that all components in the + // application use the same instance of the service. + // https://www.sitepoint.com/angular-2-components-providers-classes-factories-values/ + providers: [ + blocklyApp.ClipboardService, blocklyApp.NotificationsService, + blocklyApp.TreeService, blocklyApp.UtilsService, + blocklyApp.AudioService, blocklyApp.ModalService, + blocklyApp.KeyboardInputService] +}) +.Class({ + constructor: [ + blocklyApp.NotificationsService, function(_notificationsService) { this.notificationsService = _notificationsService; - }], - getStatusMessage: function() { - return this.notificationsService.getStatusMessage(); } - }); + ], + getStatusMessage: function() { + return this.notificationsService.getStatusMessage(); + } +}); diff --git a/accessible/audio.service.js b/accessible/audio.service.js index c358c083b..3bf51408e 100644 --- a/accessible/audio.service.js +++ b/accessible/audio.service.js @@ -22,36 +22,39 @@ * @author sll@google.com (Sean Lip) */ -blocklyApp.AudioService = ng.core - .Class({ - constructor: [function() { - // We do not play any audio unless a media path prefix is specified. - this.canPlayAudio = false; - if (ACCESSIBLE_GLOBALS.hasOwnProperty('mediaPathPrefix')) { - this.canPlayAudio = true; - var mediaPathPrefix = ACCESSIBLE_GLOBALS['mediaPathPrefix']; - this.AUDIO_PATHS_ = { - 'connect': mediaPathPrefix + 'click.mp3', - 'delete': mediaPathPrefix + 'delete.mp3' - }; - } - - // TODO(sll): Add ogg and mp3 fallbacks. - this.cachedAudioFiles_ = {}; - }], - play_: function(audioId) { - if (this.canPlayAudio) { - if (!this.cachedAudioFiles_.hasOwnProperty(audioId)) { - this.cachedAudioFiles_[audioId] = new Audio( - this.AUDIO_PATHS_[audioId]); - } - this.cachedAudioFiles_[audioId].play(); - } - }, - playConnectSound: function() { - this.play_('connect'); - }, - playDeleteSound: function() { - this.play_('delete'); +blocklyApp.AudioService = ng.core.Class({ + constructor: [function() { + // We do not play any audio unless a media path prefix is specified. + this.canPlayAudio = false; + if (ACCESSIBLE_GLOBALS.hasOwnProperty('mediaPathPrefix')) { + this.canPlayAudio = true; + var mediaPathPrefix = ACCESSIBLE_GLOBALS['mediaPathPrefix']; + this.AUDIO_PATHS_ = { + 'connect': mediaPathPrefix + 'click.mp3', + 'delete': mediaPathPrefix + 'delete.mp3', + 'oops': mediaPathPrefix + 'oops.mp3' + }; } - }); + + // TODO(sll): Add ogg and mp3 fallbacks. + this.cachedAudioFiles_ = {}; + }], + play_: function(audioId) { + if (this.canPlayAudio) { + if (!this.cachedAudioFiles_.hasOwnProperty(audioId)) { + this.cachedAudioFiles_[audioId] = new Audio( + this.AUDIO_PATHS_[audioId]); + } + this.cachedAudioFiles_[audioId].play(); + } + }, + playConnectSound: function() { + this.play_('connect'); + }, + playDeleteSound: function() { + this.play_('delete'); + }, + playOopsSound: function() { + this.play_('oops'); + } +}); diff --git a/accessible/block-options-modal.component.js b/accessible/block-options-modal.component.js new file mode 100644 index 000000000..ad765f983 --- /dev/null +++ b/accessible/block-options-modal.component.js @@ -0,0 +1,179 @@ +/** + * AccessibleBlockly + * + * Copyright 2016 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 Component that represents the block options modal. + * + * @author sll@google.com (Sean Lip) + */ + +blocklyApp.BlockOptionsModalComponent = ng.core.Component({ + selector: 'blockly-block-options-modal', + template: ` + + `, + pipes: [blocklyApp.TranslatePipe], + styles: [ + `.blocklyModalCurtain { + background-color: rgba(0,0,0,0.4); + height: 100%; + left: 0; + overflow: auto; + position: fixed; + top: 0; + width: 100%; + z-index: 1; + } + `, ` + .blocklyModal { + background-color: #fefefe; + border: 1px solid #888; + margin: 15% auto; + max-width: 600px; + padding: 20px; + width: 60%; + } + `, ` + .blocklyModalButtonContainer { + margin: 10px 0; + } + `, ` + .blocklyModal .activeButton { + border: 1px solid blue; + } + `] +}) +.Class({ + constructor: [ + blocklyApp.ModalService, blocklyApp.KeyboardInputService, + blocklyApp.AudioService, + function(modalService_, keyboardInputService_, audioService_) { + this.modalService = modalService_; + this.keyboardInputService = keyboardInputService_; + this.audioService = audioService_; + + this.modalIsVisible = false; + this.modalHeaderHtml = ''; + this.actionButtonsInfo = []; + this.activeActionButtonIndex = 0; + this.onHideCallback = null; + + var that = this; + this.modalService.registerPreShowHook( + function(newModalHeaderHtml, newActionButtonsInfo, onHideCallback) { + that.modalIsVisible = true; + that.modalHeaderHtml = newModalHeaderHtml; + that.actionButtonsInfo = newActionButtonsInfo; + that.activeActionButtonIndex = 0; + that.onHideCallback = onHideCallback; + that.keyboardInputService.setOverride({ + // Tab key: no-op. + '9': function(evt) { + evt.preventDefault(); + evt.stopPropagation(); + }, + // Enter key: selects an action, performs it, and closes the + // modal. + '13': function() { + if (that.activeActionButtonIndex < + that.actionButtonsInfo.length) { + that.actionButtonsInfo[that.activeActionButtonIndex].action(); + } + that.hideModal(); + }, + // Escape key: closes the modal. + '27': function() { + that.hideModal(); + }, + // Up key: navigates to the previous item in the list. + '38': function(evt) { + evt.preventDefault(); + if (that.activeActionButtonIndex == 0) { + that.audioService.playOopsSound(); + } else { + that.activeActionButtonIndex--; + } + that.focusOnOptionIfPossible(that.activeActionButtonIndex); + }, + // Down key: navigates to the next item in the list. + '40': function(evt) { + evt.preventDefault(); + if (that.activeActionButtonIndex == + that.actionButtonsInfo.length) { + that.audioService.playOopsSound(); + } else { + that.activeActionButtonIndex++; + } + that.focusOnOptionIfPossible(that.activeActionButtonIndex); + } + }); + + setTimeout(function() { + document.getElementById('modalId').focus(); + }, 150); + } + ); + } + ], + // Focuses on the button represented by the given index, if the button + // is not disabled. + focusOnOptionIfPossible: function(index) { + var button = document.getElementById(this.getOptionId(index)); + if (!button.disabled) { + button.focus(); + } + }, + // Returns the ID for the corresponding option button. + getOptionId: function(index) { + return 'modal-option-' + index; + }, + // Returns the ID for the "cancel" option button. + getCancelOptionId: function() { + return this.getOptionId(this.actionButtonsInfo.length); + }, + // Closes the modal. + hideModal: function() { + this.modalIsVisible = false; + this.keyboardInputService.clearOverride(); + this.modalService.hideModal(); + } +}); diff --git a/accessible/clipboard.service.js b/accessible/clipboard.service.js index 251feba45..0a51c44b7 100644 --- a/accessible/clipboard.service.js +++ b/accessible/clipboard.service.js @@ -22,158 +22,157 @@ * @author madeeha@google.com (Madeeha Ghori) */ -blocklyApp.ClipboardService = ng.core - .Class({ - constructor: [ - blocklyApp.NotificationsService, blocklyApp.UtilsService, - blocklyApp.AudioService, - function(_notificationsService, _utilsService, _audioService) { - this.clipboardBlockXml_ = null; - this.clipboardBlockPreviousConnection_ = null; - this.clipboardBlockNextConnection_ = null; - this.clipboardBlockOutputConnection_ = null; - this.markedConnection_ = null; - this.notificationsService = _notificationsService; - this.utilsService = _utilsService; - this.audioService = _audioService; - }], - areConnectionsCompatible_: function(blockConnection, connection) { - // Check that both connections exist, that it's the right kind of - // connection, and that the types match. - return Boolean( - connection && blockConnection && - Blockly.OPPOSITE_TYPE[blockConnection.type] == connection.type && - connection.checkType_(blockConnection)); - }, - isCompatibleWithClipboard: function(connection) { - var previousConnection = this.clipboardBlockPreviousConnection_; - var nextConnection = this.clipboardBlockNextConnection_; - var outputConnection = this.clipboardBlockOutputConnection_; - return Boolean( - this.areConnectionsCompatible_(connection, previousConnection) || - this.areConnectionsCompatible_(connection, nextConnection) || - this.areConnectionsCompatible_(connection, outputConnection)); - }, - getMarkedConnectionBlock: function() { - if (!this.markedConnection_) { - return null; - } else { - return this.markedConnection_.getSourceBlock(); - } - }, - isAnyConnectionMarked: function() { - return Boolean(this.markedConnection_); - }, - isMovableToMarkedConnection: function(block) { - // It should not be possible to move any ancestor of the block containing - // the marked spot to the marked spot. - if (!this.markedConnection_) { - return false; - } - - var markedSpotAncestorBlock = this.getMarkedConnectionBlock(); - while (markedSpotAncestorBlock) { - if (markedSpotAncestorBlock.id == block.id) { - return false; - } - markedSpotAncestorBlock = markedSpotAncestorBlock.getParent(); - } - - return this.canBeCopiedToMarkedConnection(block); - }, - canBeCopiedToMarkedConnection: function(block) { - if (!this.markedConnection_ || - !this.markedConnection_.getSourceBlock().workspace) { - return false; - } - - var potentialConnections = [ - block.outputConnection, - block.previousConnection, - block.nextConnection - ]; - - var that = this; - return potentialConnections.some(function(connection) { - return that.areConnectionsCompatible_( - connection, that.markedConnection_); - }); - }, - markConnection: function(connection) { - this.markedConnection_ = connection; - this.notificationsService.setStatusMessage(Blockly.Msg.MARKED_SPOT_MSG); - }, - cut: function(block) { - this.copy(block); - block.dispose(true); - }, - copy: function(block) { - this.clipboardBlockXml_ = Blockly.Xml.blockToDom(block); - Blockly.Xml.deleteNext(this.clipboardBlockXml_); - this.clipboardBlockPreviousConnection_ = block.previousConnection; - this.clipboardBlockNextConnection_ = block.nextConnection; - this.clipboardBlockOutputConnection_ = block.outputConnection; - }, - isClipboardEmpty: function() { - return !this.clipboardBlockXml_; - }, - pasteFromClipboard: function(inputConnection) { - var connection = inputConnection; - // If the connection is a 'previousConnection' and that connection is - // already joined to something, use the 'nextConnection' of the - // previous block instead in order to do an insertion. - if (inputConnection.type == Blockly.PREVIOUS_STATEMENT && - inputConnection.isConnected()) { - connection = inputConnection.targetConnection; - } - - var reconstitutedBlock = Blockly.Xml.domToBlock(blocklyApp.workspace, - this.clipboardBlockXml_); - switch (connection.type) { - case Blockly.NEXT_STATEMENT: - connection.connect(reconstitutedBlock.previousConnection); - break; - case Blockly.PREVIOUS_STATEMENT: - connection.connect(reconstitutedBlock.nextConnection); - break; - default: - connection.connect(reconstitutedBlock.outputConnection); - } - this.audioService.playConnectSound(); - this.notificationsService.setStatusMessage( - this.utilsService.getBlockDescription(reconstitutedBlock) + ' ' + - Blockly.Msg.PASTED_BLOCK_FROM_CLIPBOARD_MSG); - return reconstitutedBlock.id; - }, - pasteToMarkedConnection: function(block) { - var xml = Blockly.Xml.blockToDom(block); - var reconstitutedBlock = Blockly.Xml.domToBlock( - blocklyApp.workspace, xml); - - var potentialConnections = [ - reconstitutedBlock.outputConnection, - reconstitutedBlock.previousConnection, - reconstitutedBlock.nextConnection - ]; - - var connectionSuccessful = false; - for (var i = 0; i < potentialConnections.length; i++) { - if (this.areConnectionsCompatible_( - this.markedConnection_, potentialConnections[i])) { - this.markedConnection_.connect(potentialConnections[i]); - this.audioService.playConnectSound(); - connectionSuccessful = true; - break; - } - } - - if (!connectionSuccessful) { - console.error('ERROR: Could not connect block to marked spot.'); - return; - } - - this.markedConnection_ = null; - - return reconstitutedBlock.id; +blocklyApp.ClipboardService = ng.core.Class({ + constructor: [ + blocklyApp.NotificationsService, blocklyApp.UtilsService, + blocklyApp.AudioService, + function(_notificationsService, _utilsService, _audioService) { + this.clipboardBlockXml_ = null; + this.clipboardBlockPreviousConnection_ = null; + this.clipboardBlockNextConnection_ = null; + this.clipboardBlockOutputConnection_ = null; + this.markedConnection_ = null; + this.notificationsService = _notificationsService; + this.utilsService = _utilsService; + this.audioService = _audioService; + }], + areConnectionsCompatible_: function(blockConnection, connection) { + // Check that both connections exist, that it's the right kind of + // connection, and that the types match. + return Boolean( + connection && blockConnection && + Blockly.OPPOSITE_TYPE[blockConnection.type] == connection.type && + connection.checkType_(blockConnection)); + }, + isCompatibleWithClipboard: function(connection) { + var previousConnection = this.clipboardBlockPreviousConnection_; + var nextConnection = this.clipboardBlockNextConnection_; + var outputConnection = this.clipboardBlockOutputConnection_; + return Boolean( + this.areConnectionsCompatible_(connection, previousConnection) || + this.areConnectionsCompatible_(connection, nextConnection) || + this.areConnectionsCompatible_(connection, outputConnection)); + }, + getMarkedConnectionBlock: function() { + if (!this.markedConnection_) { + return null; + } else { + return this.markedConnection_.getSourceBlock(); } - }); + }, + isAnyConnectionMarked: function() { + return Boolean(this.markedConnection_); + }, + isMovableToMarkedConnection: function(block) { + // It should not be possible to move any ancestor of the block containing + // the marked spot to the marked spot. + if (!this.markedConnection_) { + return false; + } + + var markedSpotAncestorBlock = this.getMarkedConnectionBlock(); + while (markedSpotAncestorBlock) { + if (markedSpotAncestorBlock.id == block.id) { + return false; + } + markedSpotAncestorBlock = markedSpotAncestorBlock.getParent(); + } + + return this.canBeCopiedToMarkedConnection(block); + }, + canBeCopiedToMarkedConnection: function(block) { + if (!this.markedConnection_ || + !this.markedConnection_.getSourceBlock().workspace) { + return false; + } + + var potentialConnections = [ + block.outputConnection, + block.previousConnection, + block.nextConnection + ]; + + var that = this; + return potentialConnections.some(function(connection) { + return that.areConnectionsCompatible_( + connection, that.markedConnection_); + }); + }, + markConnection: function(connection) { + this.markedConnection_ = connection; + this.notificationsService.setStatusMessage(Blockly.Msg.MARKED_SPOT_MSG); + }, + cut: function(block) { + this.copy(block); + block.dispose(true); + }, + copy: function(block) { + this.clipboardBlockXml_ = Blockly.Xml.blockToDom(block); + Blockly.Xml.deleteNext(this.clipboardBlockXml_); + this.clipboardBlockPreviousConnection_ = block.previousConnection; + this.clipboardBlockNextConnection_ = block.nextConnection; + this.clipboardBlockOutputConnection_ = block.outputConnection; + }, + isClipboardEmpty: function() { + return !this.clipboardBlockXml_; + }, + pasteFromClipboard: function(inputConnection) { + var connection = inputConnection; + // If the connection is a 'previousConnection' and that connection is + // already joined to something, use the 'nextConnection' of the + // previous block instead in order to do an insertion. + if (inputConnection.type == Blockly.PREVIOUS_STATEMENT && + inputConnection.isConnected()) { + connection = inputConnection.targetConnection; + } + + var reconstitutedBlock = Blockly.Xml.domToBlock(blocklyApp.workspace, + this.clipboardBlockXml_); + switch (connection.type) { + case Blockly.NEXT_STATEMENT: + connection.connect(reconstitutedBlock.previousConnection); + break; + case Blockly.PREVIOUS_STATEMENT: + connection.connect(reconstitutedBlock.nextConnection); + break; + default: + connection.connect(reconstitutedBlock.outputConnection); + } + this.audioService.playConnectSound(); + this.notificationsService.setStatusMessage( + this.utilsService.getBlockDescription(reconstitutedBlock) + ' ' + + Blockly.Msg.PASTED_BLOCK_FROM_CLIPBOARD_MSG); + return reconstitutedBlock.id; + }, + pasteToMarkedConnection: function(block) { + var xml = Blockly.Xml.blockToDom(block); + var reconstitutedBlock = Blockly.Xml.domToBlock( + blocklyApp.workspace, xml); + + var potentialConnections = [ + reconstitutedBlock.outputConnection, + reconstitutedBlock.previousConnection, + reconstitutedBlock.nextConnection + ]; + + var connectionSuccessful = false; + for (var i = 0; i < potentialConnections.length; i++) { + if (this.areConnectionsCompatible_( + this.markedConnection_, potentialConnections[i])) { + this.markedConnection_.connect(potentialConnections[i]); + this.audioService.playConnectSound(); + connectionSuccessful = true; + break; + } + } + + if (!connectionSuccessful) { + console.error('ERROR: Could not connect block to marked spot.'); + return; + } + + this.markedConnection_ = null; + + return reconstitutedBlock.id; + } +}); diff --git a/accessible/field-segment.component.js b/accessible/field-segment.component.js index acb765363..1fcd3b0a8 100644 --- a/accessible/field-segment.component.js +++ b/accessible/field-segment.component.js @@ -24,144 +24,143 @@ * @author madeeha@google.com (Madeeha Ghori) */ -blocklyApp.FieldSegmentComponent = ng.core - .Component({ - selector: 'blockly-field-segment', - template: ` -