mirror of
https://github.com/google/blockly.git
synced 2026-01-09 18:10:08 +01:00
Refactor workspace-tree: define the action buttons declaratively; simplify initial lifecycle code.
This commit is contained in:
@@ -45,7 +45,7 @@ blocklyApp.ClipboardService = ng.core
|
||||
this.areConnectionsCompatible_(connection, superiorConnection) ||
|
||||
this.areConnectionsCompatible_(connection, nextConnection));
|
||||
},
|
||||
canBeMovedToMarkedConnection: function(block) {
|
||||
isMovableToMarkedConnection: function(block) {
|
||||
// It should not be possible to move a block to one of its own
|
||||
// connections.
|
||||
if (this.markedConnection_ &&
|
||||
|
||||
@@ -186,11 +186,17 @@ blocklyApp.TreeService = ng.core
|
||||
var activeDesc = document.getElementById(this.getActiveDescId(treeId));
|
||||
if (!activeDesc) {
|
||||
console.error('ERROR: no active descendant for current tree.');
|
||||
// TODO(sll): This just gives the focus somewhere to go in the event
|
||||
// of an error, but we need to generalize this to other trees (both
|
||||
// within and outside the workspace).
|
||||
this.setActiveDesc(
|
||||
blocklyApp.workspace.topBlocks_[0].id + 'blockSummary', treeId);
|
||||
|
||||
// TODO(sll): Generalize this to other trees (outside the workspace).
|
||||
var workspaceTreeNodes = this.getWorkspaceTreeNodes_();
|
||||
for (var i = 0; i < workspaceTreeNodes.length; i++) {
|
||||
if (workspaceTreeNodes[i].id == treeId) {
|
||||
// Set the active desc to the first child in this tree.
|
||||
this.setActiveDesc(
|
||||
this.getFirstChild(workspaceTreeNodes[i]).id, treeId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,69 +28,29 @@ blocklyApp.WorkspaceTreeComponent = ng.core
|
||||
.Component({
|
||||
selector: 'blockly-workspace-tree',
|
||||
template: `
|
||||
<li #parentList [id]="idMap['parentList']" role="treeitem" class="blocklyHasChildren"
|
||||
<li [id]="idMap['blockRoot']" role="treeitem" class="blocklyHasChildren"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr('blockly-block-summary', idMap['blockSummary'])"
|
||||
[attr.aria-level]="level" aria-selected=false>
|
||||
[attr.aria-level]="level" aria-selected="false">
|
||||
<label [id]="idMap['blockSummary']">{{block.toString()}}</label>
|
||||
<ol role="group" [attr.aria-level]="level+1">
|
||||
|
||||
<ol role="group" [attr.aria-level]="level + 1">
|
||||
<li [id]="idMap['listItem']" class="blocklyHasChildren" role="treeitem"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr('blockly-block-menu', idMap['blockSummary'])"
|
||||
[attr.aria-level]="level+1" aria-selected=false>
|
||||
[attr.aria-level]="level+1" aria-selected="false">
|
||||
<label [id]="idMap['label']">{{'BLOCK_ACTION_LIST'|translate}}</label>
|
||||
<ol role="group" [attr.aria-level]="level+2">
|
||||
<li [id]="idMap['cutListItem']" role="treeitem"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['cutButton'], 'blockly-button')"
|
||||
[attr.aria-level]="level+2" aria-selected=false>
|
||||
<button [id]="idMap['cutButton']" (click)="cutToClipboard(block)">{{'CUT_BLOCK'|translate}}</button>
|
||||
</li>
|
||||
<li [id]="idMap['copyListItem']" role="treeitem"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['copyButton'], 'blockly-button')"
|
||||
[attr.aria-level]="level+2" aria-selected=false>
|
||||
<button [id]="idMap['copyButton']" (click)="clipboardService.copy(block, true)">{{'COPY_BLOCK'|translate}}</button>
|
||||
</li>
|
||||
<li [id]="idMap['pasteBelow']" role="treeitem"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['pasteBelowButton'], 'blockly-button', !hasNextConnection(block) || !isCompatibleWithClipboard(block.nextConnection))"
|
||||
[attr.aria-level]="level+2" aria-selected=false>
|
||||
<button [id]="idMap['pasteBelowButton']" (click)="pasteBelow(block)"
|
||||
[disabled]="!hasNextConnection(block) || !isCompatibleWithClipboard(block.nextConnection)">
|
||||
{{'PASTE_BELOW'|translate}}
|
||||
<ol role="group" [attr.aria-level]="level + 2">
|
||||
<li *ngFor="#buttonInfo of actionButtonsInfo"
|
||||
[id]="idMap[buttonInfo.baseIdKey]" role="treeitem"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap[buttonInfo.baseIdKey + 'Button'], 'blockly-button', buttonInfo.isDisabled())"
|
||||
[attr.aria-level]="level + 2" aria-selected="false">
|
||||
<button [id]="idMap[buttonInfo.baseIdKey + 'Button']" (click)="buttonInfo.action()"
|
||||
[disabled]="buttonInfo.isDisabled()">
|
||||
{{buttonInfo.translationIdForText|translate}}
|
||||
</button>
|
||||
</li>
|
||||
<li [id]="idMap['pasteAbove']" role="treeitem"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['pasteAboveButton'], 'blockly-button', !hasPreviousConnection(block) || !isCompatibleWithClipboard(block.previousConnection))"
|
||||
[attr.aria-level]="level+2" aria-selected=false>
|
||||
<button [id]="idMap['pasteAboveButton']" (click)="pasteAbove(block)"
|
||||
[disabled]="!hasPreviousConnection(block) || !isCompatibleWithClipboard(block.previousConnection)">
|
||||
{{'PASTE_ABOVE'|translate}}
|
||||
</button>
|
||||
</li>
|
||||
<li [id]="idMap['markBelow']" role="treeitem"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['markBelowButton'], 'blockly-button', !hasNextConnection(block))"
|
||||
[attr.aria-level]="level+2" aria-selected=false>
|
||||
<button [id]="idMap['markBelowButton']" (click)="clipboardService.markConnection(block.nextConnection)"
|
||||
[disabled]="!hasNextConnection(block)">
|
||||
{{'MARK_SPOT_BELOW'|translate}}
|
||||
</button>
|
||||
</li>
|
||||
<li [id]="idMap['markAbove']" role="treeitem"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['markAboveButton'], 'blockly-button', !hasPreviousConnection(block))"
|
||||
[attr.aria-level]="level+2" aria-selected=false>
|
||||
<button [id]="idMap['markAboveButton']" (click)="clipboardService.markConnection(block.previousConnection)"
|
||||
[disabled]="!hasPreviousConnection(block)">{{'MARK_SPOT_ABOVE'|translate}}</button>
|
||||
</li>
|
||||
<li [id]="idMap['sendToSelectedListItem']" role="treeitem"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['sendToSelectedButton'], 'blockly-button', !canBeMovedToMarkedConnection(block))"
|
||||
[attr.aria-level]="level+2" aria-selected=false>
|
||||
<button [id]="idMap['sendToSelectedButton']" (click)="sendToMarkedSpot(block)"
|
||||
[disabled]="!canBeMovedToMarkedConnection(block)">{{'MOVE_TO_MARKED_SPOT'|translate}}</button>
|
||||
</li>
|
||||
<li [id]="idMap['delete']" role="treeitem"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['deleteButton'], 'blockly-button')"
|
||||
[attr.aria-level]="level+2" aria-selected=false>
|
||||
<button [id]="idMap['deleteButton']" (click)="deleteBlock(block)">{{'DELETE'|translate}}</button>
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
|
||||
<div *ngFor="#inputBlock of block.inputList; #i = index">
|
||||
<blockly-field *ngFor="#field of inputBlock.fieldRow" [field]="field"></blockly-field>
|
||||
<blockly-workspace-tree *ngIf="inputBlock.connection && inputBlock.connection.targetBlock()"
|
||||
@@ -102,7 +62,7 @@ blocklyApp.WorkspaceTreeComponent = ng.core
|
||||
*ngIf="inputBlock.connection && !inputBlock.connection.targetBlock()" (keydown)="treeService.onKeypress($event, tree)">
|
||||
<!-- TODO(madeeha): i18n here will need to happen in a different way due to the way grammar changes based on language. -->
|
||||
<label [id]="idMap['inputMenuLabel' + i]"> {{utilsService.getInputTypeLabel(inputBlock.connection)}} {{utilsService.getBlockTypeLabel(inputBlock)}} needed: </label>
|
||||
<ol role="group" [attr.aria-level]="level+2">
|
||||
<ol role="group" [attr.aria-level]="level + 2">
|
||||
<li [id]="idMap['markSpot' + i]" role="treeitem"
|
||||
[attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['markButton' + i], 'blockly-button')"
|
||||
[attr.aria-level]="level + 2" aria-selected=false>
|
||||
@@ -135,70 +95,15 @@ blocklyApp.WorkspaceTreeComponent = ng.core
|
||||
})
|
||||
.Class({
|
||||
constructor: [
|
||||
blocklyApp.ClipboardService, blocklyApp.TreeService, blocklyApp.UtilsService,
|
||||
blocklyApp.ClipboardService, blocklyApp.TreeService,
|
||||
blocklyApp.UtilsService,
|
||||
function(_clipboardService, _treeService, _utilsService) {
|
||||
this.infoBlocks = Object.create(null);
|
||||
this.clipboardService = _clipboardService;
|
||||
this.treeService = _treeService;
|
||||
this.utilsService = _utilsService;
|
||||
}],
|
||||
getElementsNeedingIds_: function() {
|
||||
var elementsNeedingIds = ['blockSummary', 'listItem', 'label',
|
||||
'cutListItem', 'cutButton', 'copyListItem', 'copyButton',
|
||||
'pasteBelow', 'pasteBelowButton', 'pasteAbove', 'pasteAboveButton',
|
||||
'markBelow', 'markBelowButton', 'markAbove', 'markAboveButton',
|
||||
'sendToSelectedListItem', 'sendToSelectedButton', 'delete',
|
||||
'deleteButton'];
|
||||
|
||||
for (var i = 0; i < this.block.inputList.length; i++) {
|
||||
var inputBlock = this.block.inputList[i];
|
||||
if (inputBlock.connection && !inputBlock.connection.targetBlock()) {
|
||||
elementsNeedingIds = elementsNeedingIds.concat(
|
||||
['inputList' + i, 'inputMenuLabel' + i, 'markSpot' + i,
|
||||
'markSpotButton' + i, 'paste' + i, 'pasteButton' + i]);
|
||||
}
|
||||
}
|
||||
|
||||
return elementsNeedingIds;
|
||||
},
|
||||
ngOnChanges: function() {
|
||||
// The ids needs to be generated on every change (as opposed to only on
|
||||
// init), so that they will update if, e.g., a new workspace-tree
|
||||
// component is added to the middle of an array.
|
||||
var elementsNeedingIds = this.getElementsNeedingIds_();
|
||||
|
||||
this.idMap = {}
|
||||
this.idMap['parentList'] = this.block.id + 'parent';
|
||||
for (var i = 0; i < elementsNeedingIds.length; i++) {
|
||||
this.idMap[elementsNeedingIds[i]] =
|
||||
this.block.id + elementsNeedingIds[i];
|
||||
}
|
||||
},
|
||||
ngAfterViewInit: function() {
|
||||
// If this is a top-level tree in the workspace, set its id and active
|
||||
// descendant.
|
||||
if (this.tree && this.isTopLevel && !this.tree.id) {
|
||||
this.tree.id = this.utilsService.generateUniqueId();
|
||||
}
|
||||
|
||||
if (this.tree && this.isTopLevel &&
|
||||
!this.treeService.getActiveDescId(this.tree.id)) {
|
||||
this.treeService.setActiveDesc(this.idMap['parentList'], this.tree.id);
|
||||
}
|
||||
},
|
||||
canBeMovedToMarkedConnection: function(block) {
|
||||
return this.clipboardService.canBeMovedToMarkedConnection(block);
|
||||
},
|
||||
hasPreviousConnection: function(block) {
|
||||
return Boolean(block.previousConnection);
|
||||
},
|
||||
hasNextConnection: function(block) {
|
||||
return Boolean(block.nextConnection);
|
||||
},
|
||||
isCompatibleWithClipboard: function(connection) {
|
||||
return this.clipboardService.isCompatibleWithClipboard(connection);
|
||||
},
|
||||
isIsolatedTopLevelBlock: function(block) {
|
||||
isIsolatedTopLevelBlock_: function(block) {
|
||||
// Returns whether the given block is at the top level, and has no
|
||||
// siblings.
|
||||
return Boolean(
|
||||
@@ -208,62 +113,164 @@ blocklyApp.WorkspaceTreeComponent = ng.core
|
||||
return topBlock.id == block.id;
|
||||
}));
|
||||
},
|
||||
generateAriaLabelledByAttr: function(mainLabel, secondLabel, isDisabled) {
|
||||
return this.utilsService.generateAriaLabelledByAttr(
|
||||
mainLabel, secondLabel, isDisabled);
|
||||
},
|
||||
pasteAbove: function(block) {
|
||||
var that = this;
|
||||
this.treeService.runWhilePreservingFocus(function() {
|
||||
that.clipboardService.pasteFromClipboard(block.previousConnection);
|
||||
}, this.tree.id);
|
||||
},
|
||||
pasteBelow: function(block) {
|
||||
var that = this;
|
||||
this.treeService.runWhilePreservingFocus(function() {
|
||||
that.clipboardService.pasteFromClipboard(block.nextConnection);
|
||||
}, this.tree.id);
|
||||
},
|
||||
getRootNode: function() {
|
||||
// Gets the root HTML node for this component.
|
||||
return document.getElementById(this.idMap['parentList']);
|
||||
},
|
||||
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)) {
|
||||
if (this.isIsolatedTopLevelBlock_(block)) {
|
||||
var nextNodeToFocusOn =
|
||||
this.treeService.getNodeToFocusOnWhenTreeIsDeleted(this.tree.id);
|
||||
deleteBlockFunc();
|
||||
nextNodeToFocusOn.focus();
|
||||
} else {
|
||||
var blockRootNode = document.getElementById(this.idMap['blockRoot']);
|
||||
var nextActiveDesc =
|
||||
this.treeService.getNextActiveDescWhenBlockIsDeleted(
|
||||
this.getRootNode());
|
||||
blockRootNode);
|
||||
this.treeService.runWhilePreservingFocus(
|
||||
deleteBlockFunc, this.tree.id, nextActiveDesc.id);
|
||||
}
|
||||
},
|
||||
cutToClipboard: function(block) {
|
||||
cutBlock_: function() {
|
||||
var that = this;
|
||||
this.removeBlockAndSetFocus_(block, function() {
|
||||
that.clipboardService.cut(block);
|
||||
this.removeBlockAndSetFocus_(this.block, function() {
|
||||
that.clipboardService.cut(that.block);
|
||||
});
|
||||
},
|
||||
deleteBlock: function(block) {
|
||||
this.removeBlockAndSetFocus_(block, function() {
|
||||
block.dispose(true);
|
||||
deleteBlock_: function() {
|
||||
var that = this;
|
||||
this.removeBlockAndSetFocus_(this.block, function() {
|
||||
that.block.dispose(true);
|
||||
});
|
||||
},
|
||||
sendToMarkedSpot: function(block) {
|
||||
this.clipboardService.pasteToMarkedConnection(block, false);
|
||||
pasteToConnection_: function(connection) {
|
||||
var that = this;
|
||||
this.treeService.runWhilePreservingFocus(function() {
|
||||
that.clipboardService.pasteFromClipboard(connection);
|
||||
}, this.tree.id);
|
||||
},
|
||||
sendToMarkedSpot_: function() {
|
||||
this.clipboardService.pasteToMarkedConnection(this.block, false);
|
||||
|
||||
this.removeBlockAndSetFocus_(block, function() {
|
||||
block.dispose(true);
|
||||
var that = this;
|
||||
this.removeBlockAndSetFocus_(this.block, function() {
|
||||
that.block.dispose(true);
|
||||
});
|
||||
|
||||
alert('Block moved to marked spot: ' + block.toString());
|
||||
alert('Block moved to marked spot: ' + this.block.toString());
|
||||
},
|
||||
ngOnInit: function() {
|
||||
var that = this;
|
||||
|
||||
// Generate a list of action buttons.
|
||||
this.actionButtonsInfo = [{
|
||||
baseIdKey: 'cut',
|
||||
translationIdForText: 'CUT_BLOCK',
|
||||
action: that.cutBlock_.bind(that),
|
||||
isDisabled: function() {
|
||||
return false;
|
||||
}
|
||||
}, {
|
||||
baseIdKey: 'copy',
|
||||
translationIdForText: 'COPY_BLOCK',
|
||||
action: that.clipboardService.copy.bind(
|
||||
that.clipboardService, that.block, true),
|
||||
isDisabled: function() {
|
||||
return false;
|
||||
}
|
||||
}, {
|
||||
baseIdKey: 'pasteBelow',
|
||||
translationIdForText: 'PASTE_BELOW',
|
||||
action: that.pasteToConnection_.bind(that, that.block.nextConnection),
|
||||
isDisabled: function() {
|
||||
return Boolean(
|
||||
!that.block.nextConnection ||
|
||||
!that.isCompatibleWithClipboard(that.block.nextConnection));
|
||||
}
|
||||
}, {
|
||||
baseIdKey: 'pasteAbove',
|
||||
translationIdForText: 'PASTE_ABOVE',
|
||||
action: that.pasteToConnection_.bind(
|
||||
that, that.block.previousConnection),
|
||||
isDisabled: function() {
|
||||
return Boolean(
|
||||
!that.block.previousConnection ||
|
||||
!that.isCompatibleWithClipboard(that.block.previousConnection));
|
||||
}
|
||||
}, {
|
||||
baseIdKey: 'markBelow',
|
||||
translationIdForText: 'MARK_SPOT_BELOW',
|
||||
action: that.clipboardService.markConnection.bind(
|
||||
that.clipboardService, that.block.nextConnection),
|
||||
isDisabled: function() {
|
||||
return !that.block.nextConnection;
|
||||
}
|
||||
}, {
|
||||
baseIdKey: 'markAbove',
|
||||
translationIdForText: 'MARK_SPOT_ABOVE',
|
||||
action: that.clipboardService.markConnection.bind(
|
||||
that.clipboardService, that.block.previousConnection),
|
||||
isDisabled: function() {
|
||||
return !that.block.previousConnection;
|
||||
}
|
||||
}, {
|
||||
baseIdKey: 'sendToMarkedSpot',
|
||||
translationIdForText: 'MOVE_TO_MARKED_SPOT',
|
||||
action: that.sendToMarkedSpot_.bind(that),
|
||||
isDisabled: function() {
|
||||
return !that.clipboardService.isMovableToMarkedConnection(
|
||||
that.block);
|
||||
}
|
||||
}, {
|
||||
baseIdKey: 'delete',
|
||||
translationIdForText: 'DELETE',
|
||||
action: that.deleteBlock_.bind(that),
|
||||
isDisabled: function() {
|
||||
return false;
|
||||
}
|
||||
}];
|
||||
|
||||
// Make a list of all the id keys.
|
||||
this.idKeys = ['blockRoot', 'blockSummary', 'listItem', 'label'];
|
||||
this.actionButtonsInfo.forEach(function(buttonInfo) {
|
||||
that.idKeys.push(buttonInfo.baseIdKey, buttonInfo.baseIdKey + 'Button');
|
||||
});
|
||||
for (var i = 0; i < this.block.inputList.length; i++) {
|
||||
var inputBlock = this.block.inputList[i];
|
||||
if (inputBlock.connection && !inputBlock.connection.targetBlock()) {
|
||||
that.idKeys.push(
|
||||
'inputList' + i, 'inputMenuLabel' + i, 'markSpot' + i,
|
||||
'markSpotButton' + i, 'paste' + i, 'pasteButton' + i);
|
||||
}
|
||||
}
|
||||
},
|
||||
ngDoCheck: function() {
|
||||
// Generate a unique id for each id key. This needs to be done every time
|
||||
// changes happen, but after the first ng-init, in order to force the
|
||||
// element ids to change in cases where, e.g., a block is inserted in the
|
||||
// middle of a sequence of blocks.
|
||||
this.idMap = {};
|
||||
for (var i = 0; i < this.idKeys.length; i++) {
|
||||
this.idMap[this.idKeys[i]] = this.block.id + this.idKeys[i];
|
||||
}
|
||||
},
|
||||
ngAfterViewInit: function() {
|
||||
// If this is a top-level tree in the workspace, set its id and active
|
||||
// descendant.
|
||||
if (this.tree && this.isTopLevel && !this.tree.id) {
|
||||
this.tree.id = this.utilsService.generateUniqueId();
|
||||
}
|
||||
if (this.tree && this.isTopLevel &&
|
||||
!this.treeService.getActiveDescId(this.tree.id)) {
|
||||
this.treeService.setActiveDesc(this.idMap['blockRoot'], this.tree.id);
|
||||
}
|
||||
},
|
||||
generateAriaLabelledByAttr: function(mainLabel, secondLabel, isDisabled) {
|
||||
return this.utilsService.generateAriaLabelledByAttr(
|
||||
mainLabel, secondLabel, isDisabled);
|
||||
},
|
||||
isCompatibleWithClipboard: function(connection) {
|
||||
return this.clipboardService.isCompatibleWithClipboard(connection);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user