123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357 |
- /**
- * 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 tree keyboard navigation.
- * This is a singleton service for the entire application.
- *
- * @author madeeha@google.com (Madeeha Ghori)
- */
- blocklyApp.TreeService = ng.core
- .Class({
- constructor: function() {
- // Stores active descendant ids for each tree in the page.
- this.activeDescendantIds_ = {};
- },
- getToolboxTreeNode_: function() {
- return document.getElementById('blockly-toolbox-tree');
- },
- getWorkspaceToolbarButtonNodes_: function() {
- return Array.from(document.querySelectorAll(
- 'button.blocklyWorkspaceToolbarButton'));
- },
- // Returns a list of all top-level workspace tree nodes on the page.
- getWorkspaceTreeNodes_: function() {
- return Array.from(document.querySelectorAll('ol.blocklyWorkspaceTree'));
- },
- // Returns a list of all top-level tree nodes on the page.
- getAllTreeNodes_: function() {
- var treeNodes = [this.getToolboxTreeNode_()];
- treeNodes = treeNodes.concat(this.getWorkspaceToolbarButtonNodes_());
- treeNodes = treeNodes.concat(this.getWorkspaceTreeNodes_());
- return treeNodes;
- },
- isTopLevelWorkspaceTree: function(treeId) {
- return this.getWorkspaceTreeNodes_().some(function(tree) {
- return tree.id == treeId;
- });
- },
- 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.
- 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_();
- for (var i = 0; i < trees.length; i++) {
- if (trees[i].id == treeId) {
- trees[i].focus();
- return true;
- }
- }
- return false;
- },
- focusOnNextTree_: function(treeId) {
- var trees = this.getAllTreeNodes_();
- for (var i = 0; i < trees.length - 1; i++) {
- if (trees[i].id == treeId) {
- trees[i + 1].focus();
- return true;
- }
- }
- return false;
- },
- focusOnPreviousTree_: function(treeId) {
- var trees = this.getAllTreeNodes_();
- for (var i = trees.length - 1; i > 0; i--) {
- if (trees[i].id == treeId) {
- trees[i - 1].focus();
- return true;
- }
- }
- return false;
- },
- getActiveDescId: function(treeId) {
- return this.activeDescendantIds_[treeId] || '';
- },
- unmarkActiveDesc_: function(activeDescId) {
- var activeDesc = document.getElementById(activeDescId);
- if (activeDesc) {
- activeDesc.classList.remove('blocklyActiveDescendant');
- activeDesc.setAttribute('aria-selected', 'false');
- }
- },
- markActiveDesc_: function(activeDescId) {
- var newActiveDesc = document.getElementById(activeDescId);
- newActiveDesc.classList.add('blocklyActiveDescendant');
- newActiveDesc.setAttribute('aria-selected', 'true');
- },
- // Runs the given function while preserving the focus and active descendant
- // for the given tree.
- runWhilePreservingFocus: function(func, treeId, optionalNewActiveDescId) {
- var oldDescId = this.getActiveDescId(treeId);
- var newDescId = optionalNewActiveDescId || oldDescId;
- this.unmarkActiveDesc_(oldDescId);
- func();
- // The timeout is needed in order to give the DOM time to stabilize
- // before setting the new active descendant, especially in cases like
- // pasteAbove().
- var that = this;
- setTimeout(function() {
- that.markActiveDesc_(newDescId);
- that.activeDescendantIds_[treeId] = newDescId;
- document.getElementById(treeId).focus();
- }, 0);
- },
- // Make a given node the active descendant of a given tree.
- setActiveDesc: function(newActiveDescId, treeId) {
- this.unmarkActiveDesc_(this.getActiveDescId(treeId));
- this.markActiveDesc_(newActiveDescId);
- this.activeDescendantIds_[treeId] = newActiveDescId;
- },
- onWorkspaceToolbarKeypress: function(e, treeId) {
- if (e.keyCode == 9) {
- // Tab key.
- if (e.shiftKey) {
- this.focusOnPreviousTree_(treeId);
- } else {
- this.focusOnNextTree_(treeId);
- }
- e.preventDefault();
- e.stopPropagation();
- }
- },
- isButtonOrFieldNode_: function(node) {
- return ['BUTTON', 'INPUT'].indexOf(node.tagName) != -1;
- },
- getNextActiveDescWhenBlockIsDeleted: function(blockRootNode) {
- // Go up a level, if possible.
- var nextNode = blockRootNode.parentNode;
- while (nextNode && nextNode.tagName != 'LI') {
- nextNode = nextNode.parentNode;
- }
- if (nextNode) {
- return nextNode;
- }
- // Otherwise, go to the next sibling.
- var nextSibling = this.getNextSibling(blockRootNode);
- if (nextSibling) {
- return nextSibling;
- }
- // Otherwise, go to the previous sibling.
- var previousSibling = this.getPreviousSibling(blockRootNode);
- if (previousSibling) {
- return previousSibling;
- }
- // Otherwise, this is a top-level isolated block, which means that
- // something's gone wrong and this function should not have been called
- // in the first place.
- console.error('Could not handle deletion of block.' + blockRootNode);
- },
- onKeypress: function(e, tree) {
- var treeId = tree.id;
- var activeDesc = document.getElementById(this.getActiveDescId(treeId));
- if (!activeDesc) {
- console.error('ERROR: no active descendant for current tree.');
- // TODO(sll): Generalize this to other trees (outside the workspace).
- var workspaceTreeNodes = this.getWorkspaceTreeNodes_();
- for (var i = 0; i < workspaceTreeNodes.length; i++) {
- if (workspaceTreeNodes[i].id == treeId) {
- // Set the active desc to the first child in this tree.
- this.setActiveDesc(
- this.getFirstChild(workspaceTreeNodes[i]).id, treeId);
- break;
- }
- }
- return;
- }
- var isFocusingIntoField = false;
- if (e.keyCode == 13) {
- // Enter key. The user wants to interact with a child.
- if (activeDesc.children.length == 1) {
- var child = activeDesc.children[0];
- if (child.tagName == 'BUTTON') {
- child.click();
- this.isFocusingIntoField = true;
- } else if (child.tagName == 'INPUT') {
- child.focus();
- }
- }
- } else if (e.keyCode == 9) {
- // Tab key.
- if (e.shiftKey) {
- this.focusOnPreviousTree_(treeId);
- } else {
- this.focusOnNextTree_(treeId);
- }
- e.preventDefault();
- e.stopPropagation();
- } else if (e.keyCode >= 37 && e.keyCode <= 40) {
- // Arrow keys.
- if (e.keyCode == 37) {
- // Left arrow key. Go up a level, if possible.
- var nextNode = activeDesc.parentNode;
- if (this.isButtonOrFieldNode_(activeDesc)) {
- nextNode = nextNode.parentNode;
- }
- while (nextNode && nextNode.tagName != 'LI') {
- nextNode = nextNode.parentNode;
- }
- if (nextNode) {
- this.setActiveDesc(nextNode.id, treeId);
- }
- } else if (e.keyCode == 38) {
- // Up arrow key. Go to the previous sibling, if possible.
- var prevSibling = this.getPreviousSibling(activeDesc);
- if (prevSibling) {
- this.setActiveDesc(prevSibling.id, treeId);
- }
- } else if (e.keyCode == 39) {
- // Right arrow key. Go down a level, if possible.
- var firstChild = this.getFirstChild(activeDesc);
- if (firstChild) {
- this.setActiveDesc(firstChild.id, treeId);
- }
- } else if (e.keyCode == 40) {
- // Down arrow key. Go to the next sibling, if possible.
- var nextSibling = this.getNextSibling(activeDesc);
- if (nextSibling) {
- this.setActiveDesc(nextSibling.id, treeId);
- }
- }
- e.preventDefault();
- e.stopPropagation();
- }
- },
- 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') {
- return node;
- } else {
- // getElementsByTagName returns in DFS order, therefore the first
- // element is the first relevant list child.
- return node.getElementsByTagName('li')[0];
- }
- } 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) {
- if (parent.tagName == 'OL') {
- break;
- }
- if (parent.previousElementSibling) {
- var node = parent.previousElementSibling;
- if (node.tagName == 'LI') {
- 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) {
- 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;
- }
- }
- }
- return null;
- }
- }
- });
|