/* * This content is licensed according to the W3C Software License at * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document */ var PopupMenu = function (domNode, controllerObj) { var elementChildren, msgPrefix = 'PopupMenu constructor argument domNode '; // Check whether domNode is a DOM element if (!domNode instanceof Element) { throw new TypeError(msgPrefix + 'is not a DOM Element.'); } // Check whether domNode has child elements if (domNode.childElementCount === 0) { throw new Error(msgPrefix + 'has no element children.'); } // Check whether domNode descendant elements have A elements var childElement = domNode.firstElementChild; while (childElement) { var menuitem = childElement.firstElementChild; if (menuitem && menuitem === 'A') { throw new Error(msgPrefix + 'has descendant elements that are not A elements.'); } childElement = childElement.nextElementSibling; } this.isMenubar = false; this.domNode = domNode; this.controller = controllerObj; this.menuitems = []; // See PopupMenu init method this.firstChars = []; // See PopupMenu init method this.firstItem = null; // See PopupMenu init method this.lastItem = null; // See PopupMenu init method this.hasFocus = false; // See MenuItem handleFocus, handleBlur this.hasHover = false; // See PopupMenu handleMouseover, handleMouseout }; /* * @method PopupMenu.prototype.init * * @desc * Add domNode event listeners for mouseover and mouseout. Traverse * domNode children to configure each menuitem and populate menuitems * array. Initialize firstItem and lastItem properties. */ PopupMenu.prototype.init = function () { var childElement, menuElement, menuItem, textContent, numItems, label; // Configure the domNode itself this.domNode.addEventListener('mouseover', this.handleMouseover.bind(this)); this.domNode.addEventListener('mouseout', this.handleMouseout.bind(this)); // Traverse the element children of domNode: configure each with // menuitem role behavior and store reference in menuitems array. childElement = this.domNode.firstElementChild; while (childElement) { menuElement = childElement.firstElementChild; if (menuElement && menuElement.tagName === 'A') { menuItem = new MenuItem(menuElement, this); menuItem.init(); this.menuitems.push(menuItem); textContent = menuElement.textContent.trim(); this.firstChars.push(textContent.substring(0, 1).toLowerCase()); } childElement = childElement.nextElementSibling; } // Use populated menuitems array to initialize firstItem and lastItem. numItems = this.menuitems.length; if (numItems > 0) { this.firstItem = this.menuitems[ 0 ]; this.lastItem = this.menuitems[ numItems - 1 ]; } }; /* EVENT HANDLERS */ PopupMenu.prototype.handleMouseover = function (event) { this.hasHover = true; }; PopupMenu.prototype.handleMouseout = function (event) { this.hasHover = false; setTimeout(this.close.bind(this, false), 1); }; /* FOCUS MANAGEMENT METHODS */ PopupMenu.prototype.setFocusToController = function (command, flag) { if (typeof command !== 'string') { command = ''; } function setFocusToMenubarItem (controller, close) { while (controller) { if (controller.isMenubarItem) { controller.domNode.focus(); return controller; } else { if (close) { controller.menu.close(true); } controller.hasFocus = false; } controller = controller.menu.controller; } return false; } if (command === '') { if (this.controller && this.controller.domNode) { this.controller.domNode.focus(); } return; } if (!this.controller.isMenubarItem) { this.controller.domNode.focus(); this.close(); if (command === 'next') { var menubarItem = setFocusToMenubarItem(this.controller, false); if (menubarItem) { menubarItem.menu.setFocusToNextItem(menubarItem, flag); } } } else { if (command === 'previous') { this.controller.menu.setFocusToPreviousItem(this.controller, flag); } else if (command === 'next') { this.controller.menu.setFocusToNextItem(this.controller, flag); } } }; PopupMenu.prototype.setFocusToFirstItem = function () { this.firstItem.domNode.focus(); }; PopupMenu.prototype.setFocusToLastItem = function () { this.lastItem.domNode.focus(); }; PopupMenu.prototype.setFocusToPreviousItem = function (currentItem) { var index; if (currentItem === this.firstItem) { this.lastItem.domNode.focus(); } else { index = this.menuitems.indexOf(currentItem); this.menuitems[ index - 1 ].domNode.focus(); } }; PopupMenu.prototype.setFocusToNextItem = function (currentItem) { var index; if (currentItem === this.lastItem) { this.firstItem.domNode.focus(); } else { index = this.menuitems.indexOf(currentItem); this.menuitems[ index + 1 ].domNode.focus(); } }; PopupMenu.prototype.setFocusByFirstCharacter = function (currentItem, char) { var start, index, char = char.toLowerCase(); // Get start index for search based on position of currentItem start = this.menuitems.indexOf(currentItem) + 1; if (start === this.menuitems.length) { start = 0; } // Check remaining slots in the menu index = this.getIndexFirstChars(start, char); // If not found in remaining slots, check from beginning if (index === -1) { index = this.getIndexFirstChars(0, char); } // If match was found... if (index > -1) { this.menuitems[ index ].domNode.focus(); } }; PopupMenu.prototype.getIndexFirstChars = function (startIndex, char) { for (var i = startIndex; i < this.firstChars.length; i++) { if (char === this.firstChars[ i ]) { return i; } } return -1; }; /* MENU DISPLAY METHODS */ PopupMenu.prototype.open = function () { // Get position and bounding rectangle of controller object's DOM node var rect = this.controller.domNode.getBoundingClientRect(); // Set CSS properties if (!this.controller.isMenubarItem) { this.domNode.parentNode.style.position = 'relative'; this.domNode.style.display = 'block'; this.domNode.style.position = 'absolute'; this.domNode.style.left = rect.width + 'px'; this.domNode.style.zIndex = 100; } else { this.domNode.style.display = 'block'; this.domNode.style.position = 'absolute'; this.domNode.style.top = (rect.height - 1) + 'px'; this.domNode.style.zIndex = 100; } this.controller.setExpanded(true); }; PopupMenu.prototype.close = function (force) { var controllerHasHover = this.controller.hasHover; var hasFocus = this.hasFocus; for (var i = 0; i < this.menuitems.length; i++) { var mi = this.menuitems[i]; if (mi.popupMenu) { hasFocus = hasFocus | mi.popupMenu.hasFocus; } } if (!this.controller.isMenubarItem) { controllerHasHover = false; } if (force || (!hasFocus && !this.hasHover && !controllerHasHover)) { this.domNode.style.display = 'none'; this.domNode.style.zIndex = 0; this.controller.setExpanded(false); } };