Add keyboard shortcuts for cut, copy and paste operations.

This commit is contained in:
Sean Lip
2016-09-19 15:45:46 -07:00
parent 791a9a2919
commit ee403c6a95
5 changed files with 186 additions and 64 deletions

View File

@@ -28,7 +28,7 @@ blocklyApp.ToolboxTreeComponent = ng.core
.Component({
selector: 'blockly-toolbox-tree',
template: `
<li #parentList [id]="idMap['parentList']" role="treeitem"
<li [id]="idMap['toolboxBlockRoot']" role="treeitem"
[ngClass]="{blocklyHasChildren: displayBlockMenu}"
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['blockSummaryLabel'], 'blockly-toolbox-block')"
[attr.aria-level]="level">
@@ -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.');
});
},

View File

@@ -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);
}
});

View File

@@ -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) {

View File

@@ -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);
}
});

View File

@@ -48,8 +48,7 @@ blocklyApp.WorkspaceTreeComponent = ng.core
<li #inputList [id]="idMap['inputList' + i]" role="treeitem"
*ngIf="inputBlock.connection && !inputBlock.connection.targetBlock()"
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['inputMenuLabel' + i], 'blockly-submenu-indicator')"
[attr.aria-level]="level + 1"
(keydown)="treeService.onKeypress($event, tree)">
[attr.aria-level]="level + 1">
<label [id]="idMap['inputMenuLabel' + i]">
{{utilsService.getInputTypeLabel(inputBlock.connection)}} {{utilsService.getBlockTypeLabel(inputBlock)}} needed:
</label>
@@ -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();