tree.service.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  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 Service that handles tree keyboard navigation.
  21. * This is a singleton service for the entire application.
  22. *
  23. * @author madeeha@google.com (Madeeha Ghori)
  24. */
  25. blocklyApp.TreeService = ng.core
  26. .Class({
  27. constructor: function() {
  28. // Stores active descendant ids for each tree in the page.
  29. this.activeDescendantIds_ = {};
  30. },
  31. getToolboxTreeNode_: function() {
  32. return document.getElementById('blockly-toolbox-tree');
  33. },
  34. getWorkspaceToolbarButtonNodes_: function() {
  35. return Array.from(document.querySelectorAll(
  36. 'button.blocklyWorkspaceToolbarButton'));
  37. },
  38. // Returns a list of all top-level workspace tree nodes on the page.
  39. getWorkspaceTreeNodes_: function() {
  40. return Array.from(document.querySelectorAll('ol.blocklyWorkspaceTree'));
  41. },
  42. // Returns a list of all top-level tree nodes on the page.
  43. getAllTreeNodes_: function() {
  44. var treeNodes = [this.getToolboxTreeNode_()];
  45. treeNodes = treeNodes.concat(this.getWorkspaceToolbarButtonNodes_());
  46. treeNodes = treeNodes.concat(this.getWorkspaceTreeNodes_());
  47. return treeNodes;
  48. },
  49. isTopLevelWorkspaceTree: function(treeId) {
  50. return this.getWorkspaceTreeNodes_().some(function(tree) {
  51. return tree.id == treeId;
  52. });
  53. },
  54. getNodeToFocusOnWhenTreeIsDeleted: function(deletedTreeId) {
  55. // This returns the node to focus on after the deletion happens.
  56. // We shift focus to the next tree (if it exists), otherwise we shift
  57. // focus to the previous tree.
  58. var trees = this.getAllTreeNodes_();
  59. for (var i = 0; i < trees.length; i++) {
  60. if (trees[i].id == deletedTreeId) {
  61. if (i + 1 < trees.length) {
  62. return trees[i + 1];
  63. } else if (i > 0) {
  64. return trees[i - 1];
  65. }
  66. }
  67. }
  68. return this.getToolboxTreeNode_();
  69. },
  70. focusOnCurrentTree_: function(treeId) {
  71. var trees = this.getAllTreeNodes_();
  72. for (var i = 0; i < trees.length; i++) {
  73. if (trees[i].id == treeId) {
  74. trees[i].focus();
  75. return true;
  76. }
  77. }
  78. return false;
  79. },
  80. focusOnNextTree_: function(treeId) {
  81. var trees = this.getAllTreeNodes_();
  82. for (var i = 0; i < trees.length - 1; i++) {
  83. if (trees[i].id == treeId) {
  84. trees[i + 1].focus();
  85. return true;
  86. }
  87. }
  88. return false;
  89. },
  90. focusOnPreviousTree_: function(treeId) {
  91. var trees = this.getAllTreeNodes_();
  92. for (var i = trees.length - 1; i > 0; i--) {
  93. if (trees[i].id == treeId) {
  94. trees[i - 1].focus();
  95. return true;
  96. }
  97. }
  98. return false;
  99. },
  100. getActiveDescId: function(treeId) {
  101. return this.activeDescendantIds_[treeId] || '';
  102. },
  103. unmarkActiveDesc_: function(activeDescId) {
  104. var activeDesc = document.getElementById(activeDescId);
  105. if (activeDesc) {
  106. activeDesc.classList.remove('blocklyActiveDescendant');
  107. activeDesc.setAttribute('aria-selected', 'false');
  108. }
  109. },
  110. markActiveDesc_: function(activeDescId) {
  111. var newActiveDesc = document.getElementById(activeDescId);
  112. newActiveDesc.classList.add('blocklyActiveDescendant');
  113. newActiveDesc.setAttribute('aria-selected', 'true');
  114. },
  115. // Runs the given function while preserving the focus and active descendant
  116. // for the given tree.
  117. runWhilePreservingFocus: function(func, treeId, optionalNewActiveDescId) {
  118. var oldDescId = this.getActiveDescId(treeId);
  119. var newDescId = optionalNewActiveDescId || oldDescId;
  120. this.unmarkActiveDesc_(oldDescId);
  121. func();
  122. // The timeout is needed in order to give the DOM time to stabilize
  123. // before setting the new active descendant, especially in cases like
  124. // pasteAbove().
  125. var that = this;
  126. setTimeout(function() {
  127. that.markActiveDesc_(newDescId);
  128. that.activeDescendantIds_[treeId] = newDescId;
  129. document.getElementById(treeId).focus();
  130. }, 0);
  131. },
  132. // Make a given node the active descendant of a given tree.
  133. setActiveDesc: function(newActiveDescId, treeId) {
  134. this.unmarkActiveDesc_(this.getActiveDescId(treeId));
  135. this.markActiveDesc_(newActiveDescId);
  136. this.activeDescendantIds_[treeId] = newActiveDescId;
  137. },
  138. onWorkspaceToolbarKeypress: function(e, treeId) {
  139. if (e.keyCode == 9) {
  140. // Tab key.
  141. if (e.shiftKey) {
  142. this.focusOnPreviousTree_(treeId);
  143. } else {
  144. this.focusOnNextTree_(treeId);
  145. }
  146. e.preventDefault();
  147. e.stopPropagation();
  148. }
  149. },
  150. isButtonOrFieldNode_: function(node) {
  151. return ['BUTTON', 'INPUT'].indexOf(node.tagName) != -1;
  152. },
  153. getNextActiveDescWhenBlockIsDeleted: function(blockRootNode) {
  154. // Go up a level, if possible.
  155. var nextNode = blockRootNode.parentNode;
  156. while (nextNode && nextNode.tagName != 'LI') {
  157. nextNode = nextNode.parentNode;
  158. }
  159. if (nextNode) {
  160. return nextNode;
  161. }
  162. // Otherwise, go to the next sibling.
  163. var nextSibling = this.getNextSibling(blockRootNode);
  164. if (nextSibling) {
  165. return nextSibling;
  166. }
  167. // Otherwise, go to the previous sibling.
  168. var previousSibling = this.getPreviousSibling(blockRootNode);
  169. if (previousSibling) {
  170. return previousSibling;
  171. }
  172. // Otherwise, this is a top-level isolated block, which means that
  173. // something's gone wrong and this function should not have been called
  174. // in the first place.
  175. console.error('Could not handle deletion of block.' + blockRootNode);
  176. },
  177. onKeypress: function(e, tree) {
  178. var treeId = tree.id;
  179. var activeDesc = document.getElementById(this.getActiveDescId(treeId));
  180. if (!activeDesc) {
  181. console.error('ERROR: no active descendant for current tree.');
  182. // TODO(sll): Generalize this to other trees (outside the workspace).
  183. var workspaceTreeNodes = this.getWorkspaceTreeNodes_();
  184. for (var i = 0; i < workspaceTreeNodes.length; i++) {
  185. if (workspaceTreeNodes[i].id == treeId) {
  186. // Set the active desc to the first child in this tree.
  187. this.setActiveDesc(
  188. this.getFirstChild(workspaceTreeNodes[i]).id, treeId);
  189. break;
  190. }
  191. }
  192. return;
  193. }
  194. var isFocusingIntoField = false;
  195. if (e.keyCode == 13) {
  196. // Enter key. The user wants to interact with a child.
  197. if (activeDesc.children.length == 1) {
  198. var child = activeDesc.children[0];
  199. if (child.tagName == 'BUTTON') {
  200. child.click();
  201. this.isFocusingIntoField = true;
  202. } else if (child.tagName == 'INPUT') {
  203. child.focus();
  204. }
  205. }
  206. } else if (e.keyCode == 9) {
  207. // Tab key.
  208. if (e.shiftKey) {
  209. this.focusOnPreviousTree_(treeId);
  210. } else {
  211. this.focusOnNextTree_(treeId);
  212. }
  213. e.preventDefault();
  214. e.stopPropagation();
  215. } else if (e.keyCode >= 37 && e.keyCode <= 40) {
  216. // Arrow keys.
  217. if (e.keyCode == 37) {
  218. // Left arrow key. Go up a level, if possible.
  219. var nextNode = activeDesc.parentNode;
  220. if (this.isButtonOrFieldNode_(activeDesc)) {
  221. nextNode = nextNode.parentNode;
  222. }
  223. while (nextNode && nextNode.tagName != 'LI') {
  224. nextNode = nextNode.parentNode;
  225. }
  226. if (nextNode) {
  227. this.setActiveDesc(nextNode.id, treeId);
  228. }
  229. } else if (e.keyCode == 38) {
  230. // Up arrow key. Go to the previous sibling, if possible.
  231. var prevSibling = this.getPreviousSibling(activeDesc);
  232. if (prevSibling) {
  233. this.setActiveDesc(prevSibling.id, treeId);
  234. }
  235. } else if (e.keyCode == 39) {
  236. // Right arrow key. Go down a level, if possible.
  237. var firstChild = this.getFirstChild(activeDesc);
  238. if (firstChild) {
  239. this.setActiveDesc(firstChild.id, treeId);
  240. }
  241. } else if (e.keyCode == 40) {
  242. // Down arrow key. Go to the next sibling, if possible.
  243. var nextSibling = this.getNextSibling(activeDesc);
  244. if (nextSibling) {
  245. this.setActiveDesc(nextSibling.id, treeId);
  246. }
  247. }
  248. e.preventDefault();
  249. e.stopPropagation();
  250. }
  251. },
  252. getFirstChild: function(element) {
  253. if (!element) {
  254. return element;
  255. } else {
  256. var childList = element.children;
  257. for (var i = 0; i < childList.length; i++) {
  258. if (childList[i].tagName == 'LI') {
  259. return childList[i];
  260. } else {
  261. var potentialElement = this.getFirstChild(childList[i]);
  262. if (potentialElement) {
  263. return potentialElement;
  264. }
  265. }
  266. }
  267. return null;
  268. }
  269. },
  270. getNextSibling: function(element) {
  271. if (element.nextElementSibling) {
  272. // If there is a sibling, find the list element child of the sibling.
  273. var node = element.nextElementSibling;
  274. if (node.tagName == 'LI') {
  275. return node;
  276. } else {
  277. // getElementsByTagName returns in DFS order, therefore the first
  278. // element is the first relevant list child.
  279. return node.getElementsByTagName('li')[0];
  280. }
  281. } else {
  282. var parent = element.parentNode;
  283. while (parent && parent.tagName != 'OL') {
  284. if (parent.nextElementSibling) {
  285. var node = parent.nextElementSibling;
  286. if (node.tagName == 'LI') {
  287. return node;
  288. } else {
  289. return this.getFirstChild(node);
  290. }
  291. } else {
  292. parent = parent.parentNode;
  293. }
  294. }
  295. return null;
  296. }
  297. },
  298. getPreviousSibling: function(element) {
  299. if (element.previousElementSibling) {
  300. var sibling = element.previousElementSibling;
  301. if (sibling.tagName == 'LI') {
  302. return sibling;
  303. } else {
  304. return this.getLastChild(sibling);
  305. }
  306. } else {
  307. var parent = element.parentNode;
  308. while (parent) {
  309. if (parent.tagName == 'OL') {
  310. break;
  311. }
  312. if (parent.previousElementSibling) {
  313. var node = parent.previousElementSibling;
  314. if (node.tagName == 'LI') {
  315. return node;
  316. } else {
  317. // Find the last list element child of the sibling of the parent.
  318. return this.getLastChild(node);
  319. }
  320. } else {
  321. parent = parent.parentNode;
  322. }
  323. }
  324. return null;
  325. }
  326. },
  327. getLastChild: function(element) {
  328. if (!element) {
  329. return element;
  330. } else {
  331. var childList = element.children;
  332. for (var i = childList.length - 1; i >= 0; i--) {
  333. // Find the last child that is a list element.
  334. if (childList[i].tagName == 'LI') {
  335. return childList[i];
  336. } else {
  337. var potentialElement = this.getLastChild(childList[i]);
  338. if (potentialElement) {
  339. return potentialElement;
  340. }
  341. }
  342. }
  343. return null;
  344. }
  345. }
  346. });