tree.service.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675
  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: [
  28. blocklyApp.NotificationsService, blocklyApp.UtilsService,
  29. blocklyApp.ClipboardService,
  30. function(_notificationsService, _utilsService, _clipboardService) {
  31. // Stores active descendant ids for each tree in the page.
  32. this.activeDescendantIds_ = {};
  33. this.notificationsService = _notificationsService;
  34. this.utilsService = _utilsService;
  35. this.clipboardService = _clipboardService;
  36. this.toolboxWorkspaces = {};
  37. }],
  38. getToolboxTreeNode_: function() {
  39. return document.getElementById('blockly-toolbox-tree');
  40. },
  41. // Returns a list of all top-level workspace tree nodes on the page.
  42. getWorkspaceTreeNodes_: function() {
  43. return Array.from(document.querySelectorAll('ol.blocklyWorkspaceTree'));
  44. },
  45. getWorkspaceToolbarButtonNodes_: function() {
  46. return Array.from(document.querySelectorAll(
  47. 'button.blocklyWorkspaceToolbarButton'));
  48. },
  49. getToolboxWorkspace: function(categoryNode) {
  50. if (categoryNode.attributes && categoryNode.attributes.name) {
  51. var categoryName = categoryNode.attributes.name.value;
  52. } else {
  53. var categoryName = 'no-category';
  54. }
  55. if (this.toolboxWorkspaces.hasOwnProperty(categoryName)) {
  56. return this.toolboxWorkspaces[categoryName];
  57. } else {
  58. var categoryWorkspace = new Blockly.Workspace();
  59. if (categoryName == 'no-category') {
  60. for (var i = 0; i < categoryNode.length; i++) {
  61. Blockly.Xml.domToBlock(categoryWorkspace, categoryNode[i]);
  62. }
  63. } else {
  64. Blockly.Xml.domToWorkspace(categoryNode, categoryWorkspace);
  65. }
  66. this.toolboxWorkspaces[categoryName] = categoryWorkspace;
  67. return this.toolboxWorkspaces[categoryName];
  68. }
  69. },
  70. getToolboxBlockById: function(blockId) {
  71. for (var categoryName in this.toolboxWorkspaces) {
  72. var putativeBlock = this.utilsService.getBlockByIdFromWorkspace(
  73. blockId, this.toolboxWorkspaces[categoryName]);
  74. if (putativeBlock) {
  75. return putativeBlock;
  76. }
  77. }
  78. return null;
  79. },
  80. // Returns a list of all top-level tree nodes on the page.
  81. getAllTreeNodes_: function() {
  82. var treeNodes = [this.getToolboxTreeNode_()];
  83. treeNodes = treeNodes.concat(this.getWorkspaceTreeNodes_());
  84. treeNodes = treeNodes.concat(this.getWorkspaceToolbarButtonNodes_());
  85. return treeNodes;
  86. },
  87. isTopLevelWorkspaceTree: function(treeId) {
  88. return this.getWorkspaceTreeNodes_().some(function(tree) {
  89. return tree.id == treeId;
  90. });
  91. },
  92. getNodeToFocusOnWhenTreeIsDeleted: function(deletedTreeId) {
  93. // This returns the node to focus on after the deletion happens.
  94. // We shift focus to the next tree (if it exists), otherwise we shift
  95. // focus to the previous tree.
  96. var trees = this.getAllTreeNodes_();
  97. for (var i = 0; i < trees.length; i++) {
  98. if (trees[i].id == deletedTreeId) {
  99. if (i + 1 < trees.length) {
  100. return trees[i + 1];
  101. } else if (i > 0) {
  102. return trees[i - 1];
  103. }
  104. }
  105. }
  106. return this.getToolboxTreeNode_();
  107. },
  108. focusOnCurrentTree_: function(treeId) {
  109. var trees = this.getAllTreeNodes_();
  110. for (var i = 0; i < trees.length; i++) {
  111. if (trees[i].id == treeId) {
  112. trees[i].focus();
  113. return trees[i].id;
  114. }
  115. }
  116. return null;
  117. },
  118. getIdOfNextTree_: function(treeId) {
  119. var trees = this.getAllTreeNodes_();
  120. for (var i = 0; i < trees.length - 1; i++) {
  121. if (trees[i].id == treeId) {
  122. return trees[i + 1].id;
  123. }
  124. }
  125. return null;
  126. },
  127. getIdOfPreviousTree_: function(treeId) {
  128. var trees = this.getAllTreeNodes_();
  129. for (var i = trees.length - 1; i > 0; i--) {
  130. if (trees[i].id == treeId) {
  131. return trees[i - 1].id;
  132. }
  133. }
  134. return null;
  135. },
  136. getActiveDescId: function(treeId) {
  137. return this.activeDescendantIds_[treeId] || '';
  138. },
  139. unmarkActiveDesc_: function(activeDescId) {
  140. var activeDesc = document.getElementById(activeDescId);
  141. if (activeDesc) {
  142. activeDesc.classList.remove('blocklyActiveDescendant');
  143. }
  144. },
  145. markActiveDesc_: function(activeDescId) {
  146. var newActiveDesc = document.getElementById(activeDescId);
  147. newActiveDesc.classList.add('blocklyActiveDescendant');
  148. },
  149. // Runs the given function while preserving the focus and active descendant
  150. // for the given tree.
  151. runWhilePreservingFocus: function(func, treeId, optionalNewActiveDescId) {
  152. var oldDescId = this.getActiveDescId(treeId);
  153. var newDescId = optionalNewActiveDescId || oldDescId;
  154. this.unmarkActiveDesc_(oldDescId);
  155. func();
  156. // The timeout is needed in order to give the DOM time to stabilize
  157. // before setting the new active descendant, especially in cases like
  158. // pasteAbove().
  159. var that = this;
  160. setTimeout(function() {
  161. that.markActiveDesc_(newDescId);
  162. that.activeDescendantIds_[treeId] = newDescId;
  163. document.getElementById(treeId).focus();
  164. }, 0);
  165. },
  166. // This clears the active descendant of the given tree. It is used just
  167. // before the tree is deleted.
  168. clearActiveDesc: function(treeId) {
  169. this.unmarkActiveDesc_(this.getActiveDescId(treeId));
  170. delete this.activeDescendantIds_[treeId];
  171. },
  172. // Make a given node the active descendant of a given tree.
  173. setActiveDesc: function(newActiveDescId, treeId) {
  174. this.unmarkActiveDesc_(this.getActiveDescId(treeId));
  175. this.markActiveDesc_(newActiveDescId);
  176. this.activeDescendantIds_[treeId] = newActiveDescId;
  177. // Scroll the new active desc into view, if needed. This has no effect
  178. // for blind users, but is helpful for sighted onlookers.
  179. var activeDescNode = document.getElementById(newActiveDescId);
  180. var documentNode = document.body || document.documentElement;
  181. if (activeDescNode.offsetTop < documentNode.scrollTop ||
  182. activeDescNode.offsetTop >
  183. documentNode.scrollTop + window.innerHeight) {
  184. window.scrollTo(0, activeDescNode.offsetTop);
  185. }
  186. },
  187. initActiveDesc: function(treeId) {
  188. // Set the active desc to the first child in this tree.
  189. var tree = document.getElementById(treeId);
  190. this.setActiveDesc(this.getFirstChild(tree).id, treeId);
  191. },
  192. getTreeIdForBlock: function(blockId) {
  193. // Walk up the DOM until we get to the root node of the tree.
  194. var domNode = document.getElementById(blockId + 'blockRoot');
  195. while (!domNode.classList.contains('blocklyTree')) {
  196. domNode = domNode.parentNode;
  197. }
  198. return domNode.id;
  199. },
  200. focusOnBlock: function(blockId) {
  201. // Set focus to the tree containing the given block, and set the active
  202. // desc for this tree to the given block.
  203. var domNode = document.getElementById(blockId + 'blockRoot');
  204. // Walk up the DOM until we get to the root node of the tree.
  205. while (!domNode.classList.contains('blocklyTree')) {
  206. domNode = domNode.parentNode;
  207. }
  208. domNode.focus();
  209. // We need to wait a while to set the active desc, because domNode takes
  210. // a while to be given an ID if a new tree has just been created.
  211. // TODO(sll): Make this more deterministic.
  212. var that = this;
  213. setTimeout(function() {
  214. that.setActiveDesc(blockId + 'blockRoot', domNode.id);
  215. }, 100);
  216. },
  217. onWorkspaceToolbarKeypress: function(e, treeId) {
  218. if (e.keyCode == 9) {
  219. // Tab key.
  220. var destinationTreeId =
  221. e.shiftKey ? this.getIdOfPreviousTree_(treeId) :
  222. this.getIdOfNextTree_(treeId);
  223. if (destinationTreeId) {
  224. this.notifyUserAboutCurrentTree_(destinationTreeId);
  225. }
  226. }
  227. },
  228. isButtonOrFieldNode_: function(node) {
  229. return ['BUTTON', 'INPUT'].indexOf(node.tagName) != -1;
  230. },
  231. getNextActiveDescWhenBlockIsDeleted: function(blockRootNode) {
  232. // Go up a level, if possible.
  233. var nextNode = blockRootNode.parentNode;
  234. while (nextNode && nextNode.tagName != 'LI') {
  235. nextNode = nextNode.parentNode;
  236. }
  237. if (nextNode) {
  238. return nextNode;
  239. }
  240. // Otherwise, go to the next sibling.
  241. var nextSibling = this.getNextSibling(blockRootNode);
  242. if (nextSibling) {
  243. return nextSibling;
  244. }
  245. // Otherwise, go to the previous sibling.
  246. var previousSibling = this.getPreviousSibling(blockRootNode);
  247. if (previousSibling) {
  248. return previousSibling;
  249. }
  250. // Otherwise, this is a top-level isolated block, which means that
  251. // something's gone wrong and this function should not have been called
  252. // in the first place.
  253. console.error('Could not handle deletion of block.' + blockRootNode);
  254. },
  255. notifyUserAboutCurrentTree_: function(treeId) {
  256. if (this.getToolboxTreeNode_().id == treeId) {
  257. this.notificationsService.setStatusMessage('Now in toolbox.');
  258. } else {
  259. var workspaceTreeNodes = this.getWorkspaceTreeNodes_();
  260. for (var i = 0; i < workspaceTreeNodes.length; i++) {
  261. if (workspaceTreeNodes[i].id == treeId) {
  262. this.notificationsService.setStatusMessage(
  263. 'Now in workspace group ' + (i + 1) + ' of ' +
  264. workspaceTreeNodes.length);
  265. }
  266. }
  267. }
  268. },
  269. isIsolatedTopLevelBlock_: function(block) {
  270. // Returns whether the given block is at the top level, and has no
  271. // siblings.
  272. var blockIsAtTopLevel = !block.getParent();
  273. var blockHasNoSiblings = (
  274. (!block.nextConnection ||
  275. !block.nextConnection.targetConnection) &&
  276. (!block.previousConnection ||
  277. !block.previousConnection.targetConnection));
  278. return blockIsAtTopLevel && blockHasNoSiblings;
  279. },
  280. removeBlockAndSetFocus: function(block, blockRootNode, deleteBlockFunc) {
  281. // This method runs the given deletion function and then does one of two
  282. // things:
  283. // - If the block is an isolated top-level block, it shifts the tree
  284. // focus.
  285. // - Otherwise, it sets the correct new active desc for the current tree.
  286. var treeId = this.getTreeIdForBlock(block.id);
  287. if (this.isIsolatedTopLevelBlock_(block)) {
  288. var nextNodeToFocusOn = this.getNodeToFocusOnWhenTreeIsDeleted(treeId);
  289. this.clearActiveDesc(treeId);
  290. deleteBlockFunc();
  291. // Invoke a digest cycle, so that the DOM settles.
  292. setTimeout(function() {
  293. nextNodeToFocusOn.focus();
  294. });
  295. } else {
  296. var nextActiveDesc = this.getNextActiveDescWhenBlockIsDeleted(
  297. blockRootNode);
  298. this.runWhilePreservingFocus(
  299. deleteBlockFunc, treeId, nextActiveDesc.id);
  300. }
  301. },
  302. cutBlock_: function(block, blockRootNode) {
  303. var blockDescription = this.utilsService.getBlockDescription(block);
  304. var that = this;
  305. this.removeBlockAndSetFocus(block, blockRootNode, function() {
  306. that.clipboardService.cut(block);
  307. });
  308. setTimeout(function() {
  309. if (that.utilsService.isWorkspaceEmpty()) {
  310. that.notificationsService.setStatusMessage(
  311. blockDescription + ' cut. Workspace is empty.');
  312. } else {
  313. that.notificationsService.setStatusMessage(
  314. blockDescription + ' cut. Now on workspace.');
  315. }
  316. });
  317. },
  318. copyBlock_: function(block) {
  319. var blockDescription = this.utilsService.getBlockDescription(block);
  320. this.clipboardService.copy(block);
  321. this.notificationsService.setStatusMessage(
  322. blockDescription + ' ' + Blockly.Msg.COPIED_BLOCK_MSG);
  323. },
  324. pasteToConnection: function(block, connection) {
  325. if (this.clipboardService.isClipboardEmpty()) {
  326. return;
  327. }
  328. var destinationTreeId = this.getTreeIdForBlock(
  329. connection.getSourceBlock().id);
  330. this.clearActiveDesc(destinationTreeId);
  331. var newBlockId = this.clipboardService.pasteFromClipboard(connection);
  332. // Invoke a digest cycle, so that the DOM settles.
  333. var that = this;
  334. setTimeout(function() {
  335. that.focusOnBlock(newBlockId);
  336. });
  337. },
  338. onKeypress: function(e, tree) {
  339. var treeId = tree.id;
  340. var activeDesc = document.getElementById(this.getActiveDescId(treeId));
  341. if (!activeDesc) {
  342. console.error('ERROR: no active descendant for current tree.');
  343. this.initActiveDesc(treeId);
  344. return;
  345. }
  346. if (e.altKey) {
  347. // Do not intercept combinations such as Alt+Home.
  348. return;
  349. }
  350. if (e.ctrlKey) {
  351. var activeDesc = document.getElementById(this.getActiveDescId(treeId));
  352. // Scout up the tree to see whether we're in the toolbox or workspace.
  353. var scoutNode = activeDesc;
  354. var TARGET_TAG_NAMES = ['BLOCKLY-TOOLBOX', 'BLOCKLY-WORKSPACE'];
  355. while (TARGET_TAG_NAMES.indexOf(scoutNode.tagName) === -1) {
  356. scoutNode = scoutNode.parentNode;
  357. }
  358. var inToolbox = (scoutNode.tagName == 'BLOCKLY-TOOLBOX');
  359. // Disallow cutting and pasting in the toolbox.
  360. if (inToolbox && e.keyCode != 67) {
  361. if (e.keyCode == 86) {
  362. this.notificationsService.setStatusMessage(
  363. 'Cannot paste block in toolbox.');
  364. } else if (e.keyCode == 88) {
  365. this.notificationsService.setStatusMessage(
  366. 'Cannot cut block in toolbox. Try copying instead.');
  367. }
  368. }
  369. // Starting from the activeDesc, walk up the tree until we find the
  370. // root of the current block.
  371. var blockRootSuffix = inToolbox ? 'toolboxBlockRoot' : 'blockRoot';
  372. var putativeBlockRootNode = activeDesc;
  373. while (putativeBlockRootNode.id.indexOf(blockRootSuffix) === -1) {
  374. putativeBlockRootNode = putativeBlockRootNode.parentNode;
  375. }
  376. var blockRootNode = putativeBlockRootNode;
  377. var blockId = blockRootNode.id.substring(
  378. 0, blockRootNode.id.length - blockRootSuffix.length);
  379. var block = inToolbox ?
  380. this.getToolboxBlockById(blockId) :
  381. this.utilsService.getBlockById(blockId);
  382. if (e.keyCode == 88) {
  383. // Cut block.
  384. this.cutBlock_(block, blockRootNode);
  385. } else if (e.keyCode == 67) {
  386. // Copy block. Note that, in this case, we might be in the workspace
  387. // or toolbox.
  388. this.copyBlock_(block);
  389. } else if (e.keyCode == 86) {
  390. // Paste block, if possible.
  391. var targetConnection =
  392. e.shiftKey ? block.previousConnection : block.nextConnection;
  393. this.pasteToConnection(block, targetConnection);
  394. }
  395. } else if (document.activeElement.tagName == 'INPUT') {
  396. // For input fields, only Esc and Tab keystrokes are handled specially.
  397. if (e.keyCode == 27 || e.keyCode == 9) {
  398. // For Esc and Tab keys, the focus is removed from the input field.
  399. this.focusOnCurrentTree_(treeId);
  400. if (e.keyCode == 9) {
  401. var destinationTreeId =
  402. e.shiftKey ? this.getIdOfPreviousTree_(treeId) :
  403. this.getIdOfNextTree_(treeId);
  404. if (destinationTreeId) {
  405. this.notifyUserAboutCurrentTree_(destinationTreeId);
  406. }
  407. }
  408. // Allow Tab keypresses to go through.
  409. if (e.keyCode == 27) {
  410. e.preventDefault();
  411. e.stopPropagation();
  412. }
  413. }
  414. } else {
  415. // Outside an input field, Enter, Tab and navigation keys are all
  416. // recognized.
  417. if (e.keyCode == 13) {
  418. // Enter key. The user wants to interact with a button, interact with
  419. // an input field, or move down one level.
  420. // Algorithm to find the field: do a DFS through the children until
  421. // we find an INPUT or BUTTON element (in which case we use it).
  422. // Truncate the search at child LI elements.
  423. var found = false;
  424. var dfsStack = Array.from(activeDesc.children);
  425. while (dfsStack.length) {
  426. var currentNode = dfsStack.shift();
  427. if (currentNode.tagName == 'BUTTON') {
  428. this.moveUpOneLevel_(treeId);
  429. currentNode.click();
  430. found = true;
  431. break;
  432. } else if (currentNode.tagName == 'INPUT') {
  433. currentNode.focus();
  434. currentNode.select();
  435. this.notificationsService.setStatusMessage(
  436. 'Type a value, then press Escape to exit');
  437. found = true;
  438. break;
  439. } else if (currentNode.tagName == 'LI') {
  440. continue;
  441. }
  442. if (currentNode.children) {
  443. var reversedChildren = Array.from(currentNode.children).reverse();
  444. reversedChildren.forEach(function(childNode) {
  445. dfsStack.unshift(childNode);
  446. });
  447. }
  448. }
  449. // If we cannot find a field to interact with, we try moving down a
  450. // level instead.
  451. if (!found) {
  452. this.moveDownOneLevel_(treeId);
  453. }
  454. } else if (e.keyCode == 9) {
  455. // Tab key. Note that allowing the event to propagate through is
  456. // intentional.
  457. var destinationTreeId =
  458. e.shiftKey ? this.getIdOfPreviousTree_(treeId) :
  459. this.getIdOfNextTree_(treeId);
  460. if (destinationTreeId) {
  461. this.notifyUserAboutCurrentTree_(destinationTreeId);
  462. }
  463. } else if (e.keyCode == 27) {
  464. this.moveUpOneLevel_(treeId);
  465. } else if (e.keyCode >= 35 && e.keyCode <= 40) {
  466. // End, home, and arrow keys.
  467. if (e.keyCode == 35) {
  468. // End key. Go to the last sibling in the subtree.
  469. var finalSibling = this.getFinalSibling(activeDesc);
  470. if (finalSibling) {
  471. this.setActiveDesc(finalSibling.id, treeId);
  472. }
  473. } else if (e.keyCode == 36) {
  474. // Home key. Go to the first sibling in the subtree.
  475. var initialSibling = this.getInitialSibling(activeDesc);
  476. if (initialSibling) {
  477. this.setActiveDesc(initialSibling.id, treeId);
  478. }
  479. } else if (e.keyCode == 37) {
  480. // Left arrow key. Go up a level, if possible.
  481. this.moveUpOneLevel_(treeId);
  482. } else if (e.keyCode == 38) {
  483. // Up arrow key. Go to the previous sibling, if possible.
  484. var prevSibling = this.getPreviousSibling(activeDesc);
  485. if (prevSibling) {
  486. this.setActiveDesc(prevSibling.id, treeId);
  487. } else {
  488. var statusMessage = 'Reached top of list.';
  489. if (this.getParentListElement_(activeDesc)) {
  490. statusMessage += ' Press left to go to parent list.';
  491. }
  492. this.notificationsService.setStatusMessage(statusMessage);
  493. }
  494. } else if (e.keyCode == 39) {
  495. // Right arrow key. Go down a level, if possible.
  496. this.moveDownOneLevel_(treeId);
  497. } else if (e.keyCode == 40) {
  498. // Down arrow key. Go to the next sibling, if possible.
  499. var nextSibling = this.getNextSibling(activeDesc);
  500. if (nextSibling) {
  501. this.setActiveDesc(nextSibling.id, treeId);
  502. } else {
  503. this.notificationsService.setStatusMessage(
  504. 'Reached bottom of list.');
  505. }
  506. }
  507. e.preventDefault();
  508. e.stopPropagation();
  509. }
  510. }
  511. },
  512. moveDownOneLevel_: function(treeId) {
  513. var activeDesc = document.getElementById(this.getActiveDescId(treeId));
  514. var firstChild = this.getFirstChild(activeDesc);
  515. if (firstChild) {
  516. this.setActiveDesc(firstChild.id, treeId);
  517. }
  518. },
  519. moveUpOneLevel_: function(treeId) {
  520. var activeDesc = document.getElementById(this.getActiveDescId(treeId));
  521. var nextNode = this.getParentListElement_(activeDesc);
  522. if (nextNode) {
  523. this.setActiveDesc(nextNode.id, treeId);
  524. }
  525. },
  526. getParentListElement_: function(element) {
  527. var nextNode = element.parentNode;
  528. while (nextNode && nextNode.tagName != 'LI') {
  529. nextNode = nextNode.parentNode;
  530. }
  531. return nextNode;
  532. },
  533. getFirstChild: function(element) {
  534. if (!element) {
  535. return element;
  536. } else {
  537. var childList = element.children;
  538. for (var i = 0; i < childList.length; i++) {
  539. if (childList[i].tagName == 'LI') {
  540. return childList[i];
  541. } else {
  542. var potentialElement = this.getFirstChild(childList[i]);
  543. if (potentialElement) {
  544. return potentialElement;
  545. }
  546. }
  547. }
  548. return null;
  549. }
  550. },
  551. getFinalSibling: function(element) {
  552. while (true) {
  553. var nextSibling = this.getNextSibling(element);
  554. if (nextSibling && nextSibling.id != element.id) {
  555. element = nextSibling;
  556. } else {
  557. return element;
  558. }
  559. }
  560. },
  561. getInitialSibling: function(element) {
  562. while (true) {
  563. var previousSibling = this.getPreviousSibling(element);
  564. if (previousSibling && previousSibling.id != element.id) {
  565. element = previousSibling;
  566. } else {
  567. return element;
  568. }
  569. }
  570. },
  571. getNextSibling: function(element) {
  572. if (element.nextElementSibling) {
  573. // If there is a sibling, find the list element child of the sibling.
  574. var node = element.nextElementSibling;
  575. if (node.tagName == 'LI') {
  576. return node;
  577. } else {
  578. // getElementsByTagName returns in DFS order, therefore the first
  579. // element is the first relevant list child.
  580. return node.getElementsByTagName('li')[0];
  581. }
  582. } else {
  583. var parent = element.parentNode;
  584. while (parent && parent.tagName != 'OL') {
  585. if (parent.nextElementSibling) {
  586. var node = parent.nextElementSibling;
  587. if (node.tagName == 'LI') {
  588. return node;
  589. } else {
  590. return this.getFirstChild(node);
  591. }
  592. } else {
  593. parent = parent.parentNode;
  594. }
  595. }
  596. return null;
  597. }
  598. },
  599. getPreviousSibling: function(element) {
  600. if (element.previousElementSibling) {
  601. var sibling = element.previousElementSibling;
  602. if (sibling.tagName == 'LI') {
  603. return sibling;
  604. } else {
  605. return this.getLastChild(sibling);
  606. }
  607. } else {
  608. var parent = element.parentNode;
  609. while (parent) {
  610. if (parent.tagName == 'OL') {
  611. break;
  612. }
  613. if (parent.previousElementSibling) {
  614. var node = parent.previousElementSibling;
  615. if (node.tagName == 'LI') {
  616. return node;
  617. } else {
  618. // Find the last list element child of the sibling of the parent.
  619. return this.getLastChild(node);
  620. }
  621. } else {
  622. parent = parent.parentNode;
  623. }
  624. }
  625. return null;
  626. }
  627. },
  628. getLastChild: function(element) {
  629. if (!element) {
  630. return element;
  631. } else {
  632. var childList = element.children;
  633. for (var i = childList.length - 1; i >= 0; i--) {
  634. // Find the last child that is a list element.
  635. if (childList[i].tagName == 'LI') {
  636. return childList[i];
  637. } else {
  638. var potentialElement = this.getLastChild(childList[i]);
  639. if (potentialElement) {
  640. return potentialElement;
  641. }
  642. }
  643. }
  644. return null;
  645. }
  646. }
  647. });