mirror of
https://github.com/google/blockly.git
synced 2026-01-09 10:00:09 +01:00
Rewrite tree.service.js.
- Remove unnecessary code and functions. - Add documentation where needed. - Fix a bug arising when a block on the workspace is attached to an existing link.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user