UNPKG

iuap-design

Version:
406 lines (368 loc) 16.5 kB
u.Menu = u.BaseComponent.extend({ _Keycodes: { ENTER: 13, ESCAPE: 27, SPACE: 32, UP_ARROW: 38, DOWN_ARROW: 40 }, _CssClasses: { BOTTOM_LEFT: 'u-menu-bottom-left', // This is the default. BOTTOM_RIGHT: 'u-menu-bottom-right', TOP_LEFT: 'u-menu-top-left', TOP_RIGHT: 'u-menu-top-right', UNALIGNED: 'u-menu-unaligned' }, init: function () { // Create container for the menu. var container = document.createElement('div'); u.addClass(container, 'u-menu-container'); this.element.parentElement.insertBefore(container, this.element); this.element.parentElement.removeChild(this.element); container.appendChild(this.element); this._container = container; // Create outline for the menu (shadow and background). var outline = document.createElement('div'); u.addClass(outline, 'u-menu-outline'); this._outline = outline; container.insertBefore(outline, this.element); // Find the "for" element and bind events to it. var forElId = this.element.getAttribute('for') || this.element.getAttribute('data-u-for'); var forEl = null; if (forElId) { forEl = document.getElementById(forElId); if (forEl) { this.for_element = forEl; u.on(forEl,'click', this._handleForClick.bind(this)); u.on(forEl,'keydown', this._handleForKeyboardEvent.bind(this)) } } var items = this.element.querySelectorAll('.u-menu-item'); this._boundItemKeydown = this._handleItemKeyboardEvent.bind(this); this._boundItemClick = this._handleItemClick.bind(this); for (var i = 0; i < items.length; i++) { // Add a listener to each menu item. u.on(items[i],'click', this._boundItemClick); // Add a tab index to each menu item. items[i].tabIndex = '-1'; // Add a keyboard listener to each menu item. u.on(items[i],'keydown', this._boundItemKeydown); } for (i = 0; i < items.length; i++) { var item = items[i]; var rippleContainer = document.createElement('span'); u.addClass(rippleContainer, 'u-ripple'); item.appendChild(rippleContainer); new URipple(item) } //} // Copy alignment classes to the container, so the outline can use them. if (u.hasClass(this.element, 'u-menu-bottom-left')) { u.addClass(this._outline, 'u-menu-bottom-left'); } if (u.hasClass(this.element, 'u-menu-bottom-right')) { u.addClass(this._outline, 'u-menu-bottom-right'); } if (u.hasClass(this.element, 'u-menu-top-left')) { u.addClass(this._outline, 'u-menu-top-left'); } if (u.hasClass(this.element, 'u-menu-top-right')) { u.addClass(this._outline, 'u-menu-top-right'); } if (u.hasClass(this.element, 'u-menu-unaligned')) { u.addClass(this._outline, 'u-menu-unaligned'); } u.addClass(container, 'is-upgraded'); }, _handleForClick: function (evt) { if (this.element && this.for_element) { var rect = this.for_element.getBoundingClientRect(); var forRect = this.for_element.parentElement.getBoundingClientRect(); if (u.hasClass(this.element, 'u-menu-unaligned')) { // Do not position the menu automatically. Requires the developer to // manually specify position. } else if (u.hasClass(this.element, 'u-menu-bottom-right')) { // Position below the "for" element, aligned to its right. this._container.style.right = (forRect.right - rect.right) + 'px'; this._container.style.top = this.for_element.offsetTop + this.for_element.offsetHeight + 'px'; } else if (u.hasClass(this.element, 'u-menu-top-left')) { // Position above the "for" element, aligned to its left. this._container.style.left = this.for_element.offsetLeft + 'px'; this._container.style.bottom = (forRect.bottom - rect.top) + 'px'; } else if (u.hasClass(this.element, 'u-menu-top-right')) { // Position above the "for" element, aligned to its right. this._container.style.right = (forRect.right - rect.right) + 'px'; this._container.style.bottom = (forRect.bottom - rect.top) + 'px'; } else { // Default: position below the "for" element, aligned to its left. this._container.style.left = this.for_element.offsetLeft + 'px'; this._container.style.top = this.for_element.offsetTop + this.for_element.offsetHeight + 'px'; } } this.toggle(evt); }, /** * Handles a keyboard event on the "for" element. * * @param {Event} evt The event that fired. * @private */ _handleForKeyboardEvent: function (evt) { if (this.element && this._container && this.for_element) { var items = this.element.querySelectorAll('.u-menu-item:not([disabled])'); if (items && items.length > 0 && u.hasClass(this._container, 'is-visible')) { if (evt.keyCode === this._Keycodes.UP_ARROW) { u.stopEvent(evt); // evt.preventDefault(); items[items.length - 1].focus(); } else if (evt.keyCode === this._Keycodes.DOWN_ARROW) { u.stopEvent(evt); // evt.preventDefault(); items[0].focus(); } } } }, /** * Handles a keyboard event on an item. * * @param {Event} evt The event that fired. * @private */ _handleItemKeyboardEvent: function (evt) { if (this.element && this._container) { var items = this.element.querySelectorAll('.u-menu-item:not([disabled])'); if (items && items.length > 0 && u.hasClass(this._container, 'is-visible')) { var currentIndex = Array.prototype.slice.call(items).indexOf(evt.target); if (evt.keyCode === this._Keycodes.UP_ARROW) { u.stopEvent(evt); // evt.preventDefault(); if (currentIndex > 0) { items[currentIndex - 1].focus(); } else { items[items.length - 1].focus(); } } else if (evt.keyCode === this._Keycodes.DOWN_ARROW) { u.stopEvent(evt); // evt.preventDefault(); if (items.length > currentIndex + 1) { items[currentIndex + 1].focus(); } else { items[0].focus(); } } else if (evt.keyCode === this._Keycodes.SPACE || evt.keyCode === this._Keycodes.ENTER) { u.stopEvent(evt); // evt.preventDefault(); // Send mousedown and mouseup to trigger ripple. var e = new MouseEvent('mousedown'); evt.target.dispatchEvent(e); e = new MouseEvent('mouseup'); evt.target.dispatchEvent(e); // Send click. evt.target.click(); } else if (evt.keyCode === this._Keycodes.ESCAPE) { u.stopEvent(evt); // evt.preventDefault(); this.hide(); } } } }, /** * Handles a click event on an item. * * @param {Event} evt The event that fired. * @private */ _handleItemClick: function (evt) { if (evt.target.hasAttribute('disabled')) { u.stopEvent(evt); // evt.stopPropagation(); } else { // Wait some time before closing menu, so the user can see the ripple. this._closing = true; window.setTimeout(function (evt) { this.hide(); this._closing = false; }.bind(this), 150); } }, /** * Calculates the initial clip (for opening the menu) or final clip (for closing * it), and applies it. This allows us to animate from or to the correct point, * that is, the point it's aligned to in the "for" element. * * @param {number} height Height of the clip rectangle * @param {number} width Width of the clip rectangle * @private */ _applyClip: function (height, width) { if (u.hasClass(this.element, 'u-menu-unaligned')) { // Do not clip. this.element.style.clip = ''; } else if (u.hasClass(this.element, 'u-menu-bottom-right')) { // Clip to the top right corner of the menu. this.element.style.clip = 'rect(0 ' + width + 'px ' + '0 ' + width + 'px)'; } else if (u.hasClass(this.element, 'u-menu-top-left')) { // Clip to the bottom left corner of the menu. this.element.style.clip = 'rect(' + height + 'px 0 ' + height + 'px 0)'; } else if (u.hasClass(this.element, 'u-menu-top-right')) { // Clip to the bottom right corner of the menu. this.element.style.clip = 'rect(' + height + 'px ' + width + 'px ' + height + 'px ' + width + 'px)'; } else { // Default: do not clip (same as clipping to the top left corner). this.element.style.clip = 'rect(' + 0 + 'px ' + 0 + 'px ' + 0 + 'px ' + 0 + 'px)'; } }, /** * Adds an event listener to clean up after the animation ends. * * @private */ _addAnimationEndListener: function () { var cleanup = function () { u.off(this.element,'transitionend', cleanup); // this.element.removeEventListener('transitionend', cleanup); u.off(this.element,'webkitTransitionEnd', cleanup); // this.element.removeEventListener('webkitTransitionEnd', cleanup); u.removeClass(this.element, 'is-animating'); }.bind(this); // Remove animation class once the transition is done. u.on(this.element,'transitionend', cleanup); // this.element.addEventListener('transitionend', cleanup); u.on(this.element,'webkitTransitionEnd', cleanup); // this.element.addEventListener('webkitTransitionEnd', cleanup); }, /** * Displays the menu. * * @public */ show: function (evt) { if (this.element && this._container && this._outline) { // Measure the inner element. var height = this.element.getBoundingClientRect().height; var width = this.element.getBoundingClientRect().width; if(!width){ var left = this.element.getBoundingClientRect().left; var right = this.element.getBoundingClientRect().right; width = right - left; } if(!height){ var top = this.element.getBoundingClientRect().top; var bottom = this.element.getBoundingClientRect().bottom; height = bottom - top; } // Apply the inner element's size to the container and outline. this._container.style.width = width + 'px'; this._container.style.height = height + 'px'; this._outline.style.width = width + 'px'; this._outline.style.height = height + 'px'; var transitionDuration = 0.24; // Calculate transition delays for individual menu items, so that they fade // in one at a time. var items = this.element.querySelectorAll('.u-menu-item'); for (var i = 0; i < items.length; i++) { var itemDelay = null; if (u.hasClass(this.element, 'u-menu-top-left') || u.hasClass(this.element, 'u-menu-top-right')) { itemDelay = ((height - items[i].offsetTop - items[i].offsetHeight) / height * transitionDuration) + 's'; } else { itemDelay = (items[i].offsetTop / height * transitionDuration) + 's'; } items[i].style.transitionDelay = itemDelay; } // Apply the initial clip to the text before we start animating. this._applyClip(height, width); // Wait for the next frame, turn on animation, and apply the final clip. // Also make it visible. This triggers the transitions. if(window.requestAnimationFrame){ window.requestAnimationFrame(function () { u.addClass(this.element, 'is-animating'); this.element.style.clip = 'rect(0 ' + width + 'px ' + height + 'px 0)'; u.addClass(this._container, 'is-visible'); }.bind(this)); }else{ u.addClass(this.element, 'is-animating'); this.element.style.clip = 'rect(0 ' + width + 'px ' + height + 'px 0)'; u.addClass(this._container, 'is-visible'); } // Clean up after the animation is complete. this._addAnimationEndListener(); // Add a click listener to the document, to close the menu. var firstFlag = true; var callback = function (e) { if(u.isIE8){ if(firstFlag){ firstFlag = false; return } } if (e !== evt && !this._closing && e.target.parentNode !== this.element) { u.off(document,'click', callback); // document.removeEventListener('click', callback); this.hide(); } }.bind(this); u.on(document,'click', callback); // document.addEventListener('click', callback); } }, /** * Hides the menu. * * @public */ hide: function () { if (this.element && this._container && this._outline) { var items = this.element.querySelectorAll('.u-menu-item'); // Remove all transition delays; menu items fade out concurrently. for (var i = 0; i < items.length; i++) { items[i].style.transitionDelay = null; } // Measure the inner element. var rect = this.element.getBoundingClientRect(); var height = rect.height; var width = rect.width; if(!width){ var left = rect.left; var right = rect.right; width = right - left; } if(!height){ var top = rect.top; var bottom = rect.bottom; height = bottom - top; } // Turn on animation, and apply the final clip. Also make invisible. // This triggers the transitions. u.addClass(this.element, 'is-animating'); this._applyClip(height, width); u.removeClass(this._container, 'is-visible'); // Clean up after the animation is complete. this._addAnimationEndListener(); } }, /** * Displays or hides the menu, depending on current state. * * @public */ toggle: function (evt) { if (u.hasClass(this._container, 'is-visible')) { this.hide(); } else { this.show(evt); } } }); u.compMgr.regComp({ comp: u.Menu, compAsString: 'u.Menu', css: 'u-menu' })