Refactor workspace-tree: define the action buttons declaratively; simplify initial lifecycle code.

This commit is contained in:
Sean Lip
2016-06-30 18:19:31 -07:00
parent be664dcdb6
commit 707c2149f1
3 changed files with 165 additions and 152 deletions

View File

@@ -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_ &&

View File

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

View File

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