diff --git a/accessible/tree.service.js b/accessible/tree.service.js index 6aee3ca51..c4d37913e 100644 --- a/accessible/tree.service.js +++ b/accessible/tree.service.js @@ -18,8 +18,9 @@ */ /** - * @fileoverview Angular2 Service that handles tree keyboard navigation. - * This is a singleton service for the entire application. + * @fileoverview Angular2 Service that handles keyboard navigation on workspace + * block groups (internally represented as trees). This is a singleton service + * for the entire application. * * @author madeeha@google.com (Madeeha Ghori) */ @@ -40,35 +41,128 @@ blocklyApp.TreeService = ng.core.Class({ this.notificationsService = notificationsService; this.utilsService = utilsService; - // Stores active descendant ids for each tree in the page. + // The suffix used for all IDs of block root elements. + this.BLOCK_ROOT_ID_SUFFIX_ = 'blockRoot'; + // Maps tree IDs to the IDs of their active descendants. this.activeDescendantIds_ = {}; + // Array containing all the sidebar button elements. + this.sidebarButtonElements_ = Array.from( + document.querySelectorAll('button.blocklySidebarButton')); } ], - // Returns a list of all top-level workspace tree nodes on the page. - getWorkspaceFocusTargets_: function() { - return Array.from( - document.querySelectorAll('.blocklyWorkspaceFocusTarget')); + scrollToElement_: function(elementId) { + var element = document.getElementById(elementId); + var documentElement = document.body || document.documentElement; + if (element.offsetTop < documentElement.scrollTop || + element.offsetTop > documentElement.scrollTop + window.innerHeight) { + window.scrollTo(0, element.offsetTop - 10); + } }, - getSidebarButtonNodes_: function() { - return Array.from(document.querySelectorAll('button.blocklySidebarButton')); + + isLi_: function(node) { + return node.tagName == 'LI'; }, - // Returns a list of all top-level tree nodes on the page. - getAllTreeNodes_: function() { - return this.getWorkspaceFocusTargets_().concat( - this.getSidebarButtonNodes_()); + getParentLi_: function(element) { + var nextNode = element.parentNode; + while (nextNode && !this.isLi_(nextNode)) { + nextNode = nextNode.parentNode; + } + return nextNode; }, - focusOnCurrentTree_: function(treeId) { - var trees = this.getAllTreeNodes_(); - for (var i = 0; i < trees.length; i++) { - if (trees[i].id == treeId) { - trees[i].focus(); - return trees[i].id; + getFirstChildLi_: function(element) { + var childList = element.children; + for (var i = 0; i < childList.length; i++) { + if (this.isLi_(childList[i])) { + return childList[i]; + } else { + var potentialElement = this.getFirstChildLi_(childList[i]); + if (potentialElement) { + return potentialElement; + } } } return null; }, - getIdOfNextTree_: function(treeId) { - var trees = this.getAllTreeNodes_(); + getLastChildLi_: function(element) { + var childList = element.children; + for (var i = childList.length - 1; i >= 0; i--) { + if (this.isLi_(childList[i])) { + return childList[i]; + } else { + var potentialElement = this.getLastChildLi_(childList[i]); + if (potentialElement) { + return potentialElement; + } + } + } + return null; + }, + getInitialSiblingLi_: function(element) { + while (true) { + var previousSibling = this.getPreviousSiblingLi_(element); + if (previousSibling && previousSibling.id != element.id) { + element = previousSibling; + } else { + return element; + } + } + }, + getPreviousSiblingLi_: function(element) { + if (element.previousElementSibling) { + var sibling = element.previousElementSibling; + return this.isLi_(sibling) ? sibling : this.getLastChildLi_(sibling); + } else { + var parent = element.parentNode; + while (parent && parent.tagName != 'OL') { + if (parent.previousElementSibling) { + var node = parent.previousElementSibling; + return this.isLi_(node) ? node : this.getLastChildLi_(node); + } else { + parent = parent.parentNode; + } + } + return null; + } + }, + getNextSiblingLi_: function(element) { + if (element.nextElementSibling) { + var sibling = element.nextElementSibling; + return this.isLi_(sibling) ? sibling : this.getFirstChildLi_(sibling); + } else { + var parent = element.parentNode; + while (parent && parent.tagName != 'OL') { + if (parent.nextElementSibling) { + var node = parent.nextElementSibling; + return this.isLi_(node) ? node : this.getFirstChildLi_(node); + } else { + parent = parent.parentNode; + } + } + return null; + } + }, + getFinalSiblingLi_: function(element) { + while (true) { + var nextSibling = this.getNextSiblingLi_(element); + if (nextSibling && nextSibling.id != element.id) { + element = nextSibling; + } else { + return element; + } + } + }, + + // Returns a list of all focus targets in the workspace, including the + // "Create new group" button that appears when no blocks are present. + getWorkspaceFocusTargets_: function() { + return Array.from( + document.querySelectorAll('.blocklyWorkspaceFocusTarget')); + }, + getAllFocusTargets_: function() { + return this.getWorkspaceFocusTargets_().concat(this.sidebarButtonElements_); + }, + getNextFocusTargetId_: function(treeId) { + var trees = this.getAllFocusTargets_(); for (var i = 0; i < trees.length - 1; i++) { if (trees[i].id == treeId) { return trees[i + 1].id; @@ -76,8 +170,8 @@ blocklyApp.TreeService = ng.core.Class({ } return null; }, - getIdOfPreviousTree_: function(treeId) { - var trees = this.getAllTreeNodes_(); + getPreviousFocusTargetId_: function(treeId) { + var trees = this.getAllFocusTargets_(); for (var i = trees.length - 1; i > 0; i--) { if (trees[i].id == treeId) { return trees[i - 1].id; @@ -85,156 +179,89 @@ blocklyApp.TreeService = ng.core.Class({ } return null; }, + getActiveDescId: function(treeId) { return this.activeDescendantIds_[treeId] || ''; }, - unmarkActiveDesc_: function(activeDescId) { - var activeDesc = document.getElementById(activeDescId); - if (activeDesc) { - activeDesc.classList.remove('blocklyActiveDescendant'); - } + // Set the active desc for this tree to its first child. + initActiveDesc: function(treeId) { + var tree = document.getElementById(treeId); + this.setActiveDesc(this.getFirstChildLi_(tree).id, treeId); }, - markActiveDesc_: function(activeDescId) { - var newActiveDesc = document.getElementById(activeDescId); - newActiveDesc.classList.add('blocklyActiveDescendant'); - }, - // 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); - }, - // This clears the active descendant of the given tree. It is used just - // before the tree is deleted. - clearActiveDesc: function(treeId) { - this.unmarkActiveDesc_(this.getActiveDescId(treeId)); - delete this.activeDescendantIds_[treeId]; - }, - // Make a given node the active descendant of a given tree. + // Make a given element the active descendant of a given tree. setActiveDesc: function(newActiveDescId, treeId) { - this.unmarkActiveDesc_(this.getActiveDescId(treeId)); - this.markActiveDesc_(newActiveDescId); + if (this.getActiveDescId(treeId)) { + this.clearActiveDesc(treeId); + } + document.getElementById(newActiveDescId).classList.add( + 'blocklyActiveDescendant'); this.activeDescendantIds_[treeId] = newActiveDescId; // Scroll the new active desc into view, if needed. This has no effect // for blind users, but is helpful for sighted onlookers. - var activeDescNode = document.getElementById(newActiveDescId); - var documentNode = document.body || document.documentElement; - if (activeDescNode.offsetTop < documentNode.scrollTop || - activeDescNode.offsetTop > - documentNode.scrollTop + window.innerHeight) { - window.scrollTo(0, activeDescNode.offsetTop); + this.scrollToElement_(newActiveDescId); + }, + // This clears the active descendant of the given tree. It is used just + // before the tree is deleted. + clearActiveDesc: function(treeId) { + var activeDesc = document.getElementById(this.getActiveDescId(treeId)); + if (activeDesc) { + activeDesc.classList.remove('blocklyActiveDescendant'); + delete this.activeDescendantIds_[treeId]; + } else { + throw Error( + 'The active desc element for the tree with ID ' + treeId + + ' is invalid.'); } }, - initActiveDesc: function(treeId) { - // Set the active desc to the first child in this tree. - var tree = document.getElementById(treeId); - this.setActiveDesc(this.getFirstChild(tree).id, treeId); - }, - getTreeIdForBlock: function(blockId) { - // Walk up the DOM until we get to the root node of the tree. - var domNode = document.getElementById(blockId + 'blockRoot'); - while (!domNode.classList.contains('blocklyTree')) { - domNode = domNode.parentNode; - } - return domNode.id; - }, - focusOnBlock: function(blockId) { - // Set focus to the tree containing the given block, and set the active - // desc for this tree to the given block. - var domNode = document.getElementById(blockId + 'blockRoot'); - // Walk up the DOM until we get to the root node of the tree. - while (!domNode.classList.contains('blocklyTree')) { - domNode = domNode.parentNode; - } - domNode.focus(); - // We need to wait a while to set the active desc, because domNode takes - // a while to be given an ID if a new tree has just been created. - // TODO(sll): Make this more deterministic. - var that = this; - setTimeout(function() { - that.setActiveDesc(blockId + 'blockRoot', domNode.id); - }, 100); + isTreeRoot_: function(element) { + return element.classList.contains('blocklyTree'); }, - 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; + getBlockRootId_: function(blockId) { + return blockId + this.BLOCK_ROOT_ID_SUFFIX_; + }, + // Return the 'lowest' Blockly block in the DOM tree that contains the given + // DOM element. + getContainingBlock_: function(domElement) { + var potentialBlockRoot = domElement; + while (potentialBlockRoot.id.indexOf(this.BLOCK_ROOT_ID_SUFFIX_) === -1) { + potentialBlockRoot = potentialBlockRoot.parentNode; } - // 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); - }, - notifyUserAboutCurrentTree_: function(treeId) { - var workspaceFocusTargets = this.getWorkspaceFocusTargets_(); - for (var i = 0; i < workspaceFocusTargets.length; i++) { - if (workspaceFocusTargets[i].tagName == 'OL' && - workspaceFocusTargets[i].id == treeId) { - this.notificationsService.speak( - 'Now in workspace group ' + (i + 1) + ' of ' + - workspaceFocusTargets.length); - } - } + var blockRootId = potentialBlockRoot.id; + var blockId = blockRootId.substring( + 0, blockRootId.length - this.BLOCK_ROOT_ID_SUFFIX_.length); + return blocklyApp.workspace.getBlockById(blockId); }, + // Returns whether the given block is at the top level, and has no siblings. isIsolatedTopLevelBlock_: function(block) { - // Returns whether the given block is at the top level, and has no - // siblings. - var blockIsAtTopLevel = !block.getParent(); var blockHasNoSiblings = ( (!block.nextConnection || !block.nextConnection.targetConnection) && (!block.previousConnection || !block.previousConnection.targetConnection)); - return blockIsAtTopLevel && blockHasNoSiblings; + return !block.getParent() && blockHasNoSiblings; }, - removeBlockAndSetFocus: function(block, blockRootNode, deleteBlockFunc) { - // This method runs the given deletion function and then does one of two - // things: - // - If the block is an isolated top-level block, it shifts the tree - // focus. - // - Otherwise, it sets the correct new active desc for the current tree. + safelyRemoveBlock_: function(block, deleteBlockFunc) { + // Runs the given deleteBlockFunc (which should have the effect of deleting + // the given block) and then does one of two things: + // - If the deleted block was an isolated top-level block, this means the + // current tree has no more blocks after the deletion. So, pick a new + // tree to focus on. + // - Otherwise, set the correct new active desc for the current tree. var treeId = this.getTreeIdForBlock(block.id); + if (this.isIsolatedTopLevelBlock_(block)) { // Find the node to focus on after the deletion happens. - var nextNodeToFocusOn = null; + var nextElementToFocusOn = null; var focusTargets = this.getWorkspaceFocusTargets_(); for (var i = 0; i < focusTargets.length; i++) { if (focusTargets[i].id == treeId) { if (i + 1 < focusTargets.length) { - nextNodeToFocusOn = focusTargets[i + 1]; + nextElementToFocusOn = focusTargets[i + 1]; } else if (i > 0) { - nextNodeToFocusOn = focusTargets[i - 1]; + nextElementToFocusOn = focusTargets[i - 1]; } break; } @@ -242,20 +269,59 @@ blocklyApp.TreeService = ng.core.Class({ this.clearActiveDesc(treeId); deleteBlockFunc(); - // Invoke a digest cycle, so that the DOM settles. + // Invoke a digest cycle, so that the DOM settles (and the "Create new + // group" button in the workspace shows up, if applicable). setTimeout(function() { - nextNodeToFocusOn = nextNodeToFocusOn || document.getElementById( - 'blocklyEmptyWorkspaceButton'); - nextNodeToFocusOn.focus(); + if (nextElementToFocusOn) { + nextElementToFocusOn.focus(); + } else { + document.getElementById( + blocklyApp.ID_FOR_EMPTY_WORKSPACE_BTN).focus(); + } }); } else { - var nextActiveDesc = this.getNextActiveDescWhenBlockIsDeleted( - blockRootNode); - this.runWhilePreservingFocus( - deleteBlockFunc, treeId, nextActiveDesc.id); + var blockRootId = this.getBlockRootId_(block.id); + var blockRootElement = document.getElementById(blockRootId); + + // Find the new active desc for the current tree by trying the following + // possibilities in order: the parent, the next sibling, and the previous + // sibling. + var newActiveDesc = + this.getParentLi_(blockRootElement) || + this.getNextSiblingLi_(blockRootElement) || + this.getPreviousSiblingLi_(blockRootElement); + + this.clearActiveDesc(treeId); + deleteBlockFunc(); + // Invoke a digest cycle, so that the DOM settles. + var that = this; + setTimeout(function() { + that.setActiveDesc(newActiveDesc.id, treeId); + document.getElementById(treeId).focus(); + }); } }, - showBlockOptionsModal: function(block, blockRootNode) { + getTreeIdForBlock: function(blockId) { + // Walk up the DOM until we get to the root element of the tree. + var potentialRoot = document.getElementById(this.getBlockRootId_(blockId)); + while (!this.isTreeRoot_(potentialRoot)) { + potentialRoot = potentialRoot.parentNode; + } + return potentialRoot.id; + }, + // Set focus to the tree containing the given block, and set the tree's + // active desc to the root element of the given block. + focusOnBlock: function(blockId) { + // Invoke a digest cycle, in order to allow the ID of the newly-created + // tree to be set in the DOM. + var that = this; + setTimeout(function() { + var treeId = that.getTreeIdForBlock(blockId); + document.getElementById(treeId).focus(); + that.setActiveDesc(that.getBlockRootId_(blockId), treeId); + }); + }, + showBlockOptionsModal: function(block) { var that = this; var actionButtonsInfo = []; @@ -282,31 +348,39 @@ blocklyApp.TreeService = ng.core.Class({ if (this.blockConnectionService.canBeMovedToMarkedConnection(block)) { actionButtonsInfo.push({ action: function() { - var blockDescription = that.utilsService.getBlockDescription( - block); + var blockDescription = that.utilsService.getBlockDescription(block); var oldDestinationTreeId = that.getTreeIdForBlock( that.blockConnectionService.getMarkedConnectionSourceBlock().id); that.clearActiveDesc(oldDestinationTreeId); var newBlockId = that.blockConnectionService.attachToMarkedConnection( block); - - that.removeBlockAndSetFocus(block, blockRootNode, function() { + that.safelyRemoveBlock_(block, function() { block.dispose(true); }); // Invoke a digest cycle, so that the DOM settles. setTimeout(function() { that.focusOnBlock(newBlockId); - var newDestinationTreeId = that.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.initActiveDesc(oldDestinationTreeId); + // The tree ID for a moved block does not seem to behave + // predictably. E.g. start with two separate groups of one block + // each, add a link before the block in the second group, and + // move the block in the first group to that link. The tree ID of + // the resulting group ends up being the tree ID for the group + // that was originally first, not second as might be expected. + // Here, we double-check to ensure that all affected trees have + // an active desc set. + if (document.getElementById(oldDestinationTreeId) && + !that.getActiveDescId(oldDestinationTreeId)) { + that.initActiveDesc(oldDestinationTreeId); + } + if (document.getElementById(newDestinationTreeId) && + !that.getActiveDescId(newDestinationTreeId)) { + that.initActiveDesc(newDestinationTreeId); + } } that.notificationsService.speak( @@ -323,19 +397,16 @@ blocklyApp.TreeService = ng.core.Class({ action: function() { var blockDescription = that.utilsService.getBlockDescription(block); - that.removeBlockAndSetFocus(block, blockRootNode, function() { + that.safelyRemoveBlock_(block, function() { block.dispose(true); that.audioService.playDeleteSound(); }); setTimeout(function() { - if (that.utilsService.isWorkspaceEmpty()) { - that.notificationsService.speak( - blockDescription + ' deleted. Workspace is empty.'); - } else { - that.notificationsService.speak( - blockDescription + ' deleted. Now on workspace.'); - } + var message = blockDescription + ' deleted. ' + ( + that.utilsService.isWorkspaceEmpty() ? + 'Workspace is empty.' : 'Now on workspace.'); + that.notificationsService.speak(message); }); }, translationIdForText: 'DELETE' @@ -345,24 +416,33 @@ blocklyApp.TreeService = ng.core.Class({ that.focusOnBlock(block.id); }); }, - getBlockRootSuffix_: function() { - return 'blockRoot'; - }, - getCurrentBlockRootNode_: function(activeDesc) { - // Starting from the activeDesc, walk up the tree until we find the - // root of the current block. - var blockRootSuffix = this.getBlockRootSuffix_(); - var putativeBlockRootNode = activeDesc; - while (putativeBlockRootNode.id.indexOf(blockRootSuffix) === -1) { - putativeBlockRootNode = putativeBlockRootNode.parentNode; + + moveUpOneLevel_: function(treeId) { + var activeDesc = document.getElementById(this.getActiveDescId(treeId)); + var nextNode = this.getParentLi_(activeDesc); + if (nextNode) { + this.setActiveDesc(nextNode.id, treeId); + } else { + this.audioService.playOopsSound(); } - return putativeBlockRootNode; }, - getBlockFromRootNode_: function(blockRootNode) { - var blockRootSuffix = this.getBlockRootSuffix_(); - var blockId = blockRootNode.id.substring( - 0, blockRootNode.id.length - blockRootSuffix.length); - return blocklyApp.workspace.getBlockById(blockId); + // Notify the user about the tree they'll land on, if it's within the + // workspace or sidebar. + notifyUserAboutTabDestination_: function(sourceTreeId, shiftKeyIsPressed) { + var targetId = shiftKeyIsPressed ? + this.getPreviousFocusTargetId_(sourceTreeId) : + this.getNextFocusTargetId_(sourceTreeId); + if (targetId) { + var workspaceFocusTargets = this.getWorkspaceFocusTargets_(); + for (var i = 0; i < workspaceFocusTargets.length; i++) { + if (workspaceFocusTargets[i].id == targetId) { + this.notificationsService.speak( + 'Now in workspace group ' + (i + 1) + ' of ' + + workspaceFocusTargets.length); + return; + } + } + } }, onKeypress: function(e, tree) { // TODO(sll): Instead of this, have a common ActiveContextService which @@ -376,7 +456,7 @@ blocklyApp.TreeService = ng.core.Class({ if (!activeDesc) { console.error('ERROR: no active descendant for current tree.'); this.initActiveDesc(treeId); - return; + activeDesc = document.getElementById(this.getActiveDescId(treeId)); } if (e.altKey || e.ctrlKey) { @@ -387,26 +467,19 @@ blocklyApp.TreeService = ng.core.Class({ if (document.activeElement.tagName == 'INPUT') { // For input fields, only Esc and Tab keystrokes are handled specially. if (e.keyCode == 27 || e.keyCode == 9) { - // For Esc and Tab keys, the focus is removed from the input field. - this.focusOnCurrentTree_(treeId); + // Return the focus to the workspace tree containing the input field. + document.getElementById(treeId).focus(); if (e.keyCode == 9) { - var destinationTreeId = - e.shiftKey ? this.getIdOfPreviousTree_(treeId) : - this.getIdOfNextTree_(treeId); - if (destinationTreeId) { - this.notifyUserAboutCurrentTree_(destinationTreeId); - } - } - - // Allow Tab keypresses to go through. - if (e.keyCode == 27) { + // Allow Tab events to propagate through. + this.notifyUserAboutTabDestination_(treeId, e.shiftKey); + } else if (e.keyCode == 27) { e.preventDefault(); e.stopPropagation(); } } } else { - // Outside an input field, Enter, Tab and navigation keys are all + // Outside an input field, Enter, Tab, Esc and navigation keys are all // recognized. if (e.keyCode == 13) { // Enter key. The user wants to interact with a button, interact with @@ -414,12 +487,13 @@ blocklyApp.TreeService = ng.core.Class({ // Algorithm to find the field: do a DFS through the children until // we find an INPUT, BUTTON or SELECT element (in which case we use it). // Truncate the search at child LI elements. + e.stopPropagation(); + var found = false; var dfsStack = Array.from(activeDesc.children); while (dfsStack.length) { var currentNode = dfsStack.shift(); if (currentNode.tagName == 'BUTTON') { - this.moveUpOneLevel_(treeId); currentNode.click(); found = true; break; @@ -427,7 +501,7 @@ blocklyApp.TreeService = ng.core.Class({ currentNode.focus(); currentNode.select(); this.notificationsService.speak( - 'Type a value, then press Escape to exit'); + 'Type a value, then press Escape to exit'); found = true; break; } else if (currentNode.tagName == 'SELECT') { @@ -449,60 +523,53 @@ 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) { - var blockRootNode = this.getCurrentBlockRootNode_(activeDesc); - var block = this.getBlockFromRootNode_(blockRootNode); - - e.stopPropagation(); - this.showBlockOptionsModal(block, blockRootNode); + var block = this.getContainingBlock_(activeDesc); + this.showBlockOptionsModal(block); } } else if (e.keyCode == 9) { - // Tab key. Note that allowing the event to propagate through is - // intentional. - var destinationTreeId = - e.shiftKey ? this.getIdOfPreviousTree_(treeId) : - this.getIdOfNextTree_(treeId); - if (destinationTreeId) { - this.notifyUserAboutCurrentTree_(destinationTreeId); - } - } else if (e.keyCode == 27) { - this.moveUpOneLevel_(treeId); - } else if (e.keyCode >= 35 && e.keyCode <= 40) { - // End, home, and arrow keys. - if (e.keyCode == 35) { + // Tab key. The event is allowed to propagate through. + this.notifyUserAboutTabDestination_(treeId, e.shiftKey); + } else if ([27, 35, 36, 37, 38, 39, 40].indexOf(e.keyCode) !== -1) { + if (e.keyCode == 27 || e.keyCode == 37) { + // Esc or left arrow key. Go up a level, if possible. + this.moveUpOneLevel_(treeId); + } else if (e.keyCode == 35) { // End key. Go to the last sibling in the subtree. - var finalSibling = this.getFinalSibling(activeDesc); - if (finalSibling) { - this.setActiveDesc(finalSibling.id, treeId); + var potentialFinalSibling = this.getFinalSiblingLi_(activeDesc); + if (potentialFinalSibling) { + this.setActiveDesc(potentialFinalSibling.id, treeId); } } else if (e.keyCode == 36) { // Home key. Go to the first sibling in the subtree. - var initialSibling = this.getInitialSibling(activeDesc); - if (initialSibling) { - this.setActiveDesc(initialSibling.id, treeId); + var potentialInitialSibling = this.getInitialSiblingLi_(activeDesc); + if (potentialInitialSibling) { + this.setActiveDesc(potentialInitialSibling.id, treeId); } - } else if (e.keyCode == 37) { - // Left arrow key. Go up a level, if possible. - this.moveUpOneLevel_(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); + var potentialPrevSibling = this.getPreviousSiblingLi_(activeDesc); + if (potentialPrevSibling) { + this.setActiveDesc(potentialPrevSibling.id, treeId); } else { var statusMessage = 'Reached top of list.'; - if (this.getParentListElement_(activeDesc)) { + if (this.getParentLi_(activeDesc)) { statusMessage += ' Press left to go to parent list.'; } this.audioService.playOopsSound(statusMessage); } } else if (e.keyCode == 39) { // Right arrow key. Go down a level, if possible. - this.moveDownOneLevel_(treeId); + var potentialFirstChild = this.getFirstChildLi_(activeDesc); + if (potentialFirstChild) { + this.setActiveDesc(potentialFirstChild.id, treeId); + } else { + this.audioService.playOopsSound(); + } } 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); + var potentialNextSibling = this.getNextSiblingLi_(activeDesc); + if (potentialNextSibling) { + this.setActiveDesc(potentialNextSibling.id, treeId); } else { this.audioService.playOopsSound('Reached bottom of list.'); } @@ -512,144 +579,5 @@ blocklyApp.TreeService = ng.core.Class({ e.stopPropagation(); } } - }, - moveDownOneLevel_: function(treeId) { - var activeDesc = document.getElementById(this.getActiveDescId(treeId)); - var firstChild = this.getFirstChild(activeDesc); - if (firstChild) { - this.setActiveDesc(firstChild.id, treeId); - } else { - this.audioService.playOopsSound(); - } - }, - moveUpOneLevel_: function(treeId) { - var activeDesc = document.getElementById(this.getActiveDescId(treeId)); - var nextNode = this.getParentListElement_(activeDesc); - if (nextNode) { - this.setActiveDesc(nextNode.id, treeId); - } else { - this.audioService.playOopsSound(); - } - }, - getParentListElement_: function(element) { - var nextNode = element.parentNode; - while (nextNode && nextNode.tagName != 'LI') { - nextNode = nextNode.parentNode; - } - return nextNode; - }, - 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; - } - }, - getFinalSibling: function(element) { - while (true) { - var nextSibling = this.getNextSibling(element); - if (nextSibling && nextSibling.id != element.id) { - element = nextSibling; - } else { - return element; - } - } - }, - getInitialSibling: function(element) { - while (true) { - var previousSibling = this.getPreviousSibling(element); - if (previousSibling && previousSibling.id != element.id) { - element = previousSibling; - } else { - return element; - } - } - }, - 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; - } } });