mirror of
https://github.com/google/blockly.git
synced 2026-01-07 00:50:27 +01:00
as an error, and turning off keypresses on the workspace when the variable modals are open.
610 lines
22 KiB
JavaScript
610 lines
22 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 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)
|
|
*/
|
|
|
|
goog.provide('blocklyApp.TreeService');
|
|
|
|
goog.require('blocklyApp.UtilsService');
|
|
|
|
goog.require('blocklyApp.AudioService');
|
|
goog.require('blocklyApp.BlockConnectionService');
|
|
goog.require('blocklyApp.BlockOptionsModalService');
|
|
goog.require('blocklyApp.NotificationsService');
|
|
goog.require('blocklyApp.VariableModalService');
|
|
|
|
|
|
blocklyApp.TreeService = ng.core.Class({
|
|
constructor: [
|
|
blocklyApp.AudioService,
|
|
blocklyApp.BlockConnectionService,
|
|
blocklyApp.BlockOptionsModalService,
|
|
blocklyApp.NotificationsService,
|
|
blocklyApp.UtilsService,
|
|
blocklyApp.VariableModalService,
|
|
function(
|
|
audioService, blockConnectionService, blockOptionsModalService,
|
|
notificationsService, utilsService, variableModalService) {
|
|
this.audioService = audioService;
|
|
this.blockConnectionService = blockConnectionService;
|
|
this.blockOptionsModalService = blockOptionsModalService;
|
|
this.notificationsService = notificationsService;
|
|
this.utilsService = utilsService;
|
|
this.variableModalService = variableModalService;
|
|
|
|
// The suffix used for all IDs of block root elements.
|
|
this.BLOCK_ROOT_ID_SUFFIX_ = blocklyApp.BLOCK_ROOT_ID_SUFFIX;
|
|
// 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'));
|
|
}
|
|
],
|
|
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);
|
|
}
|
|
},
|
|
|
|
isLi_: function(node) {
|
|
return node.tagName == 'LI';
|
|
},
|
|
getParentLi_: function(element) {
|
|
var nextNode = element.parentNode;
|
|
while (nextNode && !this.isLi_(nextNode)) {
|
|
nextNode = nextNode.parentNode;
|
|
}
|
|
return nextNode;
|
|
},
|
|
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;
|
|
},
|
|
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;
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
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;
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
getActiveDescId: function(treeId) {
|
|
return this.activeDescendantIds_[treeId] || '';
|
|
},
|
|
// 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);
|
|
},
|
|
// Make a given element the active descendant of a given tree.
|
|
setActiveDesc: function(newActiveDescId, treeId) {
|
|
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.
|
|
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');
|
|
}
|
|
|
|
if (this.activeDescendantIds_[treeId]) {
|
|
delete this.activeDescendantIds_[treeId];
|
|
}
|
|
},
|
|
clearAllActiveDescs: function() {
|
|
for (var treeId in this.activeDescendantIds_) {
|
|
var activeDesc = document.getElementById(this.getActiveDescId(treeId));
|
|
if (activeDesc) {
|
|
activeDesc.classList.remove('blocklyActiveDescendant');
|
|
}
|
|
}
|
|
|
|
this.activeDescendantIds_ = {};
|
|
},
|
|
|
|
isTreeRoot_: function(element) {
|
|
return element.classList.contains('blocklyTree');
|
|
},
|
|
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;
|
|
}
|
|
|
|
var blockRootId = potentialBlockRoot.id;
|
|
var blockId = blockRootId.substring(
|
|
0, blockRootId.length - this.BLOCK_ROOT_ID_SUFFIX_.length);
|
|
return blocklyApp.workspace.getBlockById(blockId);
|
|
},
|
|
isTopLevelBlock_: function(block) {
|
|
return !block.getParent();
|
|
},
|
|
// Returns whether the given block is at the top level, and has no siblings.
|
|
isIsolatedTopLevelBlock_: function(block) {
|
|
var blockHasNoSiblings = (
|
|
(!block.nextConnection ||
|
|
!block.nextConnection.targetConnection) &&
|
|
(!block.previousConnection ||
|
|
!block.previousConnection.targetConnection));
|
|
return this.isTopLevelBlock_(block) && blockHasNoSiblings;
|
|
},
|
|
safelyRemoveBlock_: function(block, deleteBlockFunc, areNextBlocksRemoved) {
|
|
// Runs the given deleteBlockFunc (which should have the effect of deleting
|
|
// the given block, and possibly others after it if `areNextBlocksRemoved`
|
|
// is true) and then does one of two things:
|
|
// - If the deleted block was an isolated top-level block, or it is a top-
|
|
// level block and the next blocks are going to be removed, 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);
|
|
|
|
var treeCeasesToExist = areNextBlocksRemoved ?
|
|
this.isTopLevelBlock_(block) : this.isIsolatedTopLevelBlock_(block);
|
|
|
|
if (treeCeasesToExist) {
|
|
// Find the node to focus on after the deletion happens.
|
|
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) {
|
|
nextElementToFocusOn = focusTargets[i + 1];
|
|
} else if (i > 0) {
|
|
nextElementToFocusOn = focusTargets[i - 1];
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
this.clearActiveDesc(treeId);
|
|
deleteBlockFunc();
|
|
// Invoke a digest cycle, so that the DOM settles (and the "Create new
|
|
// group" button in the workspace shows up, if applicable).
|
|
setTimeout(function() {
|
|
if (nextElementToFocusOn) {
|
|
nextElementToFocusOn.focus();
|
|
} else {
|
|
document.getElementById(
|
|
blocklyApp.ID_FOR_EMPTY_WORKSPACE_BTN).focus();
|
|
}
|
|
});
|
|
} else {
|
|
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. (If `areNextBlocksRemoved` is true, the next sibling would be
|
|
// moved together with the moved block, so we don't check it.)
|
|
if (areNextBlocksRemoved) {
|
|
var newActiveDesc =
|
|
this.getParentLi_(blockRootElement) ||
|
|
this.getPreviousSiblingLi_(blockRootElement);
|
|
} else {
|
|
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();
|
|
});
|
|
}
|
|
},
|
|
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 = [];
|
|
|
|
if (block.previousConnection) {
|
|
actionButtonsInfo.push({
|
|
action: function() {
|
|
that.blockConnectionService.markConnection(block.previousConnection);
|
|
that.focusOnBlock(block.id);
|
|
},
|
|
translationIdForText: 'MARK_SPOT_BEFORE'
|
|
});
|
|
}
|
|
|
|
if (block.nextConnection) {
|
|
actionButtonsInfo.push({
|
|
action: function() {
|
|
that.blockConnectionService.markConnection(block.nextConnection);
|
|
that.focusOnBlock(block.id);
|
|
},
|
|
translationIdForText: 'MARK_SPOT_AFTER'
|
|
});
|
|
}
|
|
|
|
if (this.blockConnectionService.canBeMovedToMarkedConnection(block)) {
|
|
actionButtonsInfo.push({
|
|
action: function() {
|
|
var blockDescription = that.utilsService.getBlockDescription(block);
|
|
var oldDestinationTreeId = that.getTreeIdForBlock(
|
|
that.blockConnectionService.getMarkedConnectionSourceBlock().id);
|
|
that.clearActiveDesc(oldDestinationTreeId);
|
|
|
|
var newBlockId = that.blockConnectionService.attachToMarkedConnection(
|
|
block);
|
|
that.safelyRemoveBlock_(block, function() {
|
|
block.dispose(false);
|
|
}, true);
|
|
|
|
// Invoke a digest cycle, so that the DOM settles.
|
|
setTimeout(function() {
|
|
that.focusOnBlock(newBlockId);
|
|
var newDestinationTreeId = that.getTreeIdForBlock(newBlockId);
|
|
|
|
if (newDestinationTreeId != 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)) {
|
|
var activeDescId = that.getActiveDescId(oldDestinationTreeId);
|
|
var activeDescTreeId = null;
|
|
if (activeDescId) {
|
|
var oldDestinationBlock = that.getContainingBlock_(
|
|
document.getElementById(activeDescId));
|
|
activeDescTreeId = that.getTreeIdForBlock(
|
|
oldDestinationBlock);
|
|
if (activeDescTreeId != oldDestinationTreeId) {
|
|
that.clearActiveDesc(oldDestinationTreeId);
|
|
}
|
|
}
|
|
that.initActiveDesc(oldDestinationTreeId);
|
|
}
|
|
}
|
|
|
|
that.notificationsService.speak(
|
|
blockDescription + ' ' +
|
|
Blockly.Msg.ATTACHED_BLOCK_TO_LINK_MSG +
|
|
'. Now on attached block in workspace.');
|
|
});
|
|
},
|
|
translationIdForText: 'MOVE_TO_MARKED_SPOT'
|
|
});
|
|
}
|
|
|
|
actionButtonsInfo.push({
|
|
action: function() {
|
|
var blockDescription = that.utilsService.getBlockDescription(block);
|
|
|
|
that.safelyRemoveBlock_(block, function() {
|
|
block.dispose(true);
|
|
that.audioService.playDeleteSound();
|
|
}, false);
|
|
|
|
setTimeout(function() {
|
|
var message = blockDescription + ' deleted. ' + (
|
|
that.utilsService.isWorkspaceEmpty() ?
|
|
'Workspace is empty.' : 'Now on workspace.');
|
|
that.notificationsService.speak(message);
|
|
});
|
|
},
|
|
translationIdForText: 'DELETE'
|
|
});
|
|
|
|
this.blockOptionsModalService.showModal(actionButtonsInfo, function() {
|
|
that.focusOnBlock(block.id);
|
|
});
|
|
},
|
|
|
|
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();
|
|
}
|
|
},
|
|
onKeypress: function(e, tree) {
|
|
// TODO(sll): Instead of this, have a common ActiveContextService which
|
|
// returns true if at least one modal is shown, and false otherwise.
|
|
if (this.blockOptionsModalService.isModalShown() ||
|
|
this.variableModalService.isModalShown()) {
|
|
return;
|
|
}
|
|
|
|
var treeId = tree.id;
|
|
var activeDesc = document.getElementById(this.getActiveDescId(treeId));
|
|
if (!activeDesc) {
|
|
// The underlying Blockly instance may have decided blocks needed to
|
|
// be deleted. This is not necessarily an error, but needs to be repaired.
|
|
this.initActiveDesc(treeId);
|
|
activeDesc = document.getElementById(this.getActiveDescId(treeId));
|
|
}
|
|
|
|
if (e.altKey || e.ctrlKey) {
|
|
// Do not intercept combinations such as Alt+Home.
|
|
return;
|
|
}
|
|
|
|
if (document.activeElement.tagName == 'INPUT' ||
|
|
document.activeElement.tagName == 'SELECT') {
|
|
// For input fields, Esc, Enter, and Tab keystrokes are handled specially.
|
|
if (e.keyCode == 9 || e.keyCode == 13 || e.keyCode == 27) {
|
|
// Return the focus to the workspace tree containing the input field.
|
|
document.getElementById(treeId).focus();
|
|
|
|
// Note that Tab and Enter events stop propagating, this behavior is
|
|
// handled on other listeners.
|
|
if (e.keyCode == 27 || e.keyCode == 13) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
}
|
|
} else {
|
|
// 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
|
|
// an input field, or open the block options modal.
|
|
// 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') {
|
|
currentNode.click();
|
|
found = true;
|
|
break;
|
|
} else if (currentNode.tagName == 'INPUT') {
|
|
currentNode.focus();
|
|
currentNode.select();
|
|
this.notificationsService.speak(
|
|
'Type a value, then press Escape to exit');
|
|
found = true;
|
|
break;
|
|
} else if (currentNode.tagName == 'SELECT') {
|
|
currentNode.focus();
|
|
found = true;
|
|
return;
|
|
} else if (currentNode.tagName == 'LI') {
|
|
continue;
|
|
}
|
|
|
|
if (currentNode.children) {
|
|
var reversedChildren = Array.from(currentNode.children).reverse();
|
|
reversedChildren.forEach(function(childNode) {
|
|
dfsStack.unshift(childNode);
|
|
});
|
|
}
|
|
}
|
|
|
|
// If we cannot find a field to interact with, we open the modal for
|
|
// the current block instead.
|
|
if (!found) {
|
|
var block = this.getContainingBlock_(activeDesc);
|
|
this.showBlockOptionsModal(block);
|
|
}
|
|
} else if (e.keyCode == 9) {
|
|
// Tab key. The event is allowed to propagate through.
|
|
} 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 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 potentialInitialSibling = this.getInitialSiblingLi_(activeDesc);
|
|
if (potentialInitialSibling) {
|
|
this.setActiveDesc(potentialInitialSibling.id, treeId);
|
|
}
|
|
} else if (e.keyCode == 38) {
|
|
// Up arrow key. Go to the previous sibling, if possible.
|
|
var potentialPrevSibling = this.getPreviousSiblingLi_(activeDesc);
|
|
if (potentialPrevSibling) {
|
|
this.setActiveDesc(potentialPrevSibling.id, treeId);
|
|
} else {
|
|
var statusMessage = 'Reached top of list.';
|
|
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.
|
|
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 potentialNextSibling = this.getNextSiblingLi_(activeDesc);
|
|
if (potentialNextSibling) {
|
|
this.setActiveDesc(potentialNextSibling.id, treeId);
|
|
} else {
|
|
this.audioService.playOopsSound('Reached bottom of list.');
|
|
}
|
|
}
|
|
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
}
|
|
}
|
|
});
|