mirror of
https://github.com/google/blockly.git
synced 2026-01-09 01:50:11 +01:00
Add keyboard shortcuts for cut, copy and paste operations.
This commit is contained in:
@@ -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.');
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user