PopupMenuLinks.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. /*
  2. * This content is licensed according to the W3C Software License at
  3. * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
  4. */
  5. var PopupMenu = function (domNode, controllerObj) {
  6. var elementChildren,
  7. msgPrefix = 'PopupMenu constructor argument domNode ';
  8. // Check whether domNode is a DOM element
  9. if (!domNode instanceof Element) {
  10. throw new TypeError(msgPrefix + 'is not a DOM Element.');
  11. }
  12. // Check whether domNode has child elements
  13. if (domNode.childElementCount === 0) {
  14. throw new Error(msgPrefix + 'has no element children.');
  15. }
  16. // Check whether domNode descendant elements have A elements
  17. var childElement = domNode.firstElementChild;
  18. while (childElement) {
  19. var menuitem = childElement.firstElementChild;
  20. if (menuitem && menuitem === 'A') {
  21. throw new Error(msgPrefix + 'has descendant elements that are not A elements.');
  22. }
  23. childElement = childElement.nextElementSibling;
  24. }
  25. this.isMenubar = false;
  26. this.domNode = domNode;
  27. this.controller = controllerObj;
  28. this.menuitems = []; // See PopupMenu init method
  29. this.firstChars = []; // See PopupMenu init method
  30. this.firstItem = null; // See PopupMenu init method
  31. this.lastItem = null; // See PopupMenu init method
  32. this.hasFocus = false; // See MenuItem handleFocus, handleBlur
  33. this.hasHover = false; // See PopupMenu handleMouseover, handleMouseout
  34. };
  35. /*
  36. * @method PopupMenu.prototype.init
  37. *
  38. * @desc
  39. * Add domNode event listeners for mouseover and mouseout. Traverse
  40. * domNode children to configure each menuitem and populate menuitems
  41. * array. Initialize firstItem and lastItem properties.
  42. */
  43. PopupMenu.prototype.init = function () {
  44. var childElement, menuElement, menuItem, textContent, numItems, label;
  45. // Configure the domNode itself
  46. this.domNode.addEventListener('mouseover', this.handleMouseover.bind(this));
  47. this.domNode.addEventListener('mouseout', this.handleMouseout.bind(this));
  48. // Traverse the element children of domNode: configure each with
  49. // menuitem role behavior and store reference in menuitems array.
  50. childElement = this.domNode.firstElementChild;
  51. while (childElement) {
  52. menuElement = childElement.firstElementChild;
  53. if (menuElement && menuElement.tagName === 'A') {
  54. menuItem = new MenuItem(menuElement, this);
  55. menuItem.init();
  56. this.menuitems.push(menuItem);
  57. textContent = menuElement.textContent.trim();
  58. this.firstChars.push(textContent.substring(0, 1).toLowerCase());
  59. }
  60. childElement = childElement.nextElementSibling;
  61. }
  62. // Use populated menuitems array to initialize firstItem and lastItem.
  63. numItems = this.menuitems.length;
  64. if (numItems > 0) {
  65. this.firstItem = this.menuitems[ 0 ];
  66. this.lastItem = this.menuitems[ numItems - 1 ];
  67. }
  68. };
  69. /* EVENT HANDLERS */
  70. PopupMenu.prototype.handleMouseover = function (event) {
  71. this.hasHover = true;
  72. };
  73. PopupMenu.prototype.handleMouseout = function (event) {
  74. this.hasHover = false;
  75. setTimeout(this.close.bind(this, false), 1);
  76. };
  77. /* FOCUS MANAGEMENT METHODS */
  78. PopupMenu.prototype.setFocusToController = function (command, flag) {
  79. if (typeof command !== 'string') {
  80. command = '';
  81. }
  82. function setFocusToMenubarItem (controller, close) {
  83. while (controller) {
  84. if (controller.isMenubarItem) {
  85. controller.domNode.focus();
  86. return controller;
  87. }
  88. else {
  89. if (close) {
  90. controller.menu.close(true);
  91. }
  92. controller.hasFocus = false;
  93. }
  94. controller = controller.menu.controller;
  95. }
  96. return false;
  97. }
  98. if (command === '') {
  99. if (this.controller && this.controller.domNode) {
  100. this.controller.domNode.focus();
  101. }
  102. return;
  103. }
  104. if (!this.controller.isMenubarItem) {
  105. this.controller.domNode.focus();
  106. this.close();
  107. if (command === 'next') {
  108. var menubarItem = setFocusToMenubarItem(this.controller, false);
  109. if (menubarItem) {
  110. menubarItem.menu.setFocusToNextItem(menubarItem, flag);
  111. }
  112. }
  113. }
  114. else {
  115. if (command === 'previous') {
  116. this.controller.menu.setFocusToPreviousItem(this.controller, flag);
  117. }
  118. else if (command === 'next') {
  119. this.controller.menu.setFocusToNextItem(this.controller, flag);
  120. }
  121. }
  122. };
  123. PopupMenu.prototype.setFocusToFirstItem = function () {
  124. this.firstItem.domNode.focus();
  125. };
  126. PopupMenu.prototype.setFocusToLastItem = function () {
  127. this.lastItem.domNode.focus();
  128. };
  129. PopupMenu.prototype.setFocusToPreviousItem = function (currentItem) {
  130. var index;
  131. if (currentItem === this.firstItem) {
  132. this.lastItem.domNode.focus();
  133. }
  134. else {
  135. index = this.menuitems.indexOf(currentItem);
  136. this.menuitems[ index - 1 ].domNode.focus();
  137. }
  138. };
  139. PopupMenu.prototype.setFocusToNextItem = function (currentItem) {
  140. var index;
  141. if (currentItem === this.lastItem) {
  142. this.firstItem.domNode.focus();
  143. }
  144. else {
  145. index = this.menuitems.indexOf(currentItem);
  146. this.menuitems[ index + 1 ].domNode.focus();
  147. }
  148. };
  149. PopupMenu.prototype.setFocusByFirstCharacter = function (currentItem, char) {
  150. var start, index, char = char.toLowerCase();
  151. // Get start index for search based on position of currentItem
  152. start = this.menuitems.indexOf(currentItem) + 1;
  153. if (start === this.menuitems.length) {
  154. start = 0;
  155. }
  156. // Check remaining slots in the menu
  157. index = this.getIndexFirstChars(start, char);
  158. // If not found in remaining slots, check from beginning
  159. if (index === -1) {
  160. index = this.getIndexFirstChars(0, char);
  161. }
  162. // If match was found...
  163. if (index > -1) {
  164. this.menuitems[ index ].domNode.focus();
  165. }
  166. };
  167. PopupMenu.prototype.getIndexFirstChars = function (startIndex, char) {
  168. for (var i = startIndex; i < this.firstChars.length; i++) {
  169. if (char === this.firstChars[ i ]) {
  170. return i;
  171. }
  172. }
  173. return -1;
  174. };
  175. /* MENU DISPLAY METHODS */
  176. PopupMenu.prototype.open = function () {
  177. // Get position and bounding rectangle of controller object's DOM node
  178. var rect = this.controller.domNode.getBoundingClientRect();
  179. // Set CSS properties
  180. if (!this.controller.isMenubarItem) {
  181. this.domNode.parentNode.style.position = 'relative';
  182. this.domNode.style.display = 'block';
  183. this.domNode.style.position = 'absolute';
  184. this.domNode.style.left = rect.width + 'px';
  185. this.domNode.style.zIndex = 100;
  186. }
  187. else {
  188. this.domNode.style.display = 'block';
  189. this.domNode.style.position = 'absolute';
  190. this.domNode.style.top = (rect.height - 1) + 'px';
  191. this.domNode.style.zIndex = 100;
  192. }
  193. this.controller.setExpanded(true);
  194. };
  195. PopupMenu.prototype.close = function (force) {
  196. var controllerHasHover = this.controller.hasHover;
  197. var hasFocus = this.hasFocus;
  198. for (var i = 0; i < this.menuitems.length; i++) {
  199. var mi = this.menuitems[i];
  200. if (mi.popupMenu) {
  201. hasFocus = hasFocus | mi.popupMenu.hasFocus;
  202. }
  203. }
  204. if (!this.controller.isMenubarItem) {
  205. controllerHasHover = false;
  206. }
  207. if (force || (!hasFocus && !this.hasHover && !controllerHasHover)) {
  208. this.domNode.style.display = 'none';
  209. this.domNode.style.zIndex = 0;
  210. this.controller.setExpanded(false);
  211. }
  212. };