diff --git a/accessible/toolbox-tree.component.js b/accessible/toolbox-tree.component.js index 15a0df07c..be14f33ff 100644 --- a/accessible/toolbox-tree.component.js +++ b/accessible/toolbox-tree.component.js @@ -28,7 +28,7 @@ blocklyApp.ToolboxTreeComponent = ng.core .Component({ selector: 'blockly-toolbox-tree', template: ` -
  • @@ -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();