Add correct focus behavior for the modal. Update boundary sounds.

This commit is contained in:
Sean Lip
2016-11-14 18:31:36 -08:00
parent 14f0a1cd3d
commit b1109f47f0
20 changed files with 1919 additions and 1624 deletions

View File

@@ -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();
}
});

View File

@@ -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');
}
});

View 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();
}
});

View File

@@ -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;
}
});

View File

@@ -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]);
}
});

View 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

Binary file not shown.

BIN
accessible/media/oops.ogg Normal file

Binary file not shown.

BIN
accessible/media/oops.wav Normal file

Binary file not shown.

View File

@@ -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. ';

View 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();
}
}
});

View File

@@ -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);
}
});

View File

@@ -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.');
});
}
});

View File

@@ -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);
}
});

View File

@@ -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

View File

@@ -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);
}
});

View File

@@ -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);
}
});

View File

@@ -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();
}
});

View File

@@ -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>