diff --git a/accessible/app.component.js b/accessible/app.component.js index e1c4329d3..42d04608e 100644 --- a/accessible/app.component.js +++ b/accessible/app.component.js @@ -32,16 +32,11 @@ blocklyApp.AppView = ng.core - - - - - -
- {{'TOOLBOX_LOAD'|translate}} - - {{'WORKSPACE_LOAD'|translate}} -
+ +
+ {{'TOOLBOX_LOAD'|translate}} + {{'WORKSPACE_LOAD'|translate}} +
diff --git a/accessible/clipboard.service.js b/accessible/clipboard.service.js index 05b0afe63..9a7e7451a 100644 --- a/accessible/clipboard.service.js +++ b/accessible/clipboard.service.js @@ -106,10 +106,14 @@ blocklyApp.ClipboardService = ng.core }, copy: function(block) { this.clipboardBlockXml_ = Blockly.Xml.blockToDom(block); + Blockly.Xml.deleteNext(this.clipboardBlockXml_); this.clipboardBlockPreviousConnection_ = block.previousConnection; this.clipboardBlockNextConnection_ = block.nextConnection; this.clipboardBlockOutputConnection_ = block.outputConnection; }, + isClipboardEmpty: function() { + return !this.clipboardBlockXml_; + }, pasteFromClipboard: function(inputConnection) { var connection = inputConnection; // If the connection is a 'previousConnection' and that connection is diff --git a/accessible/field.component.js b/accessible/field.component.js index e10323db7..5a5302960 100644 --- a/accessible/field.component.js +++ b/accessible/field.component.js @@ -30,12 +30,12 @@ blocklyApp.FieldComponent = ng.core template: `
@@ -44,7 +44,8 @@ blocklyApp.FieldComponent = ng.core
  1. + [attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap[optionValue + 'Button'], 'blockly-button')" + [attr.aria-level]="level" [attr.aria-selected]="field.getValue() == optionValue"> -
  2. -
  3. - -
  4. -
  5. +
  6. + +
@@ -69,7 +62,7 @@ blocklyApp.ToolboxTreeComponent = ng.core return blocklyApp.ToolboxTreeComponent; })], inputs: [ - 'block', 'displayBlockMenu', 'level', 'index', 'tree', 'noCategories', 'isTopLevel'], + 'block', 'displayBlockMenu', 'level', 'tree', 'isFirstToolboxTree'], pipes: [blocklyApp.TranslatePipe] }) .Class({ @@ -85,19 +78,34 @@ blocklyApp.ToolboxTreeComponent = ng.core this.utilsService = _utilsService; }], ngOnInit: function() { - var elementsNeedingIds = ['blockSummaryLabel']; + var idKeys = ['toolboxBlockRoot', 'blockSummaryLabel']; if (this.displayBlockMenu) { - elementsNeedingIds = elementsNeedingIds.concat(['blockSummarylabel', - 'workspaceCopy', 'workspaceCopyButton', 'blockCopy', - 'blockCopyButton', 'sendToSelected', 'sendToSelectedButton']); + idKeys = idKeys.concat([ + 'workspaceCopy', 'workspaceCopyButton', 'sendToSelected', + 'sendToSelectedButton']); } - this.idMap = this.utilsService.generateIds(elementsNeedingIds); - if (this.isTopLevel) { - this.idMap['parentList'] = 'blockly-toolbox-tree-node0'; - } else { - 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 + // descendant after the ids have been computed. + // Note that a timeout is needed here in order to trigger Angular + // change detection. + if (this.isFirstToolboxTree) { + var that = this; + setTimeout(function() { + that.treeService.setActiveDesc( + that.idMap['toolboxBlockRoot'], 'blockly-toolbox-tree'); + }); + } + }, + isWorkspaceEmpty: function() { + return this.utilsService.isWorkspaceEmpty(); + }, getBlockDescription: function() { return this.utilsService.getBlockDescription(this.block); }, @@ -117,15 +125,10 @@ 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.'); }); }, - copyToClipboard: function() { - this.clipboardService.copy(this.block); - this.notificationsService.setStatusMessage( - this.getBlockDescription() + ' ' + Blockly.Msg.COPIED_BLOCK_MSG); - }, copyToMarkedSpot: function() { var blockDescription = this.getBlockDescription(); // Clean up the active desc for the destination tree. diff --git a/accessible/toolbox.component.js b/accessible/toolbox.component.js index a7d5acd93..fd81d0f43 100644 --- a/accessible/toolbox.component.js +++ b/accessible/toolbox.component.js @@ -27,44 +27,45 @@ blocklyApp.ToolboxComponent = ng.core .Component({ selector: 'blockly-toolbox', template: ` -

Toolbox

-
    - -
    - - -
    -
+
+

Toolbox

+
    + + +
    + + +
    +
+
`, directives: [blocklyApp.ToolboxTreeComponent] }) @@ -73,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; @@ -125,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 ecf5fd8cc..071bc95ea 100644 --- a/accessible/tree.service.js +++ b/accessible/tree.service.js @@ -27,27 +27,65 @@ 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'); }, - getWorkspaceToolbarButtonNodes_: function() { - return Array.from(document.querySelectorAll( - 'button.blocklyWorkspaceToolbarButton')); - }, // Returns a list of all top-level workspace tree nodes on the page. getWorkspaceTreeNodes_: function() { return Array.from(document.querySelectorAll('ol.blocklyWorkspaceTree')); }, + getWorkspaceToolbarButtonNodes_: function() { + 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_()]; - treeNodes = treeNodes.concat(this.getWorkspaceToolbarButtonNodes_()); treeNodes = treeNodes.concat(this.getWorkspaceTreeNodes_()); + treeNodes = treeNodes.concat(this.getWorkspaceToolbarButtonNodes_()); return treeNodes; }, isTopLevelWorkspaceTree: function(treeId) { @@ -239,6 +277,80 @@ 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) { + if (this.clipboardService.isClipboardEmpty()) { + return; + } + + 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 +360,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) { @@ -290,6 +454,7 @@ blocklyApp.TreeService = ng.core break; } else if (currentNode.tagName == 'INPUT') { currentNode.focus(); + currentNode.select(); this.notificationsService.setStatusMessage( 'Type a value, then press Escape to exit'); break; 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 04ec09b12..d6c8fdb23 100644 --- a/accessible/workspace-tree.component.js +++ b/accessible/workspace-tree.component.js @@ -37,7 +37,8 @@ blocklyApp.WorkspaceTreeComponent = ng.core