Delete the on-screen toolbox.

This commit is contained in:
Sean Lip
2016-11-17 17:45:12 -08:00
parent cf9a4fbdf4
commit 602c1ba56e
8 changed files with 35 additions and 422 deletions

View File

@@ -36,20 +36,17 @@ blocklyApp.AppView = ng.core.Component({
<blockly-toolbox-modal></blockly-toolbox-modal>
<div>
<blockly-toolbox></blockly-toolbox>
<blockly-workspace></blockly-workspace>
<blockly-sidebar></blockly-sidebar>
</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, blocklyApp.SidebarComponent,
blocklyApp.ToolboxModalComponent],
blocklyApp.WorkspaceComponent, blocklyApp.BlockOptionsModalComponent,
blocklyApp.SidebarComponent, blocklyApp.ToolboxModalComponent],
pipes: [blocklyApp.TranslatePipe],
// All services are declared here, so that all components in the
// application use the same instance of the service.

View File

@@ -1,10 +1,6 @@
.blocklyToolboxColumn {
float: left;
width: 350px;
}
.blocklyWorkspaceColumn {
float: left;
width: 350px;
width: 700px;
}
.blocklySidebarColumn {
float: left;

View File

@@ -24,9 +24,7 @@
*/
'use strict';
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 edit. Press Enter for more options.';

View File

@@ -1,169 +0,0 @@
/**
* 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 details how blocks are
* rendered in the toolbox in AccessibleBlockly. Also handles any interactions
* with the blocks.
* @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;
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

@@ -1,133 +0,0 @@
/**
* 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 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>
<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) {
this.toolboxCategories = [];
this.treeService = _treeService;
this.utilsService = _utilsService;
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);
}
});

View File

@@ -39,12 +39,8 @@ blocklyApp.TreeService = ng.core.Class({
this.clipboardService = _clipboardService;
this.blockOptionsModalService = _blockOptionsModalService;
this.audioService = _audioService;
this.toolboxWorkspaces = {};
}
],
getToolboxTreeNode_: function() {
return document.getElementById('blockly-toolbox-tree');
},
// Returns a list of all top-level workspace tree nodes on the page.
getWorkspaceTreeNodes_: function() {
return Array.from(document.querySelectorAll('ol.blocklyWorkspaceTree'));
@@ -52,45 +48,10 @@ blocklyApp.TreeService = ng.core.Class({
getSidebarButtonNodes_: function() {
return Array.from(document.querySelectorAll('button.blocklySidebarButton'));
},
getToolboxWorkspace: function(categoryNode) {
if (categoryNode.attributes && categoryNode.attributes.name) {
var categoryName = categoryNode.attributes.name.value;
} else {
var categoryName = 'no-category';
}
if (this.toolboxWorkspaces.hasOwnProperty(categoryName)) {
return this.toolboxWorkspaces[categoryName];
} else {
var categoryWorkspace = new Blockly.Workspace();
if (categoryName == 'no-category') {
for (var i = 0; i < categoryNode.length; i++) {
Blockly.Xml.domToBlock(categoryWorkspace, categoryNode[i]);
}
} else {
Blockly.Xml.domToWorkspace(categoryNode, categoryWorkspace);
}
this.toolboxWorkspaces[categoryName] = categoryWorkspace;
return this.toolboxWorkspaces[categoryName];
}
},
getToolboxBlockById: function(blockId) {
for (var categoryName in this.toolboxWorkspaces) {
var putativeBlock = this.utilsService.getBlockByIdFromWorkspace(
blockId, this.toolboxWorkspaces[categoryName]);
if (putativeBlock) {
return putativeBlock;
}
}
return null;
},
// Returns a list of all top-level tree nodes on the page.
getAllTreeNodes_: function() {
var treeNodes = [this.getToolboxTreeNode_()];
treeNodes = treeNodes.concat(this.getWorkspaceTreeNodes_());
treeNodes = treeNodes.concat(this.getSidebarButtonNodes_());
return treeNodes;
return this.getWorkspaceTreeNodes_().concat(
this.getSidebarButtonNodes_());
},
isTopLevelWorkspaceTree: function(treeId) {
return this.getWorkspaceTreeNodes_().some(function(tree) {
@@ -99,20 +60,15 @@ blocklyApp.TreeService = ng.core.Class({
},
getNodeToFocusOnWhenTreeIsDeleted: function(deletedTreeId) {
// This returns the node to focus on after the deletion happens.
// We shift focus to the next tree (if it exists), otherwise we shift
// focus to the previous tree.
// We shift focus to the next tree (which may be a button in the sidebar).
var trees = this.getAllTreeNodes_();
for (var i = 0; i < trees.length; i++) {
if (trees[i].id == deletedTreeId) {
if (i + 1 < trees.length) {
return trees[i + 1];
} else if (i > 0) {
return trees[i - 1];
}
}
}
return this.getToolboxTreeNode_();
},
focusOnCurrentTree_: function(treeId) {
var trees = this.getAllTreeNodes_();
@@ -268,16 +224,12 @@ blocklyApp.TreeService = ng.core.Class({
console.error('Could not handle deletion of block.' + blockRootNode);
},
notifyUserAboutCurrentTree_: function(treeId) {
if (this.getToolboxTreeNode_().id == treeId) {
this.notificationsService.setStatusMessage('Now in toolbox.');
} else {
var workspaceTreeNodes = this.getWorkspaceTreeNodes_();
for (var i = 0; i < workspaceTreeNodes.length; i++) {
if (workspaceTreeNodes[i].id == treeId) {
this.notificationsService.setStatusMessage(
'Now in workspace group ' + (i + 1) + ' of ' +
workspaceTreeNodes.length);
}
var workspaceTreeNodes = this.getWorkspaceTreeNodes_();
for (var i = 0; i < workspaceTreeNodes.length; i++) {
if (workspaceTreeNodes[i].id == treeId) {
this.notificationsService.setStatusMessage(
'Now in workspace group ' + (i + 1) + ' of ' +
workspaceTreeNodes.length);
}
}
},
@@ -444,26 +396,24 @@ blocklyApp.TreeService = ng.core.Class({
that.focusOnBlock(block.id);
});
},
getBlockRootSuffix_: function(inToolbox) {
return inToolbox ? 'toolboxBlockRoot' : 'blockRoot';
getBlockRootSuffix_: function() {
return 'blockRoot';
},
getCurrentBlockRootNode_: function(inToolbox, activeDesc) {
getCurrentBlockRootNode_: function(activeDesc) {
// Starting from the activeDesc, walk up the tree until we find the
// root of the current block.
var blockRootSuffix = this.getBlockRootSuffix_(inToolbox);
var blockRootSuffix = this.getBlockRootSuffix_();
var putativeBlockRootNode = activeDesc;
while (putativeBlockRootNode.id.indexOf(blockRootSuffix) === -1) {
putativeBlockRootNode = putativeBlockRootNode.parentNode;
}
return putativeBlockRootNode;
},
getBlockFromRootNode_: function(inToolbox, blockRootNode) {
var blockRootSuffix = this.getBlockRootSuffix_(inToolbox);
getBlockFromRootNode_: function(blockRootNode) {
var blockRootSuffix = this.getBlockRootSuffix_();
var blockId = blockRootNode.id.substring(
0, blockRootNode.id.length - blockRootSuffix.length);
return inToolbox ?
this.getToolboxBlockById(blockId) :
this.utilsService.getBlockById(blockId);
return this.utilsService.getBlockById(blockId);
},
onKeypress: function(e, tree) {
// TODO(sll): Instead of this, have a common ActiveContextService which
@@ -485,36 +435,15 @@ blocklyApp.TreeService = ng.core.Class({
return;
}
// Scout up the tree to see whether we're in the toolbox or workspace.
var scoutNode = activeDesc;
var TARGET_TAG_NAMES = ['BLOCKLY-TOOLBOX', 'BLOCKLY-WORKSPACE'];
while (TARGET_TAG_NAMES.indexOf(scoutNode.tagName) === -1) {
scoutNode = scoutNode.parentNode;
}
var inToolbox = (scoutNode.tagName == 'BLOCKLY-TOOLBOX');
if (e.ctrlKey) {
// Disallow cutting and pasting in the toolbox.
if (inToolbox && e.keyCode != 67) {
if (e.keyCode == 86) {
this.notificationsService.setStatusMessage(
'Cannot paste block in toolbox.');
} else if (e.keyCode == 88) {
this.notificationsService.setStatusMessage(
'Cannot cut block in toolbox. Try copying instead.');
}
}
var blockRootNode = this.getCurrentBlockRootNode_(
inToolbox, activeDesc);
var block = this.getBlockFromRootNode_(inToolbox, blockRootNode);
var blockRootNode = this.getCurrentBlockRootNode_(activeDesc);
var block = this.getBlockFromRootNode_(blockRootNode);
if (e.keyCode == 88) {
// Cut block.
this.cutBlock_(block, blockRootNode);
} else if (e.keyCode == 67) {
// Copy block. Note that, in this case, we might be in the workspace
// or toolbox.
// Copy block.
this.copyBlock_(block);
} else if (e.keyCode == 86) {
// Paste block, if possible.
@@ -582,10 +511,9 @@ blocklyApp.TreeService = ng.core.Class({
// If we cannot find a field to interact with, we open the modal for
// the current block instead.
if (!found && !inToolbox) {
var blockRootNode = this.getCurrentBlockRootNode_(
false, activeDesc);
var block = this.getBlockFromRootNode_(false, blockRootNode);
if (!found) {
var blockRootNode = this.getCurrentBlockRootNode_(activeDesc);
var block = this.getBlockFromRootNode_(blockRootNode);
e.stopPropagation();
this.showBlockOptionsModal(block, blockRootNode);

View File

@@ -67,11 +67,6 @@ blocklyApp.UtilsService = ng.core.Class({
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);
return blocklyApp.workspace.getBlockById(blockId);
}
});

View File

@@ -31,8 +31,6 @@
<script src="../../accessible/field-segment.component.js"></script>
<script src="../../accessible/block-options-modal.component.js"></script>
<script src="../../accessible/toolbox-modal.component.js"></script>
<script src="../../accessible/toolbox-tree.component.js"></script>
<script src="../../accessible/toolbox.component.js"></script>
<script src="../../accessible/sidebar.component.js"></script>
<script src="../../accessible/workspace-tree.component.js"></script>
<script src="../../accessible/workspace.component.js"></script>
@@ -60,13 +58,16 @@
<a href="../index.html">Demos</a> &gt; Accessible Blockly
</h1>
<p>This is a simple demo of a version of Blockly designed for screen readers.</p>
<p>
In Blockly, you can move blocks from the toolbox to the workspace and join
them to create programs. To navigate between components, use Tab or
Shift-Tab. When you're on a block, move right to access its submenus, and
move up or down to go to the next or previous block in the sequence.
This is a demo of a version of Blockly designed for screen readers. It
allows users to create programs in a workspace by manipulating groups of
blocks.
<ul>
<li>To explore a group of blocks, use the arrow keys.</li>
<li>To navigate between groups, use Tab or Shift-Tab.</li>
<li>To add new blocks, use the buttons in the menu on the right.</li>
<li>To delete or add links to existing blocks, press Enter while you're on that block.</li>
</ul>
</p>
<!--