mirror of
https://github.com/google/blockly.git
synced 2026-01-10 18:37:09 +01:00
Add correct focus behavior for the modal. Update boundary sounds.
This commit is contained in:
@@ -25,39 +25,45 @@
|
||||
|
||||
blocklyApp.workspace = new Blockly.Workspace();
|
||||
|
||||
blocklyApp.AppView = ng.core
|
||||
.Component({
|
||||
selector: 'blockly-app',
|
||||
template: `
|
||||
<div *ngIf="getStatusMessage()" aria-hidden="true" class="blocklyAriaLiveStatus">
|
||||
<span aria-live="polite" role="status">{{getStatusMessage()}}</span>
|
||||
</div>
|
||||
blocklyApp.AppView = ng.core.Component({
|
||||
selector: 'blockly-app',
|
||||
template: `
|
||||
<div *ngIf="getStatusMessage()" aria-hidden="true" class="blocklyAriaLiveStatus">
|
||||
<span aria-live="polite" role="status">{{getStatusMessage()}}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<blockly-toolbox></blockly-toolbox>
|
||||
<blockly-workspace></blockly-workspace>
|
||||
</div>
|
||||
<blockly-block-options-modal></blockly-block-options-modal>
|
||||
|
||||
<label aria-hidden="true" hidden id="blockly-button">{{'BUTTON'|translate}}</label>
|
||||
<label aria-hidden="true" hidden id="blockly-more-options">{{'MORE_OPTIONS'|translate}}</label>
|
||||
<label aria-hidden="true" hidden id="blockly-toolbox-block">{{'TOOLBOX_BLOCK'|translate}}</label>
|
||||
<label aria-hidden="true" hidden id="blockly-workspace-block">{{'WORKSPACE_BLOCK'|translate}}</label>
|
||||
`,
|
||||
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) {
|
||||
<div>
|
||||
<blockly-toolbox></blockly-toolbox>
|
||||
<blockly-workspace></blockly-workspace>
|
||||
</div>
|
||||
|
||||
<label aria-hidden="true" hidden id="blockly-button">{{'BUTTON'|translate}}</label>
|
||||
<label aria-hidden="true" hidden id="blockly-more-options">{{'MORE_OPTIONS'|translate}}</label>
|
||||
<label aria-hidden="true" hidden id="blockly-toolbox-block">{{'TOOLBOX_BLOCK'|translate}}</label>
|
||||
<label aria-hidden="true" hidden id="blockly-workspace-block">{{'WORKSPACE_BLOCK'|translate}}</label>
|
||||
`,
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
179
accessible/block-options-modal.component.js
Normal file
179
accessible/block-options-modal.component.js
Normal file
@@ -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: `
|
||||
<div *ngIf="modalIsVisible" id="modalId" role="dialog" tabindex="-1">
|
||||
<div (click)="hideModal()" class="blocklyModalCurtain">
|
||||
<!-- The $event.stopPropagation() here prevents the modal from
|
||||
closing when its interior is clicked. -->
|
||||
<div class="blocklyModal" (click)="$event.stopPropagation()" role="document">
|
||||
<h3>{{modalHeaderHtml}}</h3>
|
||||
|
||||
<div class="blocklyModalButtonContainer"
|
||||
*ngFor="#buttonInfo of actionButtonsInfo; #i=index">
|
||||
<button [id]="getOptionId(i)" (click)="buttonInfo.action(); hideModal();"
|
||||
[disabled]="buttonInfo.isDisabled()"
|
||||
[ngClass]="{activeButton: activeActionButtonIndex == i}">
|
||||
{{buttonInfo.translationIdForText|translate}}
|
||||
</button>
|
||||
</div>
|
||||
<div class="blocklyModalButtonContainer">
|
||||
<button [id]="getCancelOptionId()" (click)="hideModal()"
|
||||
[ngClass]="{activeButton: activeActionButtonIndex == actionButtonsInfo.length}">
|
||||
{{'CANCEL'|translate}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
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();
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -24,144 +24,143 @@
|
||||
* @author madeeha@google.com (Madeeha Ghori)
|
||||
*/
|
||||
|
||||
blocklyApp.FieldSegmentComponent = ng.core
|
||||
.Component({
|
||||
selector: 'blockly-field-segment',
|
||||
template: `
|
||||
<template [ngIf]="!mainField">
|
||||
<label [id]="mainFieldId">{{getPrefixText()}}</label>
|
||||
blocklyApp.FieldSegmentComponent = ng.core.Component({
|
||||
selector: 'blockly-field-segment',
|
||||
template: `
|
||||
<template [ngIf]="!mainField">
|
||||
<label [id]="mainFieldId">{{getPrefixText()}}</label>
|
||||
</template>
|
||||
|
||||
<template [ngIf]="mainField">
|
||||
<template [ngIf]="isTextInput()">
|
||||
{{getPrefixText()}}
|
||||
<input [id]="mainFieldId" type="text" [disabled]="disabled"
|
||||
[ngModel]="mainField.getValue()" (ngModelChange)="mainField.setValue($event)"
|
||||
[attr.aria-label]="getFieldDescription() + (disabled ? 'Disabled text field' : 'Press Enter to edit text')"
|
||||
tabindex="-1">
|
||||
</template>
|
||||
|
||||
<template [ngIf]="mainField">
|
||||
<template [ngIf]="isTextInput()">
|
||||
{{getPrefixText()}}
|
||||
<input [id]="mainFieldId" type="text" [disabled]="disabled"
|
||||
[ngModel]="mainField.getValue()" (ngModelChange)="mainField.setValue($event)"
|
||||
[attr.aria-label]="getFieldDescription() + (disabled ? 'Disabled text field' : 'Press Enter to edit text')"
|
||||
tabindex="-1">
|
||||
</template>
|
||||
|
||||
<template [ngIf]="isNumberInput()">
|
||||
{{getPrefixText()}}
|
||||
<input [id]="mainFieldId" type="number" [disabled]="disabled"
|
||||
[ngModel]="mainField.getValue()" (ngModelChange)="setNumberValue($event)"
|
||||
[attr.aria-label]="getFieldDescription() + (disabled ? 'Disabled number field' : 'Press Enter to edit number')"
|
||||
tabindex="-1">
|
||||
</template>
|
||||
|
||||
<template [ngIf]="isDropdown()">
|
||||
<label [id]="mainFieldId" [attr.aria-label]="getFieldDescription() + ' Move right to view submenu'">
|
||||
{{getFieldDescription()}}
|
||||
</label>
|
||||
<ol role="group">
|
||||
<li [id]="idMap[optionValue]" role="treeitem" *ngFor="#optionValue of getOptions()"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap[optionValue + 'Button'], 'blockly-button')"
|
||||
[attr.aria-level]="level" [attr.aria-selected]="mainField.getValue() == optionValue"
|
||||
class="blocklyDropdownListItem">
|
||||
<button [id]="idMap[optionValue + 'Button']" (click)="handleDropdownChange(mainField, optionValue)"
|
||||
[disabled]="disabled" tabindex="-1"
|
||||
[attr.aria-label]="optionText[optionValue] + ' Press Enter to select this value'">
|
||||
{{optionText[optionValue]}}
|
||||
</button>
|
||||
</li>
|
||||
</ol>
|
||||
</template>
|
||||
<template [ngIf]="isNumberInput()">
|
||||
{{getPrefixText()}}
|
||||
<input [id]="mainFieldId" type="number" [disabled]="disabled"
|
||||
[ngModel]="mainField.getValue()" (ngModelChange)="setNumberValue($event)"
|
||||
[attr.aria-label]="getFieldDescription() + (disabled ? 'Disabled number field' : 'Press Enter to edit number')"
|
||||
tabindex="-1">
|
||||
</template>
|
||||
`,
|
||||
inputs: ['prefixFields', 'mainField', 'mainFieldId', 'level'],
|
||||
pipes: [blocklyApp.TranslatePipe]
|
||||
})
|
||||
.Class({
|
||||
constructor: [
|
||||
blocklyApp.NotificationsService, blocklyApp.UtilsService,
|
||||
function(_notificationsService, _utilsService) {
|
||||
this.optionText = {
|
||||
keys: []
|
||||
};
|
||||
this.notificationsService = _notificationsService;
|
||||
this.utilsService = _utilsService;
|
||||
}],
|
||||
ngOnInit: function() {
|
||||
var elementsNeedingIds = this.generateElementNames(this.mainField);
|
||||
// Warning: this assumes that the elements returned by
|
||||
// this.generateElementNames() are unique.
|
||||
this.idMap = this.utilsService.generateIds(elementsNeedingIds);
|
||||
},
|
||||
getPrefixText: function() {
|
||||
var prefixTexts = this.prefixFields.map(function(prefixField) {
|
||||
return prefixField.getText();
|
||||
});
|
||||
return prefixTexts.join(' ');
|
||||
},
|
||||
getFieldDescription: function() {
|
||||
var description = this.mainField.getText();
|
||||
if (this.prefixFields.length > 0) {
|
||||
description = this.getPrefixText() + ': ' + description;
|
||||
}
|
||||
return description;
|
||||
},
|
||||
setNumberValue: function(newValue) {
|
||||
// Do not permit a residual value of NaN after a backspace event.
|
||||
this.mainField.setValue(newValue || 0);
|
||||
},
|
||||
generateAriaLabelledByAttr: function(mainLabel, secondLabel) {
|
||||
return mainLabel + ' ' + secondLabel;
|
||||
},
|
||||
generateElementNames: function() {
|
||||
var elementNames = [];
|
||||
if (this.isDropdown()) {
|
||||
var keys = this.getOptions();
|
||||
for (var i = 0; i < keys.length; i++){
|
||||
elementNames.push(keys[i], keys[i] + 'Button');
|
||||
}
|
||||
}
|
||||
return elementNames;
|
||||
},
|
||||
isNumberInput: function() {
|
||||
return this.mainField instanceof Blockly.FieldNumber;
|
||||
},
|
||||
isTextInput: function() {
|
||||
return this.mainField instanceof Blockly.FieldTextInput &&
|
||||
!(this.mainField instanceof Blockly.FieldNumber);
|
||||
},
|
||||
isDropdown: function() {
|
||||
return this.mainField instanceof Blockly.FieldDropdown;
|
||||
},
|
||||
isCheckbox: function() {
|
||||
return this.mainField instanceof Blockly.FieldCheckbox;
|
||||
},
|
||||
isTextField: function() {
|
||||
return !(this.mainField instanceof Blockly.FieldTextInput) &&
|
||||
!(this.mainField instanceof Blockly.FieldDropdown) &&
|
||||
!(this.mainField instanceof Blockly.FieldCheckbox);
|
||||
},
|
||||
hasVisibleText: function() {
|
||||
var text = this.mainField.getText().trim();
|
||||
return !!text;
|
||||
},
|
||||
getOptions: function() {
|
||||
if (this.optionText.keys.length) {
|
||||
return this.optionText.keys;
|
||||
}
|
||||
var options = this.mainField.getOptions_();
|
||||
for (var i = 0; i < options.length; i++) {
|
||||
var tuple = options[i];
|
||||
this.optionText[tuple[1]] = tuple[0];
|
||||
this.optionText.keys.push(tuple[1]);
|
||||
}
|
||||
return this.optionText.keys;
|
||||
},
|
||||
handleDropdownChange: function(field, optionValue) {
|
||||
if (optionValue == 'NO_ACTION') {
|
||||
return;
|
||||
}
|
||||
if (this.mainField instanceof Blockly.FieldVariable) {
|
||||
Blockly.FieldVariable.dropdownChange.call(this.mainField, optionValue);
|
||||
} else {
|
||||
this.mainField.setValue(optionValue);
|
||||
}
|
||||
|
||||
this.notificationsService.setStatusMessage(
|
||||
'Selected option ' + this.optionText[optionValue]);
|
||||
<template [ngIf]="isDropdown()">
|
||||
<label [id]="mainFieldId" [attr.aria-label]="getFieldDescription() + ' Move right to view submenu'">
|
||||
{{getFieldDescription()}}
|
||||
</label>
|
||||
<ol role="group">
|
||||
<li [id]="idMap[optionValue]" role="treeitem" *ngFor="#optionValue of getOptions()"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap[optionValue + 'Button'], 'blockly-button')"
|
||||
[attr.aria-level]="level" [attr.aria-selected]="mainField.getValue() == optionValue"
|
||||
class="blocklyDropdownListItem">
|
||||
<button [id]="idMap[optionValue + 'Button']" (click)="handleDropdownChange(mainField, optionValue)"
|
||||
[disabled]="disabled" tabindex="-1"
|
||||
[attr.aria-label]="optionText[optionValue] + ' Press Enter to select this value'">
|
||||
{{optionText[optionValue]}}
|
||||
</button>
|
||||
</li>
|
||||
</ol>
|
||||
</template>
|
||||
</template>
|
||||
`,
|
||||
inputs: ['prefixFields', 'mainField', 'mainFieldId', 'level'],
|
||||
pipes: [blocklyApp.TranslatePipe]
|
||||
})
|
||||
.Class({
|
||||
constructor: [
|
||||
blocklyApp.NotificationsService, blocklyApp.UtilsService,
|
||||
function(_notificationsService, _utilsService) {
|
||||
this.optionText = {
|
||||
keys: []
|
||||
};
|
||||
this.notificationsService = _notificationsService;
|
||||
this.utilsService = _utilsService;
|
||||
}],
|
||||
ngOnInit: function() {
|
||||
var elementsNeedingIds = this.generateElementNames(this.mainField);
|
||||
// Warning: this assumes that the elements returned by
|
||||
// this.generateElementNames() are unique.
|
||||
this.idMap = this.utilsService.generateIds(elementsNeedingIds);
|
||||
},
|
||||
getPrefixText: function() {
|
||||
var prefixTexts = this.prefixFields.map(function(prefixField) {
|
||||
return prefixField.getText();
|
||||
});
|
||||
return prefixTexts.join(' ');
|
||||
},
|
||||
getFieldDescription: function() {
|
||||
var description = this.mainField.getText();
|
||||
if (this.prefixFields.length > 0) {
|
||||
description = this.getPrefixText() + ': ' + description;
|
||||
}
|
||||
});
|
||||
return description;
|
||||
},
|
||||
setNumberValue: function(newValue) {
|
||||
// Do not permit a residual value of NaN after a backspace event.
|
||||
this.mainField.setValue(newValue || 0);
|
||||
},
|
||||
generateAriaLabelledByAttr: function(mainLabel, secondLabel) {
|
||||
return mainLabel + ' ' + secondLabel;
|
||||
},
|
||||
generateElementNames: function() {
|
||||
var elementNames = [];
|
||||
if (this.isDropdown()) {
|
||||
var keys = this.getOptions();
|
||||
for (var i = 0; i < keys.length; i++){
|
||||
elementNames.push(keys[i], keys[i] + 'Button');
|
||||
}
|
||||
}
|
||||
return elementNames;
|
||||
},
|
||||
isNumberInput: function() {
|
||||
return this.mainField instanceof Blockly.FieldNumber;
|
||||
},
|
||||
isTextInput: function() {
|
||||
return this.mainField instanceof Blockly.FieldTextInput &&
|
||||
!(this.mainField instanceof Blockly.FieldNumber);
|
||||
},
|
||||
isDropdown: function() {
|
||||
return this.mainField instanceof Blockly.FieldDropdown;
|
||||
},
|
||||
isCheckbox: function() {
|
||||
return this.mainField instanceof Blockly.FieldCheckbox;
|
||||
},
|
||||
isTextField: function() {
|
||||
return !(this.mainField instanceof Blockly.FieldTextInput) &&
|
||||
!(this.mainField instanceof Blockly.FieldDropdown) &&
|
||||
!(this.mainField instanceof Blockly.FieldCheckbox);
|
||||
},
|
||||
hasVisibleText: function() {
|
||||
var text = this.mainField.getText().trim();
|
||||
return !!text;
|
||||
},
|
||||
getOptions: function() {
|
||||
if (this.optionText.keys.length) {
|
||||
return this.optionText.keys;
|
||||
}
|
||||
var options = this.mainField.getOptions_();
|
||||
for (var i = 0; i < options.length; i++) {
|
||||
var tuple = options[i];
|
||||
this.optionText[tuple[1]] = tuple[0];
|
||||
this.optionText.keys.push(tuple[1]);
|
||||
}
|
||||
return this.optionText.keys;
|
||||
},
|
||||
handleDropdownChange: function(field, optionValue) {
|
||||
if (optionValue == 'NO_ACTION') {
|
||||
return;
|
||||
}
|
||||
if (this.mainField instanceof Blockly.FieldVariable) {
|
||||
Blockly.FieldVariable.dropdownChange.call(this.mainField, optionValue);
|
||||
} else {
|
||||
this.mainField.setValue(optionValue);
|
||||
}
|
||||
|
||||
this.notificationsService.setStatusMessage(
|
||||
'Selected option ' + this.optionText[optionValue]);
|
||||
}
|
||||
});
|
||||
|
||||
53
accessible/keyboard-input.service.js
Normal file
53
accessible/keyboard-input.service.js
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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 Service that handles keyboard input.
|
||||
*
|
||||
* @author sll@google.com (Sean Lip)
|
||||
*/
|
||||
|
||||
blocklyApp.KeyboardInputService = ng.core.Class({
|
||||
constructor: [function() {
|
||||
// Default custom actions for global keystrokes. The keys are string
|
||||
// representations of the key codes.
|
||||
this.keysToActions = {};
|
||||
// Override for the default keysToActions mapping (e.g. in a modal
|
||||
// context).
|
||||
this.keysToActionsOverride = null;
|
||||
|
||||
// Attach a keydown handler to the entire window.
|
||||
var that = this;
|
||||
|
||||
document.addEventListener('keydown', function(evt) {
|
||||
var stringifiedKeycode = String(evt.keyCode);
|
||||
var actionsObject = that.keysToActionsOverride || that.keysToActions;
|
||||
|
||||
if (actionsObject.hasOwnProperty(stringifiedKeycode)) {
|
||||
that.keysToActionsOverride[stringifiedKeycode](evt);
|
||||
}
|
||||
});
|
||||
}],
|
||||
setOverride: function(newKeysToActions) {
|
||||
this.keysToActionsOverride = newKeysToActions;
|
||||
},
|
||||
clearOverride: function() {
|
||||
this.keysToActionsOverride = null;
|
||||
}
|
||||
});
|
||||
BIN
accessible/media/oops.mp3
Normal file
BIN
accessible/media/oops.mp3
Normal file
Binary file not shown.
BIN
accessible/media/oops.ogg
Normal file
BIN
accessible/media/oops.ogg
Normal file
Binary file not shown.
BIN
accessible/media/oops.wav
Normal file
BIN
accessible/media/oops.wav
Normal file
Binary file not shown.
@@ -27,7 +27,8 @@
|
||||
Blockly.Msg.TOOLBOX = 'Toolbox';
|
||||
Blockly.Msg.WORKSPACE = 'Workspace';
|
||||
Blockly.Msg.TOOLBOX_BLOCK = 'toolbox block. Move right to view submenu.';
|
||||
Blockly.Msg.WORKSPACE_BLOCK = 'workspace block. Move right to view submenu.';
|
||||
Blockly.Msg.WORKSPACE_BLOCK =
|
||||
'workspace block. Move right to edit. Press Enter for more options.';
|
||||
|
||||
Blockly.Msg.CLEAR_WORKSPACE = 'Erase Workspace';
|
||||
|
||||
@@ -52,6 +53,7 @@ Blockly.Msg.FOR = 'for';
|
||||
Blockly.Msg.VALUE = 'value';
|
||||
|
||||
Blockly.Msg.BLOCK_OPTIONS = 'Block options: ';
|
||||
Blockly.Msg.CANCEL = 'Cancel.';
|
||||
|
||||
Blockly.Msg.BLOCK_MOVED_TO_MARKED_SPOT_MSB = 'Block moved to marked spot: ';
|
||||
Blockly.Msg.COPIED_BLOCK_MSG = 'copied. ';
|
||||
|
||||
59
accessible/modal.service.js
Normal file
59
accessible/modal.service.js
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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 Service that stores the content for custom modals.
|
||||
* This is a singleton service.
|
||||
*
|
||||
* @author sll@google.com (Sean Lip)
|
||||
*/
|
||||
|
||||
blocklyApp.ModalService = ng.core.Class({
|
||||
constructor: [function() {
|
||||
this.modalHeaderHtml = '';
|
||||
this.actionButtonsInfo = [];
|
||||
this.preShowHookHtml = null;
|
||||
this.modalIsShown = false;
|
||||
this.onHideCallback = null;
|
||||
}],
|
||||
registerPreShowHook: function(preShowHook) {
|
||||
this.preShowHook = function() {
|
||||
preShowHook(this.modalHeaderHtml, this.actionButtonsInfo);
|
||||
};
|
||||
},
|
||||
isModalShown: function() {
|
||||
return this.modalIsShown;
|
||||
},
|
||||
showModal: function(modalHeaderHtml, actionButtonsInfo, onHideCallback) {
|
||||
this.modalHeaderHtml = modalHeaderHtml;
|
||||
this.actionButtonsInfo = actionButtonsInfo;
|
||||
this.onHideCallback = onHideCallback;
|
||||
|
||||
if (this.preShowHook) {
|
||||
this.preShowHook();
|
||||
}
|
||||
this.modalIsShown = true;
|
||||
},
|
||||
hideModal: function() {
|
||||
this.modalIsShown = false;
|
||||
if (this.onHideCallback) {
|
||||
this.onHideCallback();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -23,24 +23,23 @@
|
||||
* @author sll@google.com (Sean Lip)
|
||||
*/
|
||||
|
||||
blocklyApp.NotificationsService = ng.core
|
||||
.Class({
|
||||
constructor: [function() {
|
||||
this.statusMessage_ = '';
|
||||
}],
|
||||
getStatusMessage: function() {
|
||||
return this.statusMessage_;
|
||||
},
|
||||
setStatusMessage: function(newMessage) {
|
||||
// Introduce a temporary status message, so that if, e.g., two "copy"
|
||||
// operations are done in succession, both messages will be read.
|
||||
this.statusMessage_ = '';
|
||||
blocklyApp.NotificationsService = ng.core.Class({
|
||||
constructor: [function() {
|
||||
this.statusMessage_ = '';
|
||||
}],
|
||||
getStatusMessage: function() {
|
||||
return this.statusMessage_;
|
||||
},
|
||||
setStatusMessage: function(newMessage) {
|
||||
// Introduce a temporary status message, so that if, e.g., two "copy"
|
||||
// operations are done in succession, both messages will be read.
|
||||
this.statusMessage_ = '';
|
||||
|
||||
// We need a non-zero timeout here, otherwise NVDA does not read the
|
||||
// notification messages properly.
|
||||
var that = this;
|
||||
setTimeout(function() {
|
||||
that.statusMessage_ = newMessage;
|
||||
}, 20);
|
||||
}
|
||||
});
|
||||
// We need a non-zero timeout here, otherwise NVDA does not read the
|
||||
// notification messages properly.
|
||||
var that = this;
|
||||
setTimeout(function() {
|
||||
that.statusMessage_ = newMessage;
|
||||
}, 20);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -24,147 +24,146 @@
|
||||
* @author madeeha@google.com (Madeeha Ghori)
|
||||
*/
|
||||
|
||||
blocklyApp.ToolboxTreeComponent = ng.core
|
||||
.Component({
|
||||
selector: 'blockly-toolbox-tree',
|
||||
template: `
|
||||
<li [id]="idMap['toolboxBlockRoot']" role="treeitem"
|
||||
[ngClass]="{blocklyHasChildren: displayBlockMenu}"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['toolboxBlockSummary'], 'blockly-toolbox-block')"
|
||||
[attr.aria-level]="level">
|
||||
<label #toolboxBlockSummary [id]="idMap['toolboxBlockSummary']">{{getBlockDescription()}}</label>
|
||||
<ol role="group" *ngIf="displayBlockMenu">
|
||||
<li [id]="idMap['sendToSelected']" role="treeitem" *ngIf="!isWorkspaceEmpty()"
|
||||
[attr.aria-label]="getAriaLabelForCopyToMarkedSpotButton()"
|
||||
[attr.aria-level]="level + 1"
|
||||
[attr.aria-disabled]="!canBeCopiedToMarkedConnection()">
|
||||
<button [id]="idMap['sendToSelectedButton']" (click)="copyToMarkedSpot()"
|
||||
[disabled]="!canBeCopiedToMarkedConnection()" tabindex="-1">
|
||||
{{'COPY_TO_MARKED_SPOT'|translate}}
|
||||
</button>
|
||||
</li>
|
||||
<li [id]="idMap['workspaceCopy']" role="treeitem"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['workspaceCopyButton'], 'blockly-button')"
|
||||
[attr.aria-level]="level + 1">
|
||||
<button [id]="idMap['workspaceCopyButton']" (click)="copyToWorkspace()" tabindex="-1">
|
||||
{{'COPY_TO_WORKSPACE'|translate}}
|
||||
</button>
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
`,
|
||||
directives: [ng.core.forwardRef(function() {
|
||||
return blocklyApp.ToolboxTreeComponent;
|
||||
})],
|
||||
inputs: [
|
||||
'block', 'displayBlockMenu', 'level', 'tree', 'isFirstToolboxTree'],
|
||||
pipes: [blocklyApp.TranslatePipe]
|
||||
})
|
||||
.Class({
|
||||
constructor: [
|
||||
blocklyApp.ClipboardService, blocklyApp.NotificationsService,
|
||||
blocklyApp.TreeService, blocklyApp.UtilsService,
|
||||
function(
|
||||
_clipboardService, _notificationsService,
|
||||
_treeService, _utilsService) {
|
||||
this.clipboardService = _clipboardService;
|
||||
this.notificationsService = _notificationsService;
|
||||
this.treeService = _treeService;
|
||||
this.utilsService = _utilsService;
|
||||
}],
|
||||
ngOnInit: function() {
|
||||
var idKeys = ['toolboxBlockRoot', 'toolboxBlockSummary'];
|
||||
if (this.displayBlockMenu) {
|
||||
idKeys = idKeys.concat([
|
||||
'workspaceCopy', 'workspaceCopyButton', 'sendToSelected',
|
||||
'sendToSelectedButton', 'blockCopy', 'blockCopyButton']);
|
||||
}
|
||||
|
||||
this.idMap = {};
|
||||
for (var i = 0; i < idKeys.length; i++) {
|
||||
this.idMap[idKeys[i]] = this.block.id + idKeys[i];
|
||||
}
|
||||
},
|
||||
ngAfterViewInit: function() {
|
||||
// If this is the first tree in the category-less toolbox, set its active
|
||||
// descendant after the ids have been computed.
|
||||
// Note that a timeout is needed here in order to trigger Angular
|
||||
// change detection.
|
||||
if (this.isFirstToolboxTree) {
|
||||
var that = this;
|
||||
setTimeout(function() {
|
||||
that.treeService.setActiveDesc(
|
||||
that.idMap['toolboxBlockRoot'], 'blockly-toolbox-tree');
|
||||
});
|
||||
}
|
||||
},
|
||||
getAriaLabelForCopyToMarkedSpotButton: function() {
|
||||
// TODO(sll): Find a way to make this more like the other buttons.
|
||||
var ariaLabel = 'Attach to link button';
|
||||
if (!this.clipboardService.isAnyConnectionMarked()) {
|
||||
ariaLabel += ', unavailable. Add a link in the workspace first.';
|
||||
}
|
||||
return ariaLabel;
|
||||
},
|
||||
isWorkspaceEmpty: function() {
|
||||
return this.utilsService.isWorkspaceEmpty();
|
||||
},
|
||||
getBlockDescription: function() {
|
||||
return this.utilsService.getBlockDescription(this.block);
|
||||
},
|
||||
generateAriaLabelledByAttr: function(mainLabel, secondLabel) {
|
||||
return this.utilsService.generateAriaLabelledByAttr(
|
||||
mainLabel, secondLabel);
|
||||
},
|
||||
canBeCopiedToMarkedConnection: function() {
|
||||
return this.clipboardService.canBeCopiedToMarkedConnection(this.block);
|
||||
},
|
||||
copyToClipboard: function() {
|
||||
this.clipboardService.copy(this.block);
|
||||
this.notificationsService.setStatusMessage(
|
||||
this.getBlockDescription() + ' ' + Blockly.Msg.COPIED_BLOCK_MSG);
|
||||
},
|
||||
copyToWorkspace: function() {
|
||||
var blockDescription = this.getBlockDescription();
|
||||
var xml = Blockly.Xml.blockToDom(this.block);
|
||||
var newBlockId = Blockly.Xml.domToBlock(blocklyApp.workspace, xml).id;
|
||||
blocklyApp.ToolboxTreeComponent = ng.core.Component({
|
||||
selector: 'blockly-toolbox-tree',
|
||||
template: `
|
||||
<li [id]="idMap['toolboxBlockRoot']" role="treeitem"
|
||||
[ngClass]="{blocklyHasChildren: displayBlockMenu}"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['toolboxBlockSummary'], 'blockly-toolbox-block')"
|
||||
[attr.aria-level]="level">
|
||||
<label #toolboxBlockSummary [id]="idMap['toolboxBlockSummary']">{{getBlockDescription()}}</label>
|
||||
<ol role="group" *ngIf="displayBlockMenu">
|
||||
<li [id]="idMap['sendToSelected']" role="treeitem" *ngIf="!isWorkspaceEmpty()"
|
||||
[attr.aria-label]="getAriaLabelForCopyToMarkedSpotButton()"
|
||||
[attr.aria-level]="level + 1"
|
||||
[attr.aria-disabled]="!canBeCopiedToMarkedConnection()">
|
||||
<button [id]="idMap['sendToSelectedButton']" (click)="copyToMarkedSpot()"
|
||||
[disabled]="!canBeCopiedToMarkedConnection()" tabindex="-1">
|
||||
{{'COPY_TO_MARKED_SPOT'|translate}}
|
||||
</button>
|
||||
</li>
|
||||
<li [id]="idMap['workspaceCopy']" role="treeitem"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['workspaceCopyButton'], 'blockly-button')"
|
||||
[attr.aria-level]="level + 1">
|
||||
<button [id]="idMap['workspaceCopyButton']" (click)="copyToWorkspace()" tabindex="-1">
|
||||
{{'COPY_TO_WORKSPACE'|translate}}
|
||||
</button>
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
`,
|
||||
directives: [ng.core.forwardRef(function() {
|
||||
return blocklyApp.ToolboxTreeComponent;
|
||||
})],
|
||||
inputs: [
|
||||
'block', 'displayBlockMenu', 'level', 'tree', 'isFirstToolboxTree'],
|
||||
pipes: [blocklyApp.TranslatePipe]
|
||||
})
|
||||
.Class({
|
||||
constructor: [
|
||||
blocklyApp.ClipboardService, blocklyApp.NotificationsService,
|
||||
blocklyApp.TreeService, blocklyApp.UtilsService,
|
||||
function(
|
||||
_clipboardService, _notificationsService,
|
||||
_treeService, _utilsService) {
|
||||
this.clipboardService = _clipboardService;
|
||||
this.notificationsService = _notificationsService;
|
||||
this.treeService = _treeService;
|
||||
this.utilsService = _utilsService;
|
||||
}],
|
||||
ngOnInit: function() {
|
||||
var idKeys = ['toolboxBlockRoot', 'toolboxBlockSummary'];
|
||||
if (this.displayBlockMenu) {
|
||||
idKeys = idKeys.concat([
|
||||
'workspaceCopy', 'workspaceCopyButton', 'sendToSelected',
|
||||
'sendToSelectedButton', 'blockCopy', 'blockCopyButton']);
|
||||
}
|
||||
|
||||
this.idMap = {};
|
||||
for (var i = 0; i < idKeys.length; i++) {
|
||||
this.idMap[idKeys[i]] = this.block.id + idKeys[i];
|
||||
}
|
||||
},
|
||||
ngAfterViewInit: function() {
|
||||
// If this is the first tree in the category-less toolbox, set its active
|
||||
// descendant after the ids have been computed.
|
||||
// Note that a timeout is needed here in order to trigger Angular
|
||||
// change detection.
|
||||
if (this.isFirstToolboxTree) {
|
||||
var that = this;
|
||||
setTimeout(function() {
|
||||
that.treeService.focusOnBlock(newBlockId);
|
||||
that.notificationsService.setStatusMessage(
|
||||
blockDescription + ' added to workspace. ' +
|
||||
'Now on added block in workspace.');
|
||||
});
|
||||
},
|
||||
copyToMarkedSpot: function() {
|
||||
var blockDescription = this.getBlockDescription();
|
||||
// Clean up the active desc for the destination tree.
|
||||
var oldDestinationTreeId = this.treeService.getTreeIdForBlock(
|
||||
this.clipboardService.getMarkedConnectionBlock().id);
|
||||
this.treeService.clearActiveDesc(oldDestinationTreeId);
|
||||
|
||||
var newBlockId = this.clipboardService.pasteToMarkedConnection(
|
||||
this.block);
|
||||
|
||||
// Invoke a digest cycle, so that the DOM settles.
|
||||
var that = this;
|
||||
setTimeout(function() {
|
||||
that.treeService.focusOnBlock(newBlockId);
|
||||
|
||||
var newDestinationTreeId = that.treeService.getTreeIdForBlock(
|
||||
newBlockId);
|
||||
if (newDestinationTreeId != oldDestinationTreeId) {
|
||||
// It is possible for the tree ID for the pasted block to change
|
||||
// after the paste operation, e.g. when inserting a block between two
|
||||
// existing blocks that are joined together. In this case, we need to
|
||||
// also reset the active desc for the old destination tree.
|
||||
that.treeService.initActiveDesc(oldDestinationTreeId);
|
||||
}
|
||||
|
||||
that.notificationsService.setStatusMessage(
|
||||
blockDescription + ' connected. ' +
|
||||
'Now on copied block in workspace.');
|
||||
that.treeService.setActiveDesc(
|
||||
that.idMap['toolboxBlockRoot'], 'blockly-toolbox-tree');
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
getAriaLabelForCopyToMarkedSpotButton: function() {
|
||||
// TODO(sll): Find a way to make this more like the other buttons.
|
||||
var ariaLabel = 'Attach to link button';
|
||||
if (!this.clipboardService.isAnyConnectionMarked()) {
|
||||
ariaLabel += ', unavailable. Add a link in the workspace first.';
|
||||
}
|
||||
return ariaLabel;
|
||||
},
|
||||
isWorkspaceEmpty: function() {
|
||||
return this.utilsService.isWorkspaceEmpty();
|
||||
},
|
||||
getBlockDescription: function() {
|
||||
return this.utilsService.getBlockDescription(this.block);
|
||||
},
|
||||
generateAriaLabelledByAttr: function(mainLabel, secondLabel) {
|
||||
return this.utilsService.generateAriaLabelledByAttr(
|
||||
mainLabel, secondLabel);
|
||||
},
|
||||
canBeCopiedToMarkedConnection: function() {
|
||||
return this.clipboardService.canBeCopiedToMarkedConnection(this.block);
|
||||
},
|
||||
copyToClipboard: function() {
|
||||
this.clipboardService.copy(this.block);
|
||||
this.notificationsService.setStatusMessage(
|
||||
this.getBlockDescription() + ' ' + Blockly.Msg.COPIED_BLOCK_MSG);
|
||||
},
|
||||
copyToWorkspace: function() {
|
||||
var blockDescription = this.getBlockDescription();
|
||||
var xml = Blockly.Xml.blockToDom(this.block);
|
||||
var newBlockId = Blockly.Xml.domToBlock(blocklyApp.workspace, xml).id;
|
||||
|
||||
var that = this;
|
||||
setTimeout(function() {
|
||||
that.treeService.focusOnBlock(newBlockId);
|
||||
that.notificationsService.setStatusMessage(
|
||||
blockDescription + ' added to workspace. ' +
|
||||
'Now on added block in workspace.');
|
||||
});
|
||||
},
|
||||
copyToMarkedSpot: function() {
|
||||
var blockDescription = this.getBlockDescription();
|
||||
// Clean up the active desc for the destination tree.
|
||||
var oldDestinationTreeId = this.treeService.getTreeIdForBlock(
|
||||
this.clipboardService.getMarkedConnectionBlock().id);
|
||||
this.treeService.clearActiveDesc(oldDestinationTreeId);
|
||||
|
||||
var newBlockId = this.clipboardService.pasteToMarkedConnection(
|
||||
this.block);
|
||||
|
||||
// Invoke a digest cycle, so that the DOM settles.
|
||||
var that = this;
|
||||
setTimeout(function() {
|
||||
that.treeService.focusOnBlock(newBlockId);
|
||||
|
||||
var newDestinationTreeId = that.treeService.getTreeIdForBlock(
|
||||
newBlockId);
|
||||
if (newDestinationTreeId != oldDestinationTreeId) {
|
||||
// It is possible for the tree ID for the pasted block to change
|
||||
// after the paste operation, e.g. when inserting a block between two
|
||||
// existing blocks that are joined together. In this case, we need to
|
||||
// also reset the active desc for the old destination tree.
|
||||
that.treeService.initActiveDesc(oldDestinationTreeId);
|
||||
}
|
||||
|
||||
that.notificationsService.setStatusMessage(
|
||||
blockDescription + ' connected. ' +
|
||||
'Now on copied block in workspace.');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,112 +20,118 @@
|
||||
/**
|
||||
* @fileoverview Angular2 Component that details how a toolbox is rendered
|
||||
* in AccessibleBlockly. Also handles any interactions with the toolbox.
|
||||
*
|
||||
* @author madeeha@google.com (Madeeha Ghori)
|
||||
*/
|
||||
|
||||
blocklyApp.ToolboxComponent = ng.core
|
||||
.Component({
|
||||
selector: 'blockly-toolbox',
|
||||
template: `
|
||||
<div class="blocklyToolboxColumn">
|
||||
<h3 #toolboxTitle id="blockly-toolbox-title">{{'TOOLBOX'|translate}}</h3>
|
||||
<ol #tree
|
||||
id="blockly-toolbox-tree" role="tree" class="blocklyTree"
|
||||
*ngIf="toolboxCategories && toolboxCategories.length > 0" tabindex="0"
|
||||
[attr.aria-labelledby]="toolboxTitle.id"
|
||||
[attr.aria-activedescendant]="getActiveDescId()"
|
||||
(keydown)="treeService.onKeypress($event, tree)">
|
||||
<template [ngIf]="xmlHasCategories">
|
||||
<li #parent
|
||||
[id]="idMap['Parent' + i]" role="treeitem"
|
||||
[ngClass]="{blocklyHasChildren: true, blocklyActiveDescendant: tree.getAttribute('aria-activedescendant') == idMap['Parent' + i]}"
|
||||
*ngFor="#category of toolboxCategories; #i=index"
|
||||
aria-level="0"
|
||||
[attr.aria-label]="getCategoryAriaLabel(category)">
|
||||
<div *ngIf="category && category.attributes">
|
||||
<label [id]="idMap['Label' + i]" #name>
|
||||
{{category.attributes.name.value}}
|
||||
</label>
|
||||
<ol role="group" *ngIf="getToolboxWorkspace(category).topBlocks_.length > 0">
|
||||
<blockly-toolbox-tree *ngFor="#block of getToolboxWorkspace(category).topBlocks_"
|
||||
[level]="1" [block]="block"
|
||||
[displayBlockMenu]="true"
|
||||
[tree]="tree">
|
||||
</blockly-toolbox-tree>
|
||||
</ol>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
blocklyApp.ToolboxComponent = ng.core.Component({
|
||||
selector: 'blockly-toolbox',
|
||||
template: `
|
||||
<div class="blocklyToolboxColumn">
|
||||
<h3 #toolboxTitle id="blockly-toolbox-title">{{'TOOLBOX'|translate}}</h3>
|
||||
<ol #tree
|
||||
id="blockly-toolbox-tree" role="tree" class="blocklyTree"
|
||||
*ngIf="toolboxCategories && toolboxCategories.length > 0"
|
||||
tabindex="0"
|
||||
[attr.aria-labelledby]="toolboxTitle.id"
|
||||
[attr.aria-activedescendant]="getActiveDescId()"
|
||||
(keydown)="treeService.onKeypress($event, tree)">
|
||||
<template [ngIf]="xmlHasCategories">
|
||||
<li #parent
|
||||
[id]="idMap['Parent' + i]" role="treeitem"
|
||||
[ngClass]="{blocklyHasChildren: true, blocklyActiveDescendant: tree.getAttribute('aria-activedescendant') == idMap['Parent' + i]}"
|
||||
*ngFor="#category of toolboxCategories; #i=index"
|
||||
aria-level="0"
|
||||
[attr.aria-label]="getCategoryAriaLabel(category)">
|
||||
<div *ngIf="category && category.attributes">
|
||||
<label [id]="idMap['Label' + i]" #name>
|
||||
{{category.attributes.name.value}}
|
||||
</label>
|
||||
<ol role="group" *ngIf="getToolboxWorkspace(category).topBlocks_.length > 0">
|
||||
<blockly-toolbox-tree *ngFor="#block of getToolboxWorkspace(category).topBlocks_"
|
||||
[level]="1" [block]="block"
|
||||
[displayBlockMenu]="true"
|
||||
[tree]="tree">
|
||||
</blockly-toolbox-tree>
|
||||
</ol>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<div *ngIf="!xmlHasCategories">
|
||||
<blockly-toolbox-tree *ngFor="#block of getToolboxWorkspace(toolboxCategories[0]).topBlocks_; #i=index"
|
||||
role="treeitem" [level]="0" [block]="block"
|
||||
[tree]="tree" [displayBlockMenu]="true"
|
||||
[isFirstToolboxTree]="i === 0">
|
||||
</blockly-toolbox-tree>
|
||||
</div>
|
||||
</ol>
|
||||
</div>
|
||||
`,
|
||||
directives: [blocklyApp.ToolboxTreeComponent],
|
||||
pipes: [blocklyApp.TranslatePipe]
|
||||
})
|
||||
.Class({
|
||||
constructor: [
|
||||
blocklyApp.TreeService, blocklyApp.UtilsService,
|
||||
function(_treeService, _utilsService) {
|
||||
<div *ngIf="!xmlHasCategories">
|
||||
<blockly-toolbox-tree *ngFor="#block of getToolboxWorkspace(toolboxCategories[0]).topBlocks_; #i=index"
|
||||
role="treeitem" [level]="0" [block]="block"
|
||||
[tree]="tree" [displayBlockMenu]="true"
|
||||
[isFirstToolboxTree]="i === 0">
|
||||
</blockly-toolbox-tree>
|
||||
</div>
|
||||
</ol>
|
||||
</div>
|
||||
`,
|
||||
directives: [blocklyApp.ToolboxTreeComponent],
|
||||
pipes: [blocklyApp.TranslatePipe]
|
||||
})
|
||||
.Class({
|
||||
constructor: [
|
||||
blocklyApp.TreeService, blocklyApp.UtilsService, blocklyApp.ModalService,
|
||||
function(_treeService, _utilsService, _modalService) {
|
||||
this.toolboxCategories = [];
|
||||
this.treeService = _treeService;
|
||||
this.utilsService = _utilsService;
|
||||
this.modalService = _modalService;
|
||||
|
||||
this.xmlHasCategories = false;
|
||||
}],
|
||||
ngOnInit: function() {
|
||||
// Note that sometimes the toolbox may not have categories; it may
|
||||
// display individual blocks directly (which is often the case in,
|
||||
// e.g., Blockly Games).
|
||||
var xmlToolboxElt = document.getElementById('blockly-toolbox-xml');
|
||||
var xmlCategoryElts = xmlToolboxElt.getElementsByTagName('category');
|
||||
if (xmlCategoryElts.length) {
|
||||
this.xmlHasCategories = true;
|
||||
this.toolboxCategories = Array.from(xmlCategoryElts);
|
||||
|
||||
var elementsNeedingIds = [];
|
||||
for (var i = 0; i < this.toolboxCategories.length; i++) {
|
||||
elementsNeedingIds.push('Parent' + i, 'Label' + i);
|
||||
}
|
||||
this.idMap = this.utilsService.generateIds(elementsNeedingIds);
|
||||
for (var i = 0; i < this.toolboxCategories.length; i++) {
|
||||
this.idMap['Parent' + i] = 'blockly-toolbox-tree-node' + i;
|
||||
}
|
||||
} else {
|
||||
// Create a single category with all the top-level blocks.
|
||||
this.xmlHasCategories = false;
|
||||
this.toolboxCategories = [Array.from(xmlToolboxElt.children)];
|
||||
}
|
||||
},
|
||||
ngAfterViewInit: function() {
|
||||
// If this is a top-level tree in the toolbox, set its active
|
||||
// descendant after the ids have been computed.
|
||||
// Note that a timeout is needed here in order to trigger Angular
|
||||
// change detection.
|
||||
if (this.xmlHasCategories) {
|
||||
var that = this;
|
||||
setTimeout(function() {
|
||||
that.treeService.setActiveDesc(
|
||||
'blockly-toolbox-tree-node0', 'blockly-toolbox-tree');
|
||||
});
|
||||
}
|
||||
},
|
||||
getActiveDescId: function() {
|
||||
return this.treeService.getActiveDescId('blockly-toolbox-tree');
|
||||
},
|
||||
getCategoryAriaLabel: function(category) {
|
||||
var numBlocks = this.getToolboxWorkspace(category).topBlocks_.length;
|
||||
return category.attributes.name.value + ' category. ' +
|
||||
'Move right to access ' + numBlocks + ' blocks in this category.';
|
||||
},
|
||||
getToolboxWorkspace: function(categoryNode) {
|
||||
return this.treeService.getToolboxWorkspace(categoryNode);
|
||||
}
|
||||
});
|
||||
],
|
||||
ngOnInit: function() {
|
||||
// Note that sometimes the toolbox may not have categories; it may
|
||||
// display individual blocks directly (which is often the case in,
|
||||
// e.g., Blockly Games).
|
||||
var xmlToolboxElt = document.getElementById('blockly-toolbox-xml');
|
||||
var xmlCategoryElts = xmlToolboxElt.getElementsByTagName('category');
|
||||
if (xmlCategoryElts.length) {
|
||||
this.xmlHasCategories = true;
|
||||
this.toolboxCategories = Array.from(xmlCategoryElts);
|
||||
|
||||
var elementsNeedingIds = [];
|
||||
for (var i = 0; i < this.toolboxCategories.length; i++) {
|
||||
elementsNeedingIds.push('Parent' + i, 'Label' + i);
|
||||
}
|
||||
this.idMap = this.utilsService.generateIds(elementsNeedingIds);
|
||||
for (var i = 0; i < this.toolboxCategories.length; i++) {
|
||||
this.idMap['Parent' + i] = 'blockly-toolbox-tree-node' + i;
|
||||
}
|
||||
} else {
|
||||
// Create a single category with all the top-level blocks.
|
||||
this.xmlHasCategories = false;
|
||||
this.toolboxCategories = [Array.from(xmlToolboxElt.children)];
|
||||
}
|
||||
},
|
||||
ngAfterViewInit: function() {
|
||||
// If this is a top-level tree in the toolbox, set its active
|
||||
// descendant after the ids have been computed.
|
||||
// Note that a timeout is needed here in order to trigger Angular
|
||||
// change detection.
|
||||
if (this.xmlHasCategories) {
|
||||
var that = this;
|
||||
setTimeout(function() {
|
||||
that.treeService.setActiveDesc(
|
||||
'blockly-toolbox-tree-node0', 'blockly-toolbox-tree');
|
||||
});
|
||||
}
|
||||
},
|
||||
isModalShown: function() {
|
||||
return this.modalService.isModalShown();
|
||||
},
|
||||
getActiveDescId: function() {
|
||||
return this.treeService.getActiveDescId('blockly-toolbox-tree');
|
||||
},
|
||||
getCategoryAriaLabel: function(category) {
|
||||
var numBlocks = this.getToolboxWorkspace(category).topBlocks_.length;
|
||||
return category.attributes.name.value + ' category. ' +
|
||||
'Move right to access ' + numBlocks + ' blocks in this category.';
|
||||
},
|
||||
getToolboxWorkspace: function(categoryNode) {
|
||||
return this.treeService.getToolboxWorkspace(categoryNode);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -22,13 +22,12 @@
|
||||
* @author sll@google.com (Sean Lip)
|
||||
*/
|
||||
|
||||
blocklyApp.TranslatePipe = ng.core
|
||||
.Pipe({
|
||||
name: 'translate'
|
||||
})
|
||||
.Class({
|
||||
constructor: function() {},
|
||||
transform: function(messageId) {
|
||||
return Blockly.Msg[messageId];
|
||||
}
|
||||
});
|
||||
blocklyApp.TranslatePipe = ng.core.Pipe({
|
||||
name: 'translate'
|
||||
})
|
||||
.Class({
|
||||
constructor: function() {},
|
||||
transform: function(messageId) {
|
||||
return Blockly.Msg[messageId];
|
||||
}
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -27,52 +27,51 @@
|
||||
|
||||
var blocklyApp = {};
|
||||
|
||||
blocklyApp.UtilsService = ng.core
|
||||
.Class({
|
||||
constructor: function() {},
|
||||
generateUniqueId: function() {
|
||||
return 'blockly-' + Blockly.genUid();
|
||||
},
|
||||
generateIds: function(elementsList) {
|
||||
var idMap = {};
|
||||
for (var i = 0; i < elementsList.length; i++){
|
||||
idMap[elementsList[i]] = this.generateUniqueId();
|
||||
}
|
||||
return idMap;
|
||||
},
|
||||
generateAriaLabelledByAttr: function(mainLabel, secondLabel) {
|
||||
return mainLabel + (secondLabel ? ' ' + secondLabel : '');
|
||||
},
|
||||
getInputTypeLabel: function(connection) {
|
||||
// Returns the input type name, or 'any' if any official input type
|
||||
// qualifies.
|
||||
if (connection.check_) {
|
||||
return connection.check_.join(', ');
|
||||
} else {
|
||||
return Blockly.Msg.ANY;
|
||||
}
|
||||
},
|
||||
getBlockTypeLabel: function(inputBlock) {
|
||||
if (inputBlock.type == Blockly.NEXT_STATEMENT) {
|
||||
return Blockly.Msg.BLOCK;
|
||||
} else {
|
||||
return Blockly.Msg.VALUE;
|
||||
}
|
||||
},
|
||||
getBlockDescription: function(block) {
|
||||
// We use 'BLANK' instead of the default '?' so that the string is read
|
||||
// out. (By default, screen readers tend to ignore punctuation.)
|
||||
return block.toString(undefined, 'BLANK');
|
||||
},
|
||||
isWorkspaceEmpty: function() {
|
||||
return !blocklyApp.workspace.topBlocks_.length;
|
||||
},
|
||||
getBlockById: function(blockId) {
|
||||
return this.getBlockByIdFromWorkspace(blockId, blocklyApp.workspace);
|
||||
},
|
||||
getBlockByIdFromWorkspace: function(blockId, workspace) {
|
||||
// This is used for non-default workspaces, such as those comprising the
|
||||
// toolbox.
|
||||
return workspace.getBlockById(blockId);
|
||||
blocklyApp.UtilsService = ng.core.Class({
|
||||
constructor: function() {},
|
||||
generateUniqueId: function() {
|
||||
return 'blockly-' + Blockly.genUid();
|
||||
},
|
||||
generateIds: function(elementsList) {
|
||||
var idMap = {};
|
||||
for (var i = 0; i < elementsList.length; i++){
|
||||
idMap[elementsList[i]] = this.generateUniqueId();
|
||||
}
|
||||
});
|
||||
return idMap;
|
||||
},
|
||||
generateAriaLabelledByAttr: function(mainLabel, secondLabel) {
|
||||
return mainLabel + (secondLabel ? ' ' + secondLabel : '');
|
||||
},
|
||||
getInputTypeLabel: function(connection) {
|
||||
// Returns the input type name, or 'any' if any official input type
|
||||
// qualifies.
|
||||
if (connection.check_) {
|
||||
return connection.check_.join(', ');
|
||||
} else {
|
||||
return Blockly.Msg.ANY;
|
||||
}
|
||||
},
|
||||
getBlockTypeLabel: function(inputBlock) {
|
||||
if (inputBlock.type == Blockly.NEXT_STATEMENT) {
|
||||
return Blockly.Msg.BLOCK;
|
||||
} else {
|
||||
return Blockly.Msg.VALUE;
|
||||
}
|
||||
},
|
||||
getBlockDescription: function(block) {
|
||||
// We use 'BLANK' instead of the default '?' so that the string is read
|
||||
// out. (By default, screen readers tend to ignore punctuation.)
|
||||
return block.toString(undefined, 'BLANK');
|
||||
},
|
||||
isWorkspaceEmpty: function() {
|
||||
return !blocklyApp.workspace.topBlocks_.length;
|
||||
},
|
||||
getBlockById: function(blockId) {
|
||||
return this.getBlockByIdFromWorkspace(blockId, blocklyApp.workspace);
|
||||
},
|
||||
getBlockByIdFromWorkspace: function(blockId, workspace) {
|
||||
// This is used for non-default workspaces, such as those comprising the
|
||||
// toolbox.
|
||||
return workspace.getBlockById(blockId);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -24,310 +24,186 @@
|
||||
* @author madeeha@google.com (Madeeha Ghori)
|
||||
*/
|
||||
|
||||
blocklyApp.WorkspaceTreeComponent = ng.core
|
||||
.Component({
|
||||
selector: 'blockly-workspace-tree',
|
||||
template: `
|
||||
<li [id]="idMap['blockRoot']" role="treeitem" class="blocklyHasChildren"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['blockSummary'], 'blockly-workspace-block')"
|
||||
[attr.aria-level]="level">
|
||||
<label [id]="idMap['blockSummary']">{{getBlockDescription()}}</label>
|
||||
blocklyApp.WorkspaceTreeComponent = ng.core.Component({
|
||||
selector: 'blockly-workspace-tree',
|
||||
template: `
|
||||
<li [id]="idMap['blockRoot']" role="treeitem" class="blocklyHasChildren"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['blockSummary'], 'blockly-workspace-block')"
|
||||
[attr.aria-level]="level">
|
||||
<label [id]="idMap['blockSummary']">{{getBlockDescription()}}</label>
|
||||
|
||||
<ol role="group">
|
||||
<template ngFor #blockInput [ngForOf]="block.inputList" #i="index">
|
||||
<li role="treeitem" [id]="idMap['listItem' + i]" [attr.aria-level]="level + 1" *ngIf="blockInput.fieldRow.length"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['fieldLabel' + i])">
|
||||
<blockly-field-segment *ngFor="#fieldSegment of inputListAsFieldSegments[i]"
|
||||
[prefixFields]="fieldSegment.prefixFields"
|
||||
[mainField]="fieldSegment.mainField"
|
||||
[mainFieldId]="idMap['fieldLabel' + i]"
|
||||
[level]="level + 2">
|
||||
</blockly-field-segment>
|
||||
</li>
|
||||
|
||||
<blockly-workspace-tree *ngIf="blockInput.connection && blockInput.connection.targetBlock()"
|
||||
[block]="blockInput.connection.targetBlock()" [level]="level + 1"
|
||||
[tree]="tree">
|
||||
</blockly-workspace-tree>
|
||||
<li #inputList [id]="idMap['inputList' + i]" role="treeitem"
|
||||
*ngIf="blockInput.connection && !blockInput.connection.targetBlock()"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['inputMenuLabel' + i], 'blockly-submenu-indicator')"
|
||||
[attr.aria-level]="level + 1">
|
||||
<label [id]="idMap['inputMenuLabel' + i]">
|
||||
{{utilsService.getInputTypeLabel(blockInput.connection)}} {{utilsService.getBlockTypeLabel(blockInput)}} needed:
|
||||
</label>
|
||||
<ol role="group">
|
||||
<li *ngFor="#fieldButtonInfo of fieldButtonsInfo"
|
||||
[id]="idMap[fieldButtonInfo.baseIdKey + i]" role="treeitem"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap[fieldButtonInfo.baseIdKey + 'Button' + i], 'blockly-button')"
|
||||
[attr.aria-level]="level + 2"
|
||||
[attr.aria-disabled]="fieldButtonInfo.isDisabled(blockInput.connection)">
|
||||
<button [id]="idMap[fieldButtonInfo.baseIdKey + 'Button' + i]"
|
||||
(click)="fieldButtonInfo.action(blockInput.connection)"
|
||||
[disabled]="fieldButtonInfo.isDisabled(blockInput.connection)" tabindex="-1">
|
||||
{{fieldButtonInfo.translationIdForText|translate}}
|
||||
</button>
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<li [id]="idMap['listItem']" class="blocklyHasChildren" role="treeitem"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr('blockly-more-options', 'blockly-submenu-indicator')"
|
||||
[attr.aria-level]="level + 1">
|
||||
<label [id]="idMap['label']">{{'BLOCK_OPTIONS'|translate}}</label>
|
||||
<ol role="group">
|
||||
<li *ngFor="#buttonInfo of actionButtonsInfo"
|
||||
[id]="idMap[buttonInfo.baseIdKey]" role="treeitem"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap[buttonInfo.baseIdKey + 'Button'], 'blockly-button')"
|
||||
[attr.aria-level]="level + 2"
|
||||
[attr.aria-disabled]="buttonInfo.isDisabled()">
|
||||
<button [id]="idMap[buttonInfo.baseIdKey + 'Button']" (click)="buttonInfo.action()"
|
||||
[disabled]="buttonInfo.isDisabled()" tabindex="-1">
|
||||
{{buttonInfo.translationIdForText|translate}}
|
||||
</button>
|
||||
</li>
|
||||
</ol>
|
||||
<ol role="group">
|
||||
<template ngFor #blockInput [ngForOf]="block.inputList" #i="index">
|
||||
<li role="treeitem" [id]="idMap['listItem' + i]" [attr.aria-level]="level + 1" *ngIf="blockInput.fieldRow.length"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['fieldLabel' + i])">
|
||||
<blockly-field-segment *ngFor="#fieldSegment of inputListAsFieldSegments[i]"
|
||||
[prefixFields]="fieldSegment.prefixFields"
|
||||
[mainField]="fieldSegment.mainField"
|
||||
[mainFieldId]="idMap['fieldLabel' + i]"
|
||||
[level]="level + 2">
|
||||
</blockly-field-segment>
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
|
||||
<blockly-workspace-tree *ngIf= "block.nextConnection && block.nextConnection.targetBlock()"
|
||||
[block]="block.nextConnection.targetBlock()"
|
||||
[level]="level" [tree]="tree">
|
||||
</blockly-workspace-tree>
|
||||
`,
|
||||
directives: [blocklyApp.FieldSegmentComponent, ng.core.forwardRef(function() {
|
||||
return blocklyApp.WorkspaceTreeComponent;
|
||||
})],
|
||||
inputs: ['block', 'level', 'tree', 'isTopLevel'],
|
||||
pipes: [blocklyApp.TranslatePipe]
|
||||
})
|
||||
.Class({
|
||||
constructor: [
|
||||
blocklyApp.ClipboardService, blocklyApp.NotificationsService,
|
||||
blocklyApp.TreeService, blocklyApp.UtilsService,
|
||||
blocklyApp.AudioService,
|
||||
function(
|
||||
_clipboardService, _notificationsService, _treeService,
|
||||
_utilsService, _audioService) {
|
||||
this.clipboardService = _clipboardService;
|
||||
this.notificationsService = _notificationsService;
|
||||
this.treeService = _treeService;
|
||||
this.utilsService = _utilsService;
|
||||
this.audioService = _audioService;
|
||||
}],
|
||||
ngOnInit: function() {
|
||||
var SUPPORTED_FIELDS = [
|
||||
Blockly.FieldTextInput, Blockly.FieldDropdown,
|
||||
Blockly.FieldCheckbox];
|
||||
this.inputListAsFieldSegments = this.block.inputList.map(function(input) {
|
||||
// Converts the input to a list of field segments. Each field segment
|
||||
// represents a user-editable field, prefixed by any number of
|
||||
// non-editable fields.
|
||||
var fieldSegments = [];
|
||||
<blockly-workspace-tree *ngIf="blockInput.connection && blockInput.connection.targetBlock()"
|
||||
[block]="blockInput.connection.targetBlock()" [level]="level + 1"
|
||||
[tree]="tree">
|
||||
</blockly-workspace-tree>
|
||||
<li #inputList [id]="idMap['inputList' + i]" role="treeitem"
|
||||
*ngIf="blockInput.connection && !blockInput.connection.targetBlock()"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['inputMenuLabel' + i], 'blockly-submenu-indicator')"
|
||||
[attr.aria-level]="level + 1">
|
||||
<label [id]="idMap['inputMenuLabel' + i]">
|
||||
{{utilsService.getInputTypeLabel(blockInput.connection)}} {{utilsService.getBlockTypeLabel(blockInput)}} needed:
|
||||
</label>
|
||||
<button [id]="idMap[fieldButtonsInfo[0].baseIdKey + 'Button' + i]"
|
||||
(click)="fieldButtonsInfo[0].action(blockInput.connection)"
|
||||
[disabled]="fieldButtonsInfo[0].isDisabled(blockInput.connection)" tabindex="-1">
|
||||
{{fieldButtonsInfo[0].translationIdForText|translate}}
|
||||
</button>
|
||||
</li>
|
||||
</template>
|
||||
</ol>
|
||||
</li>
|
||||
|
||||
var bufferedFields = [];
|
||||
input.fieldRow.forEach(function(field) {
|
||||
var fieldIsSupported = SUPPORTED_FIELDS.some(function(fieldType) {
|
||||
return (field instanceof fieldType);
|
||||
});
|
||||
<blockly-workspace-tree *ngIf= "block.nextConnection && block.nextConnection.targetBlock()"
|
||||
[block]="block.nextConnection.targetBlock()"
|
||||
[level]="level" [tree]="tree">
|
||||
</blockly-workspace-tree>
|
||||
`,
|
||||
directives: [blocklyApp.FieldSegmentComponent, ng.core.forwardRef(function() {
|
||||
return blocklyApp.WorkspaceTreeComponent;
|
||||
})],
|
||||
inputs: ['block', 'level', 'tree', 'isTopLevel'],
|
||||
pipes: [blocklyApp.TranslatePipe]
|
||||
})
|
||||
.Class({
|
||||
constructor: [
|
||||
blocklyApp.ClipboardService, blocklyApp.NotificationsService,
|
||||
blocklyApp.TreeService, blocklyApp.UtilsService,
|
||||
blocklyApp.AudioService, blocklyApp.ModalService,
|
||||
function(
|
||||
_clipboardService, _notificationsService, _treeService,
|
||||
_utilsService, _audioService, _modalService) {
|
||||
this.clipboardService = _clipboardService;
|
||||
this.notificationsService = _notificationsService;
|
||||
this.treeService = _treeService;
|
||||
this.utilsService = _utilsService;
|
||||
this.audioService = _audioService;
|
||||
this.modalService = _modalService;
|
||||
}],
|
||||
ngOnInit: function() {
|
||||
var SUPPORTED_FIELDS = [
|
||||
Blockly.FieldTextInput, Blockly.FieldDropdown,
|
||||
Blockly.FieldCheckbox];
|
||||
this.inputListAsFieldSegments = this.block.inputList.map(function(input) {
|
||||
// Converts the input to a list of field segments. Each field segment
|
||||
// represents a user-editable field, prefixed by any number of
|
||||
// non-editable fields.
|
||||
var fieldSegments = [];
|
||||
|
||||
if (fieldIsSupported) {
|
||||
var fieldSegment = {
|
||||
prefixFields: [],
|
||||
mainField: field
|
||||
};
|
||||
bufferedFields.forEach(function(bufferedField) {
|
||||
fieldSegment.prefixFields.push(bufferedField);
|
||||
});
|
||||
fieldSegments.push(fieldSegment);
|
||||
bufferedFields = [];
|
||||
} else {
|
||||
bufferedFields.push(field);
|
||||
}
|
||||
var bufferedFields = [];
|
||||
input.fieldRow.forEach(function(field) {
|
||||
var fieldIsSupported = SUPPORTED_FIELDS.some(function(fieldType) {
|
||||
return (field instanceof fieldType);
|
||||
});
|
||||
|
||||
// Handle leftover text at the end.
|
||||
if (bufferedFields.length) {
|
||||
fieldSegments.push({
|
||||
prefixFields: bufferedFields,
|
||||
mainField: null
|
||||
if (fieldIsSupported) {
|
||||
var fieldSegment = {
|
||||
prefixFields: [],
|
||||
mainField: field
|
||||
};
|
||||
bufferedFields.forEach(function(bufferedField) {
|
||||
fieldSegment.prefixFields.push(bufferedField);
|
||||
});
|
||||
}
|
||||
|
||||
return fieldSegments;
|
||||
});
|
||||
|
||||
// Generate a list of action buttons.
|
||||
var that = this;
|
||||
this.actionButtonsInfo = [{
|
||||
baseIdKey: 'markBefore',
|
||||
translationIdForText: 'MARK_SPOT_BEFORE',
|
||||
action: that.markSpotBefore_.bind(that),
|
||||
isDisabled: function() {
|
||||
return !that.block.previousConnection;
|
||||
}
|
||||
}, {
|
||||
baseIdKey: 'markAfter',
|
||||
translationIdForText: 'MARK_SPOT_AFTER',
|
||||
action: that.markSpotAfter_.bind(that),
|
||||
isDisabled: function() {
|
||||
return !that.block.nextConnection;
|
||||
}
|
||||
}, {
|
||||
baseIdKey: 'moveToMarkedSpot',
|
||||
translationIdForText: 'MOVE_TO_MARKED_SPOT',
|
||||
action: that.moveToMarkedSpot_.bind(that),
|
||||
isDisabled: function() {
|
||||
return !that.clipboardService.isMovableToMarkedConnection(
|
||||
that.block);
|
||||
}
|
||||
}, {
|
||||
baseIdKey: 'delete',
|
||||
translationIdForText: 'DELETE',
|
||||
action: that.deleteBlock_.bind(that),
|
||||
isDisabled: function() {
|
||||
return false;
|
||||
}
|
||||
}];
|
||||
|
||||
// Generate a list of action buttons.
|
||||
this.fieldButtonsInfo = [{
|
||||
baseIdKey: 'markSpot',
|
||||
translationIdForText: 'MARK_THIS_SPOT',
|
||||
action: function(connection) {
|
||||
that.clipboardService.markConnection(connection);
|
||||
},
|
||||
isDisabled: function() {
|
||||
return false;
|
||||
}
|
||||
}];
|
||||
|
||||
// Make a list of all the id keys.
|
||||
this.idKeys = ['blockRoot', 'blockSummary', 'listItem', 'label'];
|
||||
this.actionButtonsInfo.forEach(function(buttonInfo) {
|
||||
that.idKeys.push(buttonInfo.baseIdKey, buttonInfo.baseIdKey + 'Button');
|
||||
});
|
||||
this.fieldButtonsInfo.forEach(function(buttonInfo) {
|
||||
for (var i = 0; i < that.block.inputList.length; i++) {
|
||||
that.idKeys.push(
|
||||
buttonInfo.baseIdKey + i, buttonInfo.baseIdKey + 'Button' + i);
|
||||
}
|
||||
});
|
||||
for (var i = 0; i < this.block.inputList.length; i++) {
|
||||
var blockInput = this.block.inputList[i];
|
||||
that.idKeys.push(
|
||||
'inputList' + i, 'inputMenuLabel' + i, 'listItem' + i,
|
||||
'fieldLabel' + i);
|
||||
}
|
||||
},
|
||||
ngDoCheck: function() {
|
||||
// Generate a unique id for each id key. This needs to be done every time
|
||||
// changes happen, but after the first ng-init, in order to force the
|
||||
// element ids to change in cases where, e.g., a block is inserted in the
|
||||
// middle of a sequence of blocks.
|
||||
this.idMap = {};
|
||||
for (var i = 0; i < this.idKeys.length; i++) {
|
||||
this.idMap[this.idKeys[i]] = this.block.id + this.idKeys[i];
|
||||
}
|
||||
},
|
||||
ngAfterViewInit: function() {
|
||||
// If this is a top-level tree in the workspace, set its id and active
|
||||
// descendant. (Note that a timeout is needed here in order to trigger
|
||||
// Angular change detection.)
|
||||
var that = this;
|
||||
setTimeout(function() {
|
||||
if (that.tree && that.isTopLevel && !that.tree.id) {
|
||||
that.tree.id = that.utilsService.generateUniqueId();
|
||||
}
|
||||
if (that.tree && that.isTopLevel &&
|
||||
!that.treeService.getActiveDescId(that.tree.id)) {
|
||||
that.treeService.setActiveDesc(that.idMap['blockRoot'], that.tree.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
getBlockDescription: function() {
|
||||
var blockDescription = this.utilsService.getBlockDescription(this.block);
|
||||
|
||||
var parentBlock = this.block.getSurroundParent();
|
||||
if (parentBlock) {
|
||||
var fullDescription = blockDescription + ' inside ' +
|
||||
this.utilsService.getBlockDescription(parentBlock);
|
||||
return fullDescription;
|
||||
} else {
|
||||
return blockDescription;
|
||||
}
|
||||
},
|
||||
removeBlockAndSetFocus_: function(block, deleteBlockFunc) {
|
||||
this.treeService.removeBlockAndSetFocus(
|
||||
block, document.getElementById(this.idMap['blockRoot']),
|
||||
deleteBlockFunc);
|
||||
},
|
||||
deleteBlock_: function() {
|
||||
var blockDescription = this.getBlockDescription();
|
||||
|
||||
var that = this;
|
||||
this.removeBlockAndSetFocus_(this.block, function() {
|
||||
that.block.dispose(true);
|
||||
that.audioService.playDeleteSound();
|
||||
});
|
||||
|
||||
setTimeout(function() {
|
||||
if (that.utilsService.isWorkspaceEmpty()) {
|
||||
that.notificationsService.setStatusMessage(
|
||||
blockDescription + ' deleted. Workspace is empty.');
|
||||
fieldSegments.push(fieldSegment);
|
||||
bufferedFields = [];
|
||||
} else {
|
||||
that.notificationsService.setStatusMessage(
|
||||
blockDescription + ' deleted. Now on workspace.');
|
||||
bufferedFields.push(field);
|
||||
}
|
||||
});
|
||||
},
|
||||
moveToMarkedSpot_: function() {
|
||||
var blockDescription = this.getBlockDescription();
|
||||
var oldDestinationTreeId = this.treeService.getTreeIdForBlock(
|
||||
this.clipboardService.getMarkedConnectionBlock().id);
|
||||
this.treeService.clearActiveDesc(oldDestinationTreeId);
|
||||
|
||||
var newBlockId = this.clipboardService.pasteToMarkedConnection(
|
||||
this.block);
|
||||
// Handle leftover text at the end.
|
||||
if (bufferedFields.length) {
|
||||
fieldSegments.push({
|
||||
prefixFields: bufferedFields,
|
||||
mainField: null
|
||||
});
|
||||
}
|
||||
|
||||
var that = this;
|
||||
this.removeBlockAndSetFocus_(this.block, function() {
|
||||
that.block.dispose(true);
|
||||
});
|
||||
return fieldSegments;
|
||||
});
|
||||
|
||||
// Invoke a digest cycle, so that the DOM settles.
|
||||
setTimeout(function() {
|
||||
that.treeService.focusOnBlock(newBlockId);
|
||||
// Make a list of all the id keys.
|
||||
this.idKeys = ['blockRoot', 'blockSummary', 'listItem', 'label'];
|
||||
|
||||
var newDestinationTreeId = that.treeService.getTreeIdForBlock(
|
||||
newBlockId);
|
||||
if (newDestinationTreeId != oldDestinationTreeId) {
|
||||
// It is possible for the tree ID for the pasted block to change
|
||||
// after the paste operation, e.g. when inserting a block between two
|
||||
// existing blocks that are joined together. In this case, we need to
|
||||
// also reset the active desc for the old destination tree.
|
||||
that.treeService.initActiveDesc(oldDestinationTreeId);
|
||||
}
|
||||
// Generate a list of action buttons.
|
||||
this.fieldButtonsInfo = [{
|
||||
baseIdKey: 'markSpot',
|
||||
translationIdForText: 'MARK_THIS_SPOT',
|
||||
action: function(connection) {
|
||||
that.clipboardService.markConnection(connection);
|
||||
},
|
||||
isDisabled: function() {
|
||||
return false;
|
||||
}
|
||||
}];
|
||||
|
||||
that.notificationsService.setStatusMessage(
|
||||
blockDescription + ' ' +
|
||||
Blockly.Msg.PASTED_BLOCK_TO_MARKED_SPOT_MSG +
|
||||
'. Now on moved block in workspace.');
|
||||
});
|
||||
},
|
||||
markSpotBefore_: function() {
|
||||
this.clipboardService.markConnection(this.block.previousConnection);
|
||||
},
|
||||
markSpotAfter_: function() {
|
||||
this.clipboardService.markConnection(this.block.nextConnection);
|
||||
},
|
||||
generateAriaLabelledByAttr: function(mainLabel, secondLabel) {
|
||||
return this.utilsService.generateAriaLabelledByAttr(
|
||||
mainLabel, secondLabel);
|
||||
},
|
||||
isCompatibleWithClipboard: function(connection) {
|
||||
return this.clipboardService.isCompatibleWithClipboard(connection);
|
||||
var that = this;
|
||||
this.fieldButtonsInfo.forEach(function(buttonInfo) {
|
||||
for (var i = 0; i < that.block.inputList.length; i++) {
|
||||
that.idKeys.push(
|
||||
buttonInfo.baseIdKey + i, buttonInfo.baseIdKey + 'Button' + i);
|
||||
}
|
||||
});
|
||||
for (var i = 0; i < this.block.inputList.length; i++) {
|
||||
var blockInput = this.block.inputList[i];
|
||||
that.idKeys.push(
|
||||
'inputList' + i, 'inputMenuLabel' + i, 'listItem' + i,
|
||||
'fieldLabel' + i);
|
||||
}
|
||||
});
|
||||
},
|
||||
ngDoCheck: function() {
|
||||
// Generate a unique id for each id key. This needs to be done every time
|
||||
// changes happen, but after the first ng-init, in order to force the
|
||||
// element ids to change in cases where, e.g., a block is inserted in the
|
||||
// middle of a sequence of blocks.
|
||||
this.idMap = {};
|
||||
for (var i = 0; i < this.idKeys.length; i++) {
|
||||
this.idMap[this.idKeys[i]] = this.block.id + this.idKeys[i];
|
||||
}
|
||||
},
|
||||
ngAfterViewInit: function() {
|
||||
// If this is a top-level tree in the workspace, set its id and active
|
||||
// descendant. (Note that a timeout is needed here in order to trigger
|
||||
// Angular change detection.)
|
||||
var that = this;
|
||||
setTimeout(function() {
|
||||
if (that.tree && that.isTopLevel && !that.tree.id) {
|
||||
that.tree.id = that.utilsService.generateUniqueId();
|
||||
}
|
||||
if (that.tree && that.isTopLevel &&
|
||||
!that.treeService.getActiveDescId(that.tree.id)) {
|
||||
that.treeService.setActiveDesc(that.idMap['blockRoot'], that.tree.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
getBlockDescription: function() {
|
||||
var blockDescription = this.utilsService.getBlockDescription(this.block);
|
||||
|
||||
var parentBlock = this.block.getSurroundParent();
|
||||
if (parentBlock) {
|
||||
var fullDescription = blockDescription + ' inside ' +
|
||||
this.utilsService.getBlockDescription(parentBlock);
|
||||
return fullDescription;
|
||||
} else {
|
||||
return blockDescription;
|
||||
}
|
||||
},
|
||||
generateAriaLabelledByAttr: function(mainLabel, secondLabel) {
|
||||
return this.utilsService.generateAriaLabelledByAttr(
|
||||
mainLabel, secondLabel);
|
||||
},
|
||||
isCompatibleWithClipboard: function(connection) {
|
||||
return this.clipboardService.isCompatibleWithClipboard(connection);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -23,60 +23,60 @@
|
||||
* @author madeeha@google.com (Madeeha Ghori)
|
||||
*/
|
||||
|
||||
blocklyApp.WorkspaceComponent = ng.core
|
||||
.Component({
|
||||
selector: 'blockly-workspace',
|
||||
template: `
|
||||
<div class="blocklyWorkspaceColumn">
|
||||
<h3 #workspaceTitle id="blockly-workspace-title">{{'WORKSPACE'|translate}}</h3>
|
||||
blocklyApp.WorkspaceComponent = ng.core.Component({
|
||||
selector: 'blockly-workspace',
|
||||
template: `
|
||||
<div class="blocklyWorkspaceColumn">
|
||||
<h3 #workspaceTitle id="blockly-workspace-title">{{'WORKSPACE'|translate}}</h3>
|
||||
|
||||
<div *ngIf="workspace" class="blocklyWorkspace">
|
||||
<ol #tree *ngFor="#block of workspace.topBlocks_; #i = index"
|
||||
tabindex="0" role="tree" class="blocklyTree blocklyWorkspaceTree"
|
||||
[attr.aria-activedescendant]="getActiveDescId(tree.id)"
|
||||
[attr.aria-labelledby]="workspaceTitle.id"
|
||||
(keydown)="onKeypress($event, tree)">
|
||||
<blockly-workspace-tree [level]="0" [block]="block" [tree]="tree" [isTopLevel]="true">
|
||||
</blockly-workspace-tree>
|
||||
</ol>
|
||||
<div *ngIf="workspace" class="blocklyWorkspace">
|
||||
<ol #tree *ngFor="#block of workspace.topBlocks_; #i = index"
|
||||
tabindex="0" role="tree" class="blocklyTree blocklyWorkspaceTree"
|
||||
[attr.aria-activedescendant]="getActiveDescId(tree.id)"
|
||||
[attr.aria-labelledby]="workspaceTitle.id"
|
||||
(keydown)="onKeypress($event, tree)">
|
||||
<blockly-workspace-tree [level]="0" [block]="block" [tree]="tree" [isTopLevel]="true">
|
||||
</blockly-workspace-tree>
|
||||
</ol>
|
||||
|
||||
<span *ngIf="workspace.topBlocks_.length === 0">
|
||||
<i>Workspace is empty.</i>
|
||||
</span>
|
||||
</div>
|
||||
<span *ngIf="workspace.topBlocks_.length === 0">
|
||||
<i>Workspace is empty.</i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="blocklyToolbarColumn">
|
||||
<div id="blockly-workspace-toolbar" (keydown)="onWorkspaceToolbarKeypress($event)">
|
||||
<span *ngFor="#buttonConfig of toolbarButtonConfig">
|
||||
<button *ngIf="!buttonConfig.isHidden()"
|
||||
(click)="handleButtonClick(buttonConfig)"
|
||||
[attr.aria-describedby]="buttonConfig.ariaDescribedBy"
|
||||
class="blocklyTree blocklyWorkspaceToolbarButton">
|
||||
{{buttonConfig.text}}
|
||||
</button>
|
||||
</span>
|
||||
<button id="clear-workspace" (click)="clearWorkspace()"
|
||||
[attr.aria-disabled]="isWorkspaceEmpty()"
|
||||
<div class="blocklyToolbarColumn">
|
||||
<div id="blockly-workspace-toolbar" (keydown)="onWorkspaceToolbarKeypress($event)">
|
||||
<span *ngFor="#buttonConfig of toolbarButtonConfig">
|
||||
<button *ngIf="!buttonConfig.isHidden()"
|
||||
(click)="handleButtonClick(buttonConfig)"
|
||||
[attr.aria-describedby]="buttonConfig.ariaDescribedBy"
|
||||
class="blocklyTree blocklyWorkspaceToolbarButton">
|
||||
{{'CLEAR_WORKSPACE'|translate}}
|
||||
{{buttonConfig.text}}
|
||||
</button>
|
||||
</div>
|
||||
</span>
|
||||
<button id="clear-workspace" (click)="clearWorkspace()"
|
||||
[attr.aria-disabled]="isWorkspaceEmpty()"
|
||||
class="blocklyTree blocklyWorkspaceToolbarButton">
|
||||
{{'CLEAR_WORKSPACE'|translate}}
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
directives: [blocklyApp.WorkspaceTreeComponent],
|
||||
pipes: [blocklyApp.TranslatePipe]
|
||||
})
|
||||
.Class({
|
||||
constructor: [
|
||||
blocklyApp.NotificationsService, blocklyApp.TreeService,
|
||||
blocklyApp.UtilsService,
|
||||
function(_notificationsService, _treeService, _utilsService) {
|
||||
</div>
|
||||
`,
|
||||
directives: [blocklyApp.WorkspaceTreeComponent],
|
||||
pipes: [blocklyApp.TranslatePipe]
|
||||
})
|
||||
.Class({
|
||||
constructor: [
|
||||
blocklyApp.NotificationsService, blocklyApp.TreeService,
|
||||
blocklyApp.UtilsService, blocklyApp.ModalService,
|
||||
function(
|
||||
_notificationsService, _treeService, _utilsService, _modalService) {
|
||||
// ACCESSIBLE_GLOBALS is a global variable defined by the containing
|
||||
// page. It should contain a key, toolbarButtonConfig, whose
|
||||
// corresponding value is an Array with two keys: 'text' and 'action'.
|
||||
// The first is the text to display on the button, and the second is the
|
||||
// function that gets run when the button is clicked.
|
||||
// The first is the text to display on the button, and the second is
|
||||
// the function that gets run when the button is clicked.
|
||||
this.toolbarButtonConfig =
|
||||
ACCESSIBLE_GLOBALS && ACCESSIBLE_GLOBALS.toolbarButtonConfig ?
|
||||
ACCESSIBLE_GLOBALS.toolbarButtonConfig : [];
|
||||
@@ -84,28 +84,33 @@ blocklyApp.WorkspaceComponent = ng.core
|
||||
this.notificationsService = _notificationsService;
|
||||
this.treeService = _treeService;
|
||||
this.utilsService = _utilsService;
|
||||
}],
|
||||
clearWorkspace: function() {
|
||||
this.workspace.clear();
|
||||
},
|
||||
getActiveDescId: function(treeId) {
|
||||
return this.treeService.getActiveDescId(treeId);
|
||||
},
|
||||
handleButtonClick: function(buttonConfig) {
|
||||
buttonConfig.action();
|
||||
if (buttonConfig.onClickNotification) {
|
||||
this.notificationsService.setStatusMessage(
|
||||
buttonConfig.onClickNotification);
|
||||
}
|
||||
},
|
||||
onWorkspaceToolbarKeypress: function(e) {
|
||||
this.treeService.onWorkspaceToolbarKeypress(
|
||||
e, document.activeElement.id);
|
||||
},
|
||||
onKeypress: function(e, tree) {
|
||||
this.treeService.onKeypress(e, tree);
|
||||
},
|
||||
isWorkspaceEmpty: function() {
|
||||
return this.utilsService.isWorkspaceEmpty();
|
||||
this.modalService = _modalService;
|
||||
}
|
||||
});
|
||||
],
|
||||
isModalShown: function() {
|
||||
return this.modalService.isModalShown();
|
||||
},
|
||||
clearWorkspace: function() {
|
||||
this.workspace.clear();
|
||||
},
|
||||
getActiveDescId: function(treeId) {
|
||||
return this.treeService.getActiveDescId(treeId);
|
||||
},
|
||||
handleButtonClick: function(buttonConfig) {
|
||||
buttonConfig.action();
|
||||
if (buttonConfig.onClickNotification) {
|
||||
this.notificationsService.setStatusMessage(
|
||||
buttonConfig.onClickNotification);
|
||||
}
|
||||
},
|
||||
onWorkspaceToolbarKeypress: function(e) {
|
||||
this.treeService.onWorkspaceToolbarKeypress(
|
||||
e, document.activeElement.id);
|
||||
},
|
||||
onKeypress: function(e, tree) {
|
||||
this.treeService.onKeypress(e, tree);
|
||||
},
|
||||
isWorkspaceEmpty: function() {
|
||||
return this.utilsService.isWorkspaceEmpty();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,12 +20,15 @@
|
||||
|
||||
<script src="../../accessible/utils.service.js"></script>
|
||||
<script src="../../accessible/audio.service.js"></script>
|
||||
<script src="../../accessible/modal.service.js"></script>
|
||||
<script src="../../accessible/keyboard-input.service.js"></script>
|
||||
<script src="../../accessible/notifications.service.js"></script>
|
||||
<script src="../../accessible/clipboard.service.js"></script>
|
||||
<script src="../../accessible/tree.service.js"></script>
|
||||
<script src="../../accessible/translate.pipe.js"></script>
|
||||
|
||||
<script src="../../accessible/field-segment.component.js"></script>
|
||||
<script src="../../accessible/block-options-modal.component.js"></script>
|
||||
<script src="../../accessible/toolbox-tree.component.js"></script>
|
||||
<script src="../../accessible/toolbox.component.js"></script>
|
||||
<script src="../../accessible/workspace-tree.component.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user