workspace-tree.component.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. /**
  2. * AccessibleBlockly
  3. *
  4. * Copyright 2016 Google Inc.
  5. * https://developers.google.com/blockly/
  6. *
  7. * Licensed under the Apache License, Version 2.0 (the 'License');
  8. * you may not use this file except in compliance with the License.
  9. * You may obtain a copy of the License at
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing, software
  14. * distributed under the License is distributed on an 'AS IS' BASIS,
  15. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. * See the License for the specific language governing permissions and
  17. * limitations under the License.
  18. */
  19. /**
  20. * @fileoverview Angular2 Component that details how Blockly.Block's are
  21. * rendered in the workspace in AccessibleBlockly. Also handles any
  22. * interactions with the blocks.
  23. * @author madeeha@google.com (Madeeha Ghori)
  24. */
  25. blocklyApp.WorkspaceTreeComponent = ng.core
  26. .Component({
  27. selector: 'blockly-workspace-tree',
  28. template: `
  29. <li [id]="idMap['blockRoot']" role="treeitem" class="blocklyHasChildren"
  30. [attr.aria-labelledBy]="generateAriaLabelledByAttr('blockly-block-summary', idMap['blockSummary'])"
  31. [attr.aria-level]="level" aria-selected="false">
  32. <label [id]="idMap['blockSummary']">{{block.toString()}}</label>
  33. <ol role="group" [attr.aria-level]="level + 1">
  34. <li [id]="idMap['listItem']" class="blocklyHasChildren" role="treeitem"
  35. [attr.aria-labelledBy]="generateAriaLabelledByAttr('blockly-block-menu', idMap['blockSummary'])"
  36. [attr.aria-level]="level+1" aria-selected="false">
  37. <label [id]="idMap['label']">{{'BLOCK_ACTION_LIST'|translate}}</label>
  38. <ol role="group" [attr.aria-level]="level + 2">
  39. <li *ngFor="#buttonInfo of actionButtonsInfo"
  40. [id]="idMap[buttonInfo.baseIdKey]" role="treeitem"
  41. [attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap[buttonInfo.baseIdKey + 'Button'], 'blockly-button', buttonInfo.isDisabled())"
  42. [attr.aria-level]="level + 2" aria-selected="false">
  43. <button [id]="idMap[buttonInfo.baseIdKey + 'Button']" (click)="buttonInfo.action()"
  44. [disabled]="buttonInfo.isDisabled()">
  45. {{buttonInfo.translationIdForText|translate}}
  46. </button>
  47. </li>
  48. </ol>
  49. </li>
  50. <div *ngFor="#inputBlock of block.inputList; #i = index">
  51. <blockly-field *ngFor="#field of inputBlock.fieldRow" [field]="field"></blockly-field>
  52. <blockly-workspace-tree *ngIf="inputBlock.connection && inputBlock.connection.targetBlock()"
  53. [block]="inputBlock.connection.targetBlock()" [level]="level"
  54. [tree]="tree">
  55. </blockly-workspace-tree>
  56. <li #inputList [attr.aria-level]="level + 1" [id]="idMap['inputList' + i]"
  57. [attr.aria-labelledBy]="generateAriaLabelledByAttr('blockly-menu', idMap['inputMenuLabel' + i])"
  58. *ngIf="inputBlock.connection && !inputBlock.connection.targetBlock()" (keydown)="treeService.onKeypress($event, tree)">
  59. <!-- TODO(madeeha): i18n here will need to happen in a different way due to the way grammar changes based on language. -->
  60. <label [id]="idMap['inputMenuLabel' + i]"> {{utilsService.getInputTypeLabel(inputBlock.connection)}} {{utilsService.getBlockTypeLabel(inputBlock)}} needed: </label>
  61. <ol role="group" [attr.aria-level]="level + 2">
  62. <li [id]="idMap['markSpot' + i]" role="treeitem"
  63. [attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['markButton' + i], 'blockly-button')"
  64. [attr.aria-level]="level + 2" aria-selected=false>
  65. <button [id]="idMap['markSpotButton + i']" (click)="clipboardService.markConnection(inputBlock.connection)">{{'MARK_THIS_SPOT'|translate}}</button>
  66. </li>
  67. <li [id]="idMap['paste' + i]" role="treeitem"
  68. [attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['pasteButton' + i], 'blockly-button', !isCompatibleWithClipboard(inputBlock.connection))"
  69. [attr.aria-level]="level+2" aria-selected=false>
  70. <button [id]="idMap['pasteButton' + i]" (click)="clipboardService.pasteFromClipboard(inputBlock.connection)"
  71. [disabled]="!isCompatibleWithClipboard(inputBlock.connection)">
  72. {{'PASTE'|translate}}
  73. </button>
  74. </li>
  75. </ol>
  76. </li>
  77. </div>
  78. </ol>
  79. </li>
  80. <blockly-workspace-tree *ngIf= "block.nextConnection && block.nextConnection.targetBlock()"
  81. [block]="block.nextConnection.targetBlock()"
  82. [level]="level" [tree]="tree">
  83. </blockly-workspace-tree>
  84. `,
  85. directives: [blocklyApp.FieldComponent, ng.core.forwardRef(function() {
  86. return blocklyApp.WorkspaceTreeComponent;
  87. })],
  88. inputs: ['block', 'level', 'tree', 'isTopLevel'],
  89. pipes: [blocklyApp.TranslatePipe]
  90. })
  91. .Class({
  92. constructor: [
  93. blocklyApp.ClipboardService, blocklyApp.TreeService,
  94. blocklyApp.UtilsService,
  95. function(_clipboardService, _treeService, _utilsService) {
  96. this.infoBlocks = Object.create(null);
  97. this.clipboardService = _clipboardService;
  98. this.treeService = _treeService;
  99. this.utilsService = _utilsService;
  100. }],
  101. isIsolatedTopLevelBlock_: function(block) {
  102. // Returns whether the given block is at the top level, and has no
  103. // siblings.
  104. return Boolean(
  105. !block.nextConnection.targetConnection &&
  106. !block.previousConnection.targetConnection &&
  107. blocklyApp.workspace.topBlocks_.some(function(topBlock) {
  108. return topBlock.id == block.id;
  109. }));
  110. },
  111. removeBlockAndSetFocus_: function(block, deleteBlockFunc) {
  112. // This method runs the given function and then does one of two things:
  113. // - If the block is an isolated top-level block, it shifts the tree
  114. // focus.
  115. // - Otherwise, it sets the correct new active desc for the current tree.
  116. if (this.isIsolatedTopLevelBlock_(block)) {
  117. var nextNodeToFocusOn =
  118. this.treeService.getNodeToFocusOnWhenTreeIsDeleted(this.tree.id);
  119. deleteBlockFunc();
  120. nextNodeToFocusOn.focus();
  121. } else {
  122. var blockRootNode = document.getElementById(this.idMap['blockRoot']);
  123. var nextActiveDesc =
  124. this.treeService.getNextActiveDescWhenBlockIsDeleted(
  125. blockRootNode);
  126. this.treeService.runWhilePreservingFocus(
  127. deleteBlockFunc, this.tree.id, nextActiveDesc.id);
  128. }
  129. },
  130. cutBlock_: function() {
  131. var that = this;
  132. this.removeBlockAndSetFocus_(this.block, function() {
  133. that.clipboardService.cut(that.block);
  134. });
  135. },
  136. deleteBlock_: function() {
  137. var that = this;
  138. this.removeBlockAndSetFocus_(this.block, function() {
  139. that.block.dispose(true);
  140. });
  141. },
  142. pasteToConnection_: function(connection) {
  143. var that = this;
  144. this.treeService.runWhilePreservingFocus(function() {
  145. // If the connection is a 'previousConnection' and that connection is
  146. // already joined to something, use the 'nextConnection' of the
  147. // previous block instead in order to do an insertion.
  148. if (connection.type == Blockly.PREVIOUS_STATEMENT &&
  149. connection.isConnected()) {
  150. that.clipboardService.pasteFromClipboard(
  151. connection.targetConnection);
  152. } else {
  153. that.clipboardService.pasteFromClipboard(connection);
  154. }
  155. }, this.tree.id);
  156. },
  157. sendToMarkedSpot_: function() {
  158. this.clipboardService.pasteToMarkedConnection(this.block, false);
  159. var that = this;
  160. this.removeBlockAndSetFocus_(this.block, function() {
  161. that.block.dispose(true);
  162. });
  163. alert('Block moved to marked spot: ' + this.block.toString());
  164. },
  165. ngOnInit: function() {
  166. var that = this;
  167. // Generate a list of action buttons.
  168. this.actionButtonsInfo = [{
  169. baseIdKey: 'cut',
  170. translationIdForText: 'CUT_BLOCK',
  171. action: that.cutBlock_.bind(that),
  172. isDisabled: function() {
  173. return false;
  174. }
  175. }, {
  176. baseIdKey: 'copy',
  177. translationIdForText: 'COPY_BLOCK',
  178. action: that.clipboardService.copy.bind(
  179. that.clipboardService, that.block, true),
  180. isDisabled: function() {
  181. return false;
  182. }
  183. }, {
  184. baseIdKey: 'pasteBelow',
  185. translationIdForText: 'PASTE_BELOW',
  186. action: that.pasteToConnection_.bind(that, that.block.nextConnection),
  187. isDisabled: function() {
  188. return Boolean(
  189. !that.block.nextConnection ||
  190. !that.isCompatibleWithClipboard(that.block.nextConnection));
  191. }
  192. }, {
  193. baseIdKey: 'pasteAbove',
  194. translationIdForText: 'PASTE_ABOVE',
  195. action: that.pasteToConnection_.bind(
  196. that, that.block.previousConnection),
  197. isDisabled: function() {
  198. return Boolean(
  199. !that.block.previousConnection ||
  200. !that.isCompatibleWithClipboard(that.block.previousConnection));
  201. }
  202. }, {
  203. baseIdKey: 'markBelow',
  204. translationIdForText: 'MARK_SPOT_BELOW',
  205. action: that.clipboardService.markConnection.bind(
  206. that.clipboardService, that.block.nextConnection),
  207. isDisabled: function() {
  208. return !that.block.nextConnection;
  209. }
  210. }, {
  211. baseIdKey: 'markAbove',
  212. translationIdForText: 'MARK_SPOT_ABOVE',
  213. action: that.clipboardService.markConnection.bind(
  214. that.clipboardService, that.block.previousConnection),
  215. isDisabled: function() {
  216. return !that.block.previousConnection;
  217. }
  218. }, {
  219. baseIdKey: 'sendToMarkedSpot',
  220. translationIdForText: 'MOVE_TO_MARKED_SPOT',
  221. action: that.sendToMarkedSpot_.bind(that),
  222. isDisabled: function() {
  223. return !that.clipboardService.isMovableToMarkedConnection(
  224. that.block);
  225. }
  226. }, {
  227. baseIdKey: 'delete',
  228. translationIdForText: 'DELETE',
  229. action: that.deleteBlock_.bind(that),
  230. isDisabled: function() {
  231. return false;
  232. }
  233. }];
  234. // Make a list of all the id keys.
  235. this.idKeys = ['blockRoot', 'blockSummary', 'listItem', 'label'];
  236. this.actionButtonsInfo.forEach(function(buttonInfo) {
  237. that.idKeys.push(buttonInfo.baseIdKey, buttonInfo.baseIdKey + 'Button');
  238. });
  239. for (var i = 0; i < this.block.inputList.length; i++) {
  240. var inputBlock = this.block.inputList[i];
  241. if (inputBlock.connection && !inputBlock.connection.targetBlock()) {
  242. that.idKeys.push(
  243. 'inputList' + i, 'inputMenuLabel' + i, 'markSpot' + i,
  244. 'markSpotButton' + i, 'paste' + i, 'pasteButton' + i);
  245. }
  246. }
  247. },
  248. ngDoCheck: function() {
  249. // Generate a unique id for each id key. This needs to be done every time
  250. // changes happen, but after the first ng-init, in order to force the
  251. // element ids to change in cases where, e.g., a block is inserted in the
  252. // middle of a sequence of blocks.
  253. this.idMap = {};
  254. for (var i = 0; i < this.idKeys.length; i++) {
  255. this.idMap[this.idKeys[i]] = this.block.id + this.idKeys[i];
  256. }
  257. },
  258. ngAfterViewInit: function() {
  259. // If this is a top-level tree in the workspace, set its id and active
  260. // descendant.
  261. if (this.tree && this.isTopLevel && !this.tree.id) {
  262. this.tree.id = this.utilsService.generateUniqueId();
  263. }
  264. if (this.tree && this.isTopLevel &&
  265. !this.treeService.getActiveDescId(this.tree.id)) {
  266. this.treeService.setActiveDesc(this.idMap['blockRoot'], this.tree.id);
  267. }
  268. },
  269. generateAriaLabelledByAttr: function(mainLabel, secondLabel, isDisabled) {
  270. return this.utilsService.generateAriaLabelledByAttr(
  271. mainLabel, secondLabel, isDisabled);
  272. },
  273. isCompatibleWithClipboard: function(connection) {
  274. return this.clipboardService.isCompatibleWithClipboard(connection);
  275. }
  276. });