Files
blockly/accessible/tree.service.js
M 6474fcd774 AccessibleBlockly (#354)
* adding a folder in demos for blind and setting up the package.json file for development. Adding package.json to .gitignore

* removing gitignore~ file

* copied blind blockly demo code over. set up a new dev environment. Up for review: the blind blockly demo code. Much of the drop downs don't actually do anything, but a general look at the way angular is used would be helpful in shaping future development.

* adding an index file that was being ignored by .gitignore

* adding required angular files and libraries previously ignored by git add --a for some unknown reason.

* starting work on toolbox

* got workspace and toolbox connected. Copy to workspace annd copy to clipboard working at 80%.

* cleaning up autogenerated files

* adding my own files. Not sure why the test files are being added.

* cleaning up comments

* removed empty html_formatter.html file from project and put on git ignore list. The file is used on my end to help with formatting the html in javascript files.

* getting rid of unnecessary html_formatter file

* removing merge conflicts

* cleaned up comments

* cleaned up comments, organized file structure, made ToolboxXML a file that can be changed by other developers porting this code, added copyright and file summaries to js files, linted files, moved third party libraries to lib folder

* removing lint files that shouldn't be tracked.

* Made the clipboard a service instead of multiple pieces embedded within the app. Not sold on the name ClipboardService. Open to suggestions.

* syncing up demo updates

* adding insert block menus above and below appropriate blocks.

* hacky version of getting code to run.

* changing toolbox to be a menu. making both the toolbox and workspace ordered lists

* adding new folder dev-old containing a version of blind blockly that uses nested lists for the toolbox

* getting rid of menubar experiment

* adding treeview shenanigans part 1

* adding treeview experiment page and finishing up treeview.

* getting a treeview implementation working in the toolbox

* creating a fishfood folder for fishfood development

* making first level of fishfood

* fixing something

* adding index file for fishfood

* beginning merging the treeview system.

* creating two treeviews

* fishfood level 1 without working music blocks

* fixes to treeview navigation

* final fishfood features done

* adding support for toolboxes without categories

* beginning integration of Sean's new music code

* adding Sean's music blocks and beginning music_player integration.

* merged Sean's music block code into fishfood

* merged Sean's music code and fixed treeview bug with toolbox when no categories

* adding blank_demo

* fixing tab navigation.

* fixing bug with shift-tab. New bug found in alt+tab not opening dropdown?

* fixing labels

* fixing bugs and finding more.

* removing dropdowns in favor of buttons part 1. Still need to change field options.

* index.html for blank_demo

* beginning cleanup

* adding blockly- prefix to label ids.

* integrating AccessibleBlockly with Blockly library.

* remove closure library

* removing testing files that aren't in develop

* removing all demo stuff for a later pull request

* removing gitignore file that doesn't exist in developer branch

* syncing with develop branch files

* removing music files from this pull request

* doing some lint cleanup

* rolling back accidental package.json change

* removing var blocklyApp = blocklyApp || {} from each extraneous file

* adding last debug statement

* changed name of inputType function

* renaming _service arguments

* fixing function names

* fixing all comments.

* fixing minor comments

* renaming variables, etc

* fixing bugs that break demo. Adding the run code and clear workspace buttons back to the workspaceview as their current position breaks tab navigation.

* fixing minor comments

* fixing function names, etc.

* adding comments to getInputTypeLabel function

* removing addClass() calls from the template

* small change to clipboardService

* splitting getCategoryDependantId into two functions: getCategoryId and setActiveDesc

* fixing minor bug with last commit

* using conditional attributes to remove setActiveAttributes function from treeService and do same work in template.

* deleting unnecessary comment created in previous commit.

* adding a utilsService to remove duplicated code.

* changing function names in utilsService

* changing delete to [delete] to allow HTMLText functions to return an empty string instead of undefined. Also renaming concatStringWithSpaces to generateAriaLabelledByAttr

* generating ids in ngOnInit instead of on the fly, allowing us to use [attr.aria-labelledBy] in the template instead of using setLabelledBy(element). Deleting function utilsService.setLabelledBy.

* fixing small bug with tree service left arrow: was throwing an error when you can't go any further left.

* reformatting html to make it easier to read.

* moving from importing the xml file to using an xml toolbox in the demo.

* fixing minor bugs and removing code relevant only to the music game.

* minor fixes

* adding alerts to copy, paste, and mark actions. renaming blocklyApp.TreeView to blocklyApp.WorkspaceTreeView

* Beginning to add Blockly.msg strings.

* making sure index file is being tracked

* adding all demo bits

* removing demo files from this pull request

* fixing providers, some linting, and removing getInfo() function.

* adding small todo comment for sll@

* adding minimal Accessible Blockly demo

* removing demo: accidentally committed to wrong branch

* fixing Blockly.Msg string bugs

* fixing TODOs

* package file changed by an automerge. Changing back.

* addressing sll's comments. Creating shim for utilsService functions.

* fixing comments, changing fieldview to deal with this.field.

* fixing clipboard service bug

* fixing focus bug when editing a text input.

* fixed clipboard bug and added README

* fixing workspaceview not to implement on runCode() function and adding note to README
2016-05-23 16:42:18 -07:00

332 lines
12 KiB
JavaScript

/**
* 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 all tree keyboard navigation.
* @author madeeha@google.com (Madeeha Ghori)
*/
blocklyApp.TreeService = ng.core
.Class({
constructor: function() {
blocklyApp.debug && console.log('making a new tree service');
// Keeping track of the active descendants in each tree.
this.activeDesc_ = Object.create(null);
this.trees = document.getElementsByClassName('blocklyTree');
// Keeping track of the last key pressed. If the user presses
// enter (to edit a text input or press a button), the keyboard
// focus shifts to that element. In the next keystroke, if the user
// navigates away from the element using the arrow keys, we want
// to shift focus back to the tree as a whole.
this.previousKey_ = null;
},
createId: function(obj) {
if (obj && obj.id) {
return obj.id;
}
return 'blockly-' + Blockly.genUid();
},
setActiveDesc: function(node, id) {
blocklyApp.debug && console.log('setting active descendant for tree ' + id);
this.activeDesc_[id] = node;
},
getActiveDesc: function(id) {
return this.activeDesc_[id] || document.getElementById((document.getElementById(id)).getAttribute('aria-activedescendant'));
},
// Makes a given node the active descendant of a given tree.
updateSelectedNode: function(node, tree, keepFocus) {
blocklyApp.debug && console.log('updating node: ' + node.id);
var treeId = tree.id;
var activeDesc = this.getActiveDesc(treeId);
if (activeDesc) {
activeDesc.classList.remove('blocklyActiveDescendant');
activeDesc.setAttribute('aria-selected', 'false');
} else {
blocklyApp.debug && console.log('updateSelectedNode: there is no active descendant');
}
node.classList.add('blocklyActiveDescendant');
tree.setAttribute('aria-activedescendant', node.id);
this.setActiveDesc(node, treeId);
node.setAttribute('aria-selected', 'true');
// Make sure keyboard focus is on tree as a whole
// in case focus was previously on a button or input
// element.
if (keepFocus) {
tree.focus();
}
},
onWorkspaceToolbarKeypress: function(e, treeId) {
blocklyApp.debug && console.log(e.keyCode + 'inside TreeService onWorkspaceToolbarKeypress');
switch (e.keyCode) {
case 9:
// 16,9: shift, tab
if (e.shiftKey) {
blocklyApp.debug && console.log('shifttabbing');
// If the previous key is shift, we're shift-tabbing mode.
this.goToPreviousTree(treeId);
e.preventDefault();
e.stopPropagation();
} else {
// If previous key isn't shift, we're tabbing.
this.goToNextTree(treeId);
e.preventDefault();
e.stopPropagation();
}
break;
default:
break;
}
},
goToNextTree: function(treeId, e) {
for (var i = 0; i < this.trees.length; i++) {
if (this.trees[i].id == treeId) {
if (i + 1 < this.trees.length) {
this.trees[i + 1].focus();
}
break;
}
}
},
goToPreviousTree: function(treeId, e) {
if (treeId == this.trees[0].id) {
return;
}
for (var i = (this.trees.length - 1); i >= 0; i--) {
if (this.trees[i].id == treeId) {
if (i - 1 < this.trees.length) {
this.trees[i - 1].focus();
}
break;
}
}
},
onKeypress: function(e, tree) {
var treeId = tree.id;
var node = this.getActiveDesc(treeId);
var keepFocus = this.previousKey_ == 13;
if (!node) {
blocklyApp.debug && console.log('KeyHandler: no active descendant');
}
blocklyApp.debug && console.log(e.keyCode + ': inside TreeService');
switch (e.keyCode) {
case 9:
// 16,9: shift, tab
if (e.shiftKey) {
blocklyApp.debug && console.log('shifttabbing');
// If the previous key is shift, we're shift-tabbing.
this.goToPreviousTree(treeId);
e.preventDefault();
e.stopPropagation();
} else {
// If previous key isn't shift, we're tabbing
// we want to go to the run code button.
this.goToNextTree(treeId);
e.preventDefault();
e.stopPropagation();
}
// Setting the previous key variable in each case because
// we only want to save the previous navigation keystroke,
// not any typing.
this.previousKey_ = e.keyCode;
break;
case 37:
// Left-facing arrow: go out a level, if possible. If not, do nothing.
e.preventDefault();
e.stopPropagation();
blocklyApp.debug && console.log('in left arrow section');
var nextNode = node.parentNode;
if (node.tagName == 'BUTTON' || node.tagName == 'INPUT') {
nextNode = nextNode.parentNode;
}
while (nextNode && nextNode.className != 'treeview' &&
nextNode.tagName != 'LI') {
nextNode = nextNode.parentNode;
}
if (!nextNode || nextNode.className == 'treeview') {
return;
}
this.updateSelectedNode(nextNode, tree, keepFocus);
this.previousKey_ = e.keyCode;
break;
case 38:
// Up-facing arrow: go up a level, if possible. If not, do nothing.
e.preventDefault();
e.stopPropagation();
blocklyApp.debug && console.log('node passed in: ' + node.id);
var prevSibling = this.getPreviousSibling(node);
if (prevSibling && prevSibling.tagName != 'H1') {
this.updateSelectedNode(prevSibling, tree, keepFocus);
} else {
blocklyApp.debug && console.log('no previous sibling');
}
this.previousKey_ = e.keyCode;
break;
case 39:
e.preventDefault();
e.stopPropagation();
blocklyApp.debug && console.log('in right arrow section');
var firstChild = this.getFirstChild(node);
if (firstChild) {
this.updateSelectedNode(firstChild, tree, keepFocus);
} else {
blocklyApp.debug && console.log('no valid child');
}
this.previousKey_ = e.keyCode;
break;
case 40:
// Down-facing arrow: go down a level, if possible.
// If not, do nothing.
blocklyApp.debug && console.log('preventing propogation');
e.preventDefault();
e.stopPropagation();
var nextSibling = this.getNextSibling(node);
if (nextSibling) {
this.updateSelectedNode(nextSibling, tree, keepFocus);
} else {
blocklyApp.debug && console.log('no next sibling');
}
this.previousKey_ = e.keyCode;
break;
case 13:
// If I've pressed enter, I want to interact with a child.
blocklyApp.debug && console.log('enter is pressed');
var activeDesc = this.getActiveDesc(treeId);
if (activeDesc) {
var children = activeDesc.children;
var child = children[0];
if (children.length == 1 && (child.tagName == 'INPUT' ||
child.tagName == 'BUTTON')) {
if (child.tagName == 'BUTTON') {
child.click();
}
else if (child.tagName == 'INPUT') {
child.focus();
}
}
} else {
blocklyApp.debug && console.log('no activeDesc');
}
this.previousKey_ = e.keyCode;
break;
default:
break;
}
},
getFirstChild: function(element) {
if (!element) {
return element;
} else {
var childList = element.children;
for (var i = 0; i < childList.length; i++) {
if (childList[i].tagName == 'LI') {
return childList[i];
} else {
var potentialElement = this.getFirstChild(childList[i]);
if (potentialElement) {
return potentialElement;
}
}
}
return null;
}
},
getNextSibling: function(element) {
if (element.nextElementSibling) {
// If there is a sibling, find the list element child of the sibling.
var node = element.nextElementSibling;
if (node.tagName != 'LI') {
var listElems = node.getElementsByTagName('li');
// getElementsByTagName returns in DFS order
// therefore the first element is the first relevant list child.
return listElems[0];
} else {
return element.nextElementSibling;
}
} else {
var parent = element.parentNode;
while (parent && parent.tagName != 'OL') {
if (parent.nextElementSibling) {
var node = parent.nextElementSibling;
if (node.tagName == 'LI') {
return node;
} else {
return this.getFirstChild(node);
}
} else {
parent = parent.parentNode;
}
}
return null;
}
},
getPreviousSibling: function(element) {
if (element.previousElementSibling) {
var sibling = element.previousElementSibling;
if (sibling.tagName == 'LI') {
return sibling;
} else {
return this.getLastChild(sibling);
}
} else {
var parent = element.parentNode;
while (parent != null) {
blocklyApp.debug && console.log('looping');
if (parent.tagName == 'OL') {
break;
}
if (parent.previousElementSibling) {
blocklyApp.debug && console.log('parent has a sibling!');
var node = parent.previousElementSibling;
if (node.tagName == 'LI') {
blocklyApp.debug && console.log('return the sibling of the parent!');
return node;
} else {
// Find the last list element child of the sibling of the parent.
return this.getLastChild(node);
}
} else {
parent = parent.parentNode;
}
}
return null;
}
},
getLastChild: function(element) {
if (!element) {
blocklyApp.debug && console.log('no element');
return element;
} else {
var childList = element.children;
for (var i = childList.length - 1; i >= 0; i--) {
// Find the last child that is a list element.
if (childList[i].tagName == 'LI') {
return childList[i];
} else {
var potentialElement = this.getLastChild(childList[i]);
if (potentialElement) {
return potentialElement;
}
}
}
blocklyApp.debug && console.log('no last child');
return null;
}
},
});