workspace-tree.component.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  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(idMap['blockSummary'], 'blockly-workspace-block')"
  31. [attr.aria-level]="level">
  32. <label [id]="idMap['blockSummary']">{{getBlockDescription()}}</label>
  33. <ol role="group">
  34. <template ngFor #blockInput [ngForOf]="block.inputList" #i="index">
  35. <li role="treeitem" [id]="idMap['listItem' + i]" [attr.aria-level]="level + 1" *ngIf="blockInput.fieldRow.length"
  36. [attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['fieldLabel' + i])">
  37. <blockly-field-segment *ngFor="#fieldSegment of inputListAsFieldSegments[i]"
  38. [prefixFields]="fieldSegment.prefixFields"
  39. [mainField]="fieldSegment.mainField"
  40. [mainFieldId]="idMap['fieldLabel' + i]"
  41. [level]="level + 2">
  42. </blockly-field-segment>
  43. </li>
  44. <blockly-workspace-tree *ngIf="blockInput.connection && blockInput.connection.targetBlock()"
  45. [block]="blockInput.connection.targetBlock()" [level]="level + 1"
  46. [tree]="tree">
  47. </blockly-workspace-tree>
  48. <li #inputList [id]="idMap['inputList' + i]" role="treeitem"
  49. *ngIf="blockInput.connection && !blockInput.connection.targetBlock()"
  50. [attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['inputMenuLabel' + i], 'blockly-submenu-indicator')"
  51. [attr.aria-level]="level + 1">
  52. <label [id]="idMap['inputMenuLabel' + i]">
  53. {{utilsService.getInputTypeLabel(blockInput.connection)}} {{utilsService.getBlockTypeLabel(blockInput)}} needed:
  54. </label>
  55. <ol role="group">
  56. <li *ngFor="#fieldButtonInfo of fieldButtonsInfo"
  57. [id]="idMap[fieldButtonInfo.baseIdKey + i]" role="treeitem"
  58. [attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap[fieldButtonInfo.baseIdKey + 'Button' + i], 'blockly-button')"
  59. [attr.aria-level]="level + 2"
  60. [attr.aria-disabled]="fieldButtonInfo.isDisabled(blockInput.connection)">
  61. <button [id]="idMap[fieldButtonInfo.baseIdKey + 'Button' + i]"
  62. (click)="fieldButtonInfo.action(blockInput.connection)"
  63. [disabled]="fieldButtonInfo.isDisabled(blockInput.connection)" tabindex="-1">
  64. {{fieldButtonInfo.translationIdForText|translate}}
  65. </button>
  66. </li>
  67. </ol>
  68. </li>
  69. </template>
  70. <li [id]="idMap['listItem']" class="blocklyHasChildren" role="treeitem"
  71. [attr.aria-labelledBy]="generateAriaLabelledByAttr('blockly-more-options', 'blockly-submenu-indicator')"
  72. [attr.aria-level]="level + 1">
  73. <label [id]="idMap['label']">{{'BLOCK_OPTIONS'|translate}}</label>
  74. <ol role="group">
  75. <li *ngFor="#buttonInfo of actionButtonsInfo"
  76. [id]="idMap[buttonInfo.baseIdKey]" role="treeitem"
  77. [attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap[buttonInfo.baseIdKey + 'Button'], 'blockly-button')"
  78. [attr.aria-level]="level + 2"
  79. [attr.aria-disabled]="buttonInfo.isDisabled()">
  80. <button [id]="idMap[buttonInfo.baseIdKey + 'Button']" (click)="buttonInfo.action()"
  81. [disabled]="buttonInfo.isDisabled()" tabindex="-1">
  82. {{buttonInfo.translationIdForText|translate}}
  83. </button>
  84. </li>
  85. </ol>
  86. </li>
  87. </ol>
  88. </li>
  89. <blockly-workspace-tree *ngIf= "block.nextConnection && block.nextConnection.targetBlock()"
  90. [block]="block.nextConnection.targetBlock()"
  91. [level]="level" [tree]="tree">
  92. </blockly-workspace-tree>
  93. `,
  94. directives: [blocklyApp.FieldSegmentComponent, ng.core.forwardRef(function() {
  95. return blocklyApp.WorkspaceTreeComponent;
  96. })],
  97. inputs: ['block', 'level', 'tree', 'isTopLevel'],
  98. pipes: [blocklyApp.TranslatePipe]
  99. })
  100. .Class({
  101. constructor: [
  102. blocklyApp.ClipboardService, blocklyApp.NotificationsService,
  103. blocklyApp.TreeService, blocklyApp.UtilsService,
  104. blocklyApp.AudioService,
  105. function(
  106. _clipboardService, _notificationsService, _treeService,
  107. _utilsService, _audioService) {
  108. this.clipboardService = _clipboardService;
  109. this.notificationsService = _notificationsService;
  110. this.treeService = _treeService;
  111. this.utilsService = _utilsService;
  112. this.audioService = _audioService;
  113. }],
  114. ngOnInit: function() {
  115. var SUPPORTED_FIELDS = [
  116. Blockly.FieldTextInput, Blockly.FieldDropdown,
  117. Blockly.FieldCheckbox];
  118. this.inputListAsFieldSegments = this.block.inputList.map(function(input) {
  119. // Converts the input to a list of field segments. Each field segment
  120. // represents a user-editable field, prefixed by any number of
  121. // non-editable fields.
  122. var fieldSegments = [];
  123. var bufferedFields = [];
  124. input.fieldRow.forEach(function(field) {
  125. var fieldIsSupported = SUPPORTED_FIELDS.some(function(fieldType) {
  126. return (field instanceof fieldType);
  127. });
  128. if (fieldIsSupported) {
  129. var fieldSegment = {
  130. prefixFields: [],
  131. mainField: field
  132. };
  133. bufferedFields.forEach(function(bufferedField) {
  134. fieldSegment.prefixFields.push(bufferedField);
  135. });
  136. fieldSegments.push(fieldSegment);
  137. bufferedFields = [];
  138. } else {
  139. bufferedFields.push(field);
  140. }
  141. });
  142. // Handle leftover text at the end.
  143. if (bufferedFields.length) {
  144. fieldSegments.push({
  145. prefixFields: bufferedFields,
  146. mainField: null
  147. });
  148. }
  149. return fieldSegments;
  150. });
  151. // Generate a list of action buttons.
  152. var that = this;
  153. this.actionButtonsInfo = [{
  154. baseIdKey: 'copy',
  155. translationIdForText: 'COPY_BLOCK',
  156. action: function() {
  157. that.clipboardService.copy(that.block);
  158. that.notificationsService.setStatusMessage(
  159. that.getBlockDescription() + ' ' + Blockly.Msg.COPIED_BLOCK_MSG);
  160. },
  161. isDisabled: function() {
  162. return false;
  163. }
  164. }, {
  165. baseIdKey: 'pasteBefore',
  166. translationIdForText: 'PASTE_BEFORE',
  167. action: function() {
  168. that.treeService.pasteToConnection(
  169. that.block, that.block.previousConnection);
  170. },
  171. isDisabled: function() {
  172. return Boolean(
  173. !that.block.previousConnection ||
  174. !that.isCompatibleWithClipboard(that.block.previousConnection));
  175. }
  176. }, {
  177. baseIdKey: 'pasteAfter',
  178. translationIdForText: 'PASTE_AFTER',
  179. action: function() {
  180. that.treeService.pasteToConnection(
  181. that.block, that.block.nextConnection);
  182. },
  183. isDisabled: function() {
  184. return Boolean(
  185. !that.block.nextConnection ||
  186. !that.isCompatibleWithClipboard(that.block.nextConnection));
  187. }
  188. }, {
  189. baseIdKey: 'markBefore',
  190. translationIdForText: 'MARK_SPOT_BEFORE',
  191. action: that.markSpotBefore_.bind(that),
  192. isDisabled: function() {
  193. return !that.block.previousConnection;
  194. }
  195. }, {
  196. baseIdKey: 'markAfter',
  197. translationIdForText: 'MARK_SPOT_AFTER',
  198. action: that.markSpotAfter_.bind(that),
  199. isDisabled: function() {
  200. return !that.block.nextConnection;
  201. }
  202. }, {
  203. baseIdKey: 'moveToMarkedSpot',
  204. translationIdForText: 'MOVE_TO_MARKED_SPOT',
  205. action: that.moveToMarkedSpot_.bind(that),
  206. isDisabled: function() {
  207. return !that.clipboardService.isMovableToMarkedConnection(
  208. that.block);
  209. }
  210. }, {
  211. baseIdKey: 'delete',
  212. translationIdForText: 'DELETE',
  213. action: that.deleteBlock_.bind(that),
  214. isDisabled: function() {
  215. return false;
  216. }
  217. }];
  218. // Generate a list of action buttons.
  219. this.fieldButtonsInfo = [{
  220. baseIdKey: 'markSpot',
  221. translationIdForText: 'MARK_THIS_SPOT',
  222. action: function(connection) {
  223. that.clipboardService.markConnection(connection);
  224. },
  225. isDisabled: function() {
  226. return false;
  227. }
  228. }, {
  229. baseIdKey: 'paste',
  230. translationIdForText: 'PASTE_INSIDE',
  231. action: function(connection) {
  232. that.treeService.pasteToConnection(that.block, connection);
  233. },
  234. isDisabled: function(connection) {
  235. return !that.isCompatibleWithClipboard(connection);
  236. }
  237. }];
  238. // Make a list of all the id keys.
  239. this.idKeys = ['blockRoot', 'blockSummary', 'listItem', 'label'];
  240. this.actionButtonsInfo.forEach(function(buttonInfo) {
  241. that.idKeys.push(buttonInfo.baseIdKey, buttonInfo.baseIdKey + 'Button');
  242. });
  243. this.fieldButtonsInfo.forEach(function(buttonInfo) {
  244. for (var i = 0; i < that.block.inputList.length; i++) {
  245. that.idKeys.push(
  246. buttonInfo.baseIdKey + i, buttonInfo.baseIdKey + 'Button' + i);
  247. }
  248. });
  249. for (var i = 0; i < this.block.inputList.length; i++) {
  250. var blockInput = this.block.inputList[i];
  251. that.idKeys.push(
  252. 'inputList' + i, 'inputMenuLabel' + i, 'listItem' + i,
  253. 'fieldLabel' + i);
  254. }
  255. },
  256. ngDoCheck: function() {
  257. // Generate a unique id for each id key. This needs to be done every time
  258. // changes happen, but after the first ng-init, in order to force the
  259. // element ids to change in cases where, e.g., a block is inserted in the
  260. // middle of a sequence of blocks.
  261. this.idMap = {};
  262. for (var i = 0; i < this.idKeys.length; i++) {
  263. this.idMap[this.idKeys[i]] = this.block.id + this.idKeys[i];
  264. }
  265. },
  266. ngAfterViewInit: function() {
  267. // If this is a top-level tree in the workspace, set its id and active
  268. // descendant. (Note that a timeout is needed here in order to trigger
  269. // Angular change detection.)
  270. var that = this;
  271. setTimeout(function() {
  272. if (that.tree && that.isTopLevel && !that.tree.id) {
  273. that.tree.id = that.utilsService.generateUniqueId();
  274. }
  275. if (that.tree && that.isTopLevel &&
  276. !that.treeService.getActiveDescId(that.tree.id)) {
  277. that.treeService.setActiveDesc(that.idMap['blockRoot'], that.tree.id);
  278. }
  279. });
  280. },
  281. getBlockDescription: function() {
  282. var blockDescription = this.utilsService.getBlockDescription(this.block);
  283. var parentBlock = this.block.getSurroundParent();
  284. if (parentBlock) {
  285. var fullDescription = blockDescription + ' inside ' +
  286. this.utilsService.getBlockDescription(parentBlock);
  287. return fullDescription;
  288. } else {
  289. return blockDescription;
  290. }
  291. },
  292. removeBlockAndSetFocus_: function(block, deleteBlockFunc) {
  293. this.treeService.removeBlockAndSetFocus(
  294. block, document.getElementById(this.idMap['blockRoot']),
  295. deleteBlockFunc);
  296. },
  297. deleteBlock_: function() {
  298. var blockDescription = this.getBlockDescription();
  299. var that = this;
  300. this.removeBlockAndSetFocus_(this.block, function() {
  301. that.block.dispose(true);
  302. that.audioService.playDeleteSound();
  303. });
  304. setTimeout(function() {
  305. if (that.utilsService.isWorkspaceEmpty()) {
  306. that.notificationsService.setStatusMessage(
  307. blockDescription + ' deleted. Workspace is empty.');
  308. } else {
  309. that.notificationsService.setStatusMessage(
  310. blockDescription + ' deleted. Now on workspace.');
  311. }
  312. });
  313. },
  314. moveToMarkedSpot_: function() {
  315. var blockDescription = this.getBlockDescription();
  316. var oldDestinationTreeId = this.treeService.getTreeIdForBlock(
  317. this.clipboardService.getMarkedConnectionBlock().id);
  318. this.treeService.clearActiveDesc(oldDestinationTreeId);
  319. var newBlockId = this.clipboardService.pasteToMarkedConnection(
  320. this.block);
  321. var that = this;
  322. this.removeBlockAndSetFocus_(this.block, function() {
  323. that.block.dispose(true);
  324. });
  325. // Invoke a digest cycle, so that the DOM settles.
  326. setTimeout(function() {
  327. that.treeService.focusOnBlock(newBlockId);
  328. var newDestinationTreeId = that.treeService.getTreeIdForBlock(
  329. newBlockId);
  330. if (newDestinationTreeId != oldDestinationTreeId) {
  331. // It is possible for the tree ID for the pasted block to change
  332. // after the paste operation, e.g. when inserting a block between two
  333. // existing blocks that are joined together. In this case, we need to
  334. // also reset the active desc for the old destination tree.
  335. that.treeService.initActiveDesc(oldDestinationTreeId);
  336. }
  337. that.notificationsService.setStatusMessage(
  338. blockDescription + ' ' +
  339. Blockly.Msg.PASTED_BLOCK_TO_MARKED_SPOT_MSG +
  340. '. Now on moved block in workspace.');
  341. });
  342. },
  343. markSpotBefore_: function() {
  344. this.clipboardService.markConnection(this.block.previousConnection);
  345. },
  346. markSpotAfter_: function() {
  347. this.clipboardService.markConnection(this.block.nextConnection);
  348. },
  349. generateAriaLabelledByAttr: function(mainLabel, secondLabel) {
  350. return this.utilsService.generateAriaLabelledByAttr(
  351. mainLabel, secondLabel);
  352. },
  353. isCompatibleWithClipboard: function(connection) {
  354. return this.clipboardService.isCompatibleWithClipboard(connection);
  355. }
  356. });