@@ -85,14 +85,17 @@ blocklyApp.ToolboxTreeComponent = ng.core
this.utilsService = _utilsService;
}],
ngOnInit: function() {
- var elementsNeedingIds = ['blockSummaryLabel'];
+ var idKeys = ['toolboxBlockRoot', 'blockSummaryLabel'];
if (this.displayBlockMenu) {
- elementsNeedingIds = elementsNeedingIds.concat(['blockSummarylabel',
+ idKeys = idKeys.concat([
'workspaceCopy', 'workspaceCopyButton', 'blockCopy',
'blockCopyButton', 'sendToSelected', 'sendToSelectedButton']);
}
- this.idMap = this.utilsService.generateIds(elementsNeedingIds);
- this.idMap['parentList'] = this.utilsService.generateUniqueId();
+
+ this.idMap = {};
+ for (var i = 0; i < idKeys.length; i++) {
+ this.idMap[idKeys[i]] = this.block.id + idKeys[i];
+ }
},
ngAfterViewInit: function() {
// If this is the first tree in the category-less toolbox, set its active
@@ -103,7 +106,7 @@ blocklyApp.ToolboxTreeComponent = ng.core
var that = this;
setTimeout(function() {
that.treeService.setActiveDesc(
- that.idMap['parentList'], 'blockly-toolbox-tree');
+ that.idMap['toolboxBlockRoot'], 'blockly-toolbox-tree');
});
}
},
@@ -129,7 +132,7 @@ blocklyApp.ToolboxTreeComponent = ng.core
setTimeout(function() {
that.treeService.focusOnBlock(newBlockId);
that.notificationsService.setStatusMessage(
- blockDescription + ' copied to workspace. ' +
+ blockDescription + ' copied to new workspace group. ' +
'Now on copied block in workspace.');
});
},
diff --git a/accessible/toolbox.component.js b/accessible/toolbox.component.js
index 6b7e14af3..fd81d0f43 100644
--- a/accessible/toolbox.component.js
+++ b/accessible/toolbox.component.js
@@ -74,7 +74,6 @@ blocklyApp.ToolboxComponent = ng.core
blocklyApp.TreeService, blocklyApp.UtilsService,
function(_treeService, _utilsService) {
this.toolboxCategories = [];
- this.toolboxWorkspaces = Object.create(null);
this.treeService = _treeService;
this.utilsService = _utilsService;
@@ -126,24 +125,6 @@ blocklyApp.ToolboxComponent = ng.core
'Move right to access ' + numBlocks + ' blocks in this category.';
},
getToolboxWorkspace: function(categoryNode) {
- if (categoryNode.attributes && categoryNode.attributes.name) {
- var categoryName = categoryNode.attributes.name.value;
- } else {
- var categoryName = 'no-category';
- }
- if (this.toolboxWorkspaces[categoryName]) {
- return this.toolboxWorkspaces[categoryName];
- } else {
- var categoryWorkspace = new Blockly.Workspace();
- if (categoryName == 'no-category') {
- for (var i = 0; i < categoryNode.length; i++) {
- Blockly.Xml.domToBlock(categoryWorkspace, categoryNode[i]);
- }
- } else {
- Blockly.Xml.domToWorkspace(categoryNode, categoryWorkspace);
- }
- this.toolboxWorkspaces[categoryName] = categoryWorkspace;
- return this.toolboxWorkspaces[categoryName];
- }
+ return this.treeService.getToolboxWorkspace(categoryNode);
}
});
diff --git a/accessible/tree.service.js b/accessible/tree.service.js
index 2ce53dbe2..d303591b7 100644
--- a/accessible/tree.service.js
+++ b/accessible/tree.service.js
@@ -27,10 +27,15 @@
blocklyApp.TreeService = ng.core
.Class({
constructor: [
- blocklyApp.NotificationsService, function(_notificationsService) {
+ blocklyApp.NotificationsService, blocklyApp.UtilsService,
+ blocklyApp.ClipboardService,
+ function(_notificationsService, _utilsService, _clipboardService) {
// Stores active descendant ids for each tree in the page.
this.activeDescendantIds_ = {};
this.notificationsService = _notificationsService;
+ this.utilsService = _utilsService;
+ this.clipboardService = _clipboardService;
+ this.toolboxWorkspaces = {};
}],
getToolboxTreeNode_: function() {
return document.getElementById('blockly-toolbox-tree');
@@ -43,6 +48,39 @@ blocklyApp.TreeService = ng.core
return Array.from(document.querySelectorAll(
'button.blocklyWorkspaceToolbarButton'));
},
+ getToolboxWorkspace: function(categoryNode) {
+ if (categoryNode.attributes && categoryNode.attributes.name) {
+ var categoryName = categoryNode.attributes.name.value;
+ } else {
+ var categoryName = 'no-category';
+ }
+
+ if (this.toolboxWorkspaces.hasOwnProperty(categoryName)) {
+ return this.toolboxWorkspaces[categoryName];
+ } else {
+ var categoryWorkspace = new Blockly.Workspace();
+ if (categoryName == 'no-category') {
+ for (var i = 0; i < categoryNode.length; i++) {
+ Blockly.Xml.domToBlock(categoryWorkspace, categoryNode[i]);
+ }
+ } else {
+ Blockly.Xml.domToWorkspace(categoryNode, categoryWorkspace);
+ }
+
+ this.toolboxWorkspaces[categoryName] = categoryWorkspace;
+ return this.toolboxWorkspaces[categoryName];
+ }
+ },
+ getToolboxBlockById: function(blockId) {
+ for (var categoryName in this.toolboxWorkspaces) {
+ var putativeBlock = this.utilsService.getBlockByIdFromWorkspace(
+ blockId, this.toolboxWorkspaces[categoryName]);
+ if (putativeBlock) {
+ return putativeBlock;
+ }
+ }
+ return null;
+ },
// Returns a list of all top-level tree nodes on the page.
getAllTreeNodes_: function() {
var treeNodes = [this.getToolboxTreeNode_()];
@@ -239,6 +277,77 @@ blocklyApp.TreeService = ng.core
}
}
},
+ 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;
+ },
+ 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.
+ var treeId = this.getTreeIdForBlock(block.id);
+ if (this.isIsolatedTopLevelBlock_(block)) {
+ var nextNodeToFocusOn = this.getNodeToFocusOnWhenTreeIsDeleted(treeId);
+
+ this.clearActiveDesc(treeId);
+ deleteBlockFunc();
+ // Invoke a digest cycle, so that the DOM settles.
+ setTimeout(function() {
+ nextNodeToFocusOn.focus();
+ });
+ } else {
+ var nextActiveDesc = this.getNextActiveDescWhenBlockIsDeleted(
+ blockRootNode);
+ this.runWhilePreservingFocus(
+ deleteBlockFunc, treeId, nextActiveDesc.id);
+ }
+ },
+ cutBlock_: function(block, blockRootNode) {
+ var blockDescription = this.utilsService.getBlockDescription(block);
+
+ var that = this;
+ this.removeBlockAndSetFocus(block, blockRootNode, function() {
+ that.clipboardService.cut(block);
+ });
+
+ setTimeout(function() {
+ if (that.utilsService.isWorkspaceEmpty()) {
+ that.notificationsService.setStatusMessage(
+ blockDescription + ' cut. Workspace is empty.');
+ } else {
+ that.notificationsService.setStatusMessage(
+ blockDescription + ' cut. Now on workspace.');
+ }
+ });
+ },
+ copyBlock_: function(block) {
+ var blockDescription = this.utilsService.getBlockDescription(block);
+ this.clipboardService.copy(block);
+ this.notificationsService.setStatusMessage(
+ blockDescription + ' ' + Blockly.Msg.COPIED_BLOCK_MSG);
+ },
+ pasteToConnection_: function(block, connection) {
+ var destinationTreeId = this.getTreeIdForBlock(
+ connection.getSourceBlock().id);
+ this.clearActiveDesc(destinationTreeId);
+
+ var newBlockId = this.clipboardService.pasteFromClipboard(connection);
+
+ // Invoke a digest cycle, so that the DOM settles.
+ var that = this;
+ setTimeout(function() {
+ that.focusOnBlock(newBlockId);
+ });
+ },
onKeypress: function(e, tree) {
var treeId = tree.id;
var activeDesc = document.getElementById(this.getActiveDescId(treeId));
@@ -248,9 +357,61 @@ blocklyApp.TreeService = ng.core
return;
}
- if (e.altKey || e.ctrlKey) {
+ if (e.altKey) {
// Do not intercept combinations such as Alt+Home.
return;
+ }
+
+ if (e.ctrlKey) {
+ var activeDesc = document.getElementById(this.getActiveDescId(treeId));
+
+ // Scout up the tree to see whether we're in the toolbox or workspace.
+ var scoutNode = activeDesc;
+ var TARGET_TAG_NAMES = ['BLOCKLY-TOOLBOX', 'BLOCKLY-WORKSPACE'];
+ while (TARGET_TAG_NAMES.indexOf(scoutNode.tagName) === -1) {
+ scoutNode = scoutNode.parentNode;
+ }
+ var inToolbox = (scoutNode.tagName == 'BLOCKLY-TOOLBOX');
+
+ // Disallow cutting and pasting in the toolbox.
+ if (inToolbox && e.keyCode != 67) {
+ if (e.keyCode == 86) {
+ this.notificationsService.setStatusMessage(
+ 'Cannot paste block in toolbox.');
+ } else if (e.keyCode == 88) {
+ this.notificationsService.setStatusMessage(
+ 'Cannot cut block in toolbox. Try copying instead.');
+ }
+ }
+
+ // Starting from the activeDesc, walk up the tree until we find the
+ // root of the current block.
+ var blockRootSuffix = inToolbox ? 'toolboxBlockRoot' : 'blockRoot';
+ var putativeBlockRootNode = activeDesc;
+ while (putativeBlockRootNode.id.indexOf(blockRootSuffix) === -1) {
+ putativeBlockRootNode = putativeBlockRootNode.parentNode;
+ }
+ var blockRootNode = putativeBlockRootNode;
+
+ var blockId = blockRootNode.id.substring(
+ 0, blockRootNode.id.length - blockRootSuffix.length);
+ var block = inToolbox ?
+ this.getToolboxBlockById(blockId) :
+ this.utilsService.getBlockById(blockId);
+
+ if (e.keyCode == 88) {
+ // Cut block.
+ this.cutBlock_(block, blockRootNode);
+ } else if (e.keyCode == 67) {
+ // Copy block. Note that, in this case, we might be in the workspace
+ // or toolbox.
+ this.copyBlock_(block);
+ } else if (e.keyCode == 86) {
+ // Paste block, if possible.
+ var targetConnection =
+ e.shiftKey ? block.previousConnection : block.nextConnection;
+ this.pasteToConnection_(block, targetConnection);
+ }
} else if (document.activeElement.tagName == 'INPUT') {
// For input fields, only Esc and Tab keystrokes are handled specially.
if (e.keyCode == 27 || e.keyCode == 9) {
diff --git a/accessible/utils.service.js b/accessible/utils.service.js
index 3a7eef797..160f90ad6 100644
--- a/accessible/utils.service.js
+++ b/accessible/utils.service.js
@@ -70,5 +70,13 @@ blocklyApp.UtilsService = ng.core
},
isWorkspaceEmpty: function() {
return !blocklyApp.workspace.topBlocks_.length;
+ },
+ getBlockById: function(blockId) {
+ return this.getBlockByIdFromWorkspace(blockId, blocklyApp.workspace);
+ },
+ getBlockByIdFromWorkspace: function(blockId, workspace) {
+ // This is used for non-default workspaces, such as those comprising the
+ // toolbox.
+ return workspace.getBlockById(blockId);
}
});
diff --git a/accessible/workspace-tree.component.js b/accessible/workspace-tree.component.js
index 8aa2e65eb..eb8361ff1 100644
--- a/accessible/workspace-tree.component.js
+++ b/accessible/workspace-tree.component.js
@@ -48,8 +48,7 @@ blocklyApp.WorkspaceTreeComponent = ng.core
+ [attr.aria-level]="level + 1">
@@ -115,40 +114,10 @@ blocklyApp.WorkspaceTreeComponent = ng.core
getBlockDescription: function() {
return this.utilsService.getBlockDescription(this.block);
},
- 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;
- },
removeBlockAndSetFocus_: function(block, deleteBlockFunc) {
- // This method runs the given 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.
- if (this.isIsolatedTopLevelBlock_(block)) {
- var nextNodeToFocusOn =
- this.treeService.getNodeToFocusOnWhenTreeIsDeleted(this.tree.id);
-
- this.treeService.clearActiveDesc(this.tree.id);
- deleteBlockFunc();
- // Invoke a digest cycle, so that the DOM settles.
- setTimeout(function() {
- nextNodeToFocusOn.focus();
- });
- } else {
- var blockRootNode = document.getElementById(this.idMap['blockRoot']);
- var nextActiveDesc =
- this.treeService.getNextActiveDescWhenBlockIsDeleted(
- blockRootNode);
- this.treeService.runWhilePreservingFocus(
- deleteBlockFunc, this.tree.id, nextActiveDesc.id);
- }
+ this.treeService.removeBlockAndSetFocus(
+ block, document.getElementById(this.idMap['blockRoot']),
+ deleteBlockFunc);
},
cutBlock_: function() {
var blockDescription = this.getBlockDescription();