text: /**
   * @license
   * Copyright 2015 Google Inc. All Rights Reserved.
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  (function () {
  
  
    /**
     * Class constructor for dropdown MDL component.
     * Implements MDL component design pattern defined at:
     * https://github.com/jasonmayes/mdl-component-design-pattern
     *
     * @constructor
     * @param {HTMLElement} element The element that will be upgraded.
     */
    const MaterialMenu = function MaterialMenu(element) {
      this.element_ = element;
  
      // Initialize instance.
      this.init();
    };
    window.MaterialMenu = MaterialMenu;
  
    /**
     * Store constants in one place so they can be updated easily.
     *
     * @enum {string | number}
     * @private
     */
    MaterialMenu.prototype.Constant_ = {
      // Total duration of the menu animation.
      TRANSITION_DURATION_SECONDS: 0.3,
      // The fraction of the total duration we want to use for menu item animations.
      TRANSITION_DURATION_FRACTION: 0.8,
      // How long the menu stays open after choosing an option (so the user can see
      // the ripple).
      CLOSE_TIMEOUT: 150
    };
  
    /**
     * Keycodes, for code readability.
     *
     * @enum {number}
     * @private
     */
    MaterialMenu.prototype.Keycodes_ = {
      ENTER: 13,
      ESCAPE: 27,
      SPACE: 32,
      UP_ARROW: 38,
      DOWN_ARROW: 40
    };
  
    /**
     * Store strings for class names defined by this component that are used in
     * JavaScript. This allows us to simply change it in one place should we
     * decide to modify at a later date.
     *
     * @enum {string}
     * @private
     */
    MaterialMenu.prototype.CssClasses_ = {
      CONTAINER: 'mdl-menu__container',
      OUTLINE: 'mdl-menu__outline',
      ITEM: 'mdl-menu__item',
      ITEM_RIPPLE_CONTAINER: 'mdl-menu__item-ripple-container',
      RIPPLE_EFFECT: 'mdl-js-ripple-effect',
      RIPPLE_IGNORE_EVENTS: 'mdl-js-ripple-effect--ignore-events',
      RIPPLE: 'mdl-ripple',
      // Statuses
      IS_UPGRADED: 'is-upgraded',
      IS_VISIBLE: 'is-visible',
      IS_ANIMATING: 'is-animating',
      // Alignment options
      BOTTOM_LEFT: 'mdl-menu--bottom-left',  // This is the default.
      BOTTOM_RIGHT: 'mdl-menu--bottom-right',
      TOP_LEFT: 'mdl-menu--top-left',
      TOP_RIGHT: 'mdl-menu--top-right',
      UNALIGNED: 'mdl-menu--unaligned'
    };
  
    /**
     * Initialize element.
     */
    MaterialMenu.prototype.init = function () {
      if (this.element_) {
        // Create container for the menu.
        const container = document.createElement('div');
        container.classList.add(this.CssClasses_.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).
        const outline = document.createElement('div');
        outline.classList.add(this.CssClasses_.OUTLINE);
        this.outline_ = outline;
        container.insertBefore(outline, this.element_);
  
        // Find the "for" element and bind events to it.
        const forElId = this.element_.getAttribute('for') ||
                        this.element_.getAttribute('data-mdl-for');
        let forEl = null;
        if (forElId) {
          forEl = document.getElementById(forElId);
          if (forEl) {
            this.forElement_ = forEl;
            forEl.addEventListener('click', this.handleForClick_.bind(this));
            forEl.addEventListener('keydown',
                this.handleForKeyboardEvent_.bind(this));
          }
        }
  
        const items = this.element_.querySelectorAll(`.${this.CssClasses_.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.
          items[i].addEventListener('click', this.boundItemClick_);
          // Add a tab index to each menu item.
          items[i].tabIndex = '-1';
          // Add a keyboard listener to each menu item.
          items[i].addEventListener('keydown', this.boundItemKeydown_);
        }
  
        // Add ripple classes to each item, if the user has enabled ripples.
        if (this.element_.classList.contains(this.CssClasses_.RIPPLE_EFFECT)) {
          this.element_.classList.add(this.CssClasses_.RIPPLE_IGNORE_EVENTS);
  
          for (i = 0; i < items.length; i++) {
            const item = items[i];
  
            const rippleContainer = document.createElement('span');
            rippleContainer.classList.add(this.CssClasses_.ITEM_RIPPLE_CONTAINER);
  
            const ripple = document.createElement('span');
            ripple.classList.add(this.CssClasses_.RIPPLE);
            rippleContainer.appendChild(ripple);
  
            item.appendChild(rippleContainer);
            item.classList.add(this.CssClasses_.RIPPLE_EFFECT);
          }
        }
  
        // Copy alignment classes to the container, so the outline can use them.
        if (this.element_.classList.contains(this.CssClasses_.BOTTOM_LEFT)) {
          this.outline_.classList.add(this.CssClasses_.BOTTOM_LEFT);
        }
        if (this.element_.classList.contains(this.CssClasses_.BOTTOM_RIGHT)) {
          this.outline_.classList.add(this.CssClasses_.BOTTOM_RIGHT);
        }
        if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT)) {
          this.outline_.classList.add(this.CssClasses_.TOP_LEFT);
        }
        if (this.element_.classList.contains(this.CssClasses_.TOP_RIGHT)) {
          this.outline_.classList.add(this.CssClasses_.TOP_RIGHT);
        }
        if (this.element_.classList.contains(this.CssClasses_.UNALIGNED)) {
          this.outline_.classList.add(this.CssClasses_.UNALIGNED);
        }
  
        container.classList.add(this.CssClasses_.IS_UPGRADED);
      }
    };
  
    /**
     * Handles a click on the "for" element, by positioning the menu and then
     * toggling it.
     *
     * @param {Event} evt The event that fired.
     * @private
     */
    MaterialMenu.prototype.handleForClick_ = function (evt) {
      if (this.element_ && this.forElement_) {
        const rect = this.forElement_.getBoundingClientRect();
        const forRect = this.forElement_.parentElement.getBoundingClientRect();
  
        if (this.element_.classList.contains(this.CssClasses_.UNALIGNED)) {
          // Do not position the menu automatically. Requires the developer to
          // manually specify position.
        } else if (this.element_.classList.contains(
            this.CssClasses_.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.forElement_.offsetTop + this.forElement_.offsetHeight}px`;
        } else if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT)) {
          // Position above the "for" element, aligned to its left.
          this.container_.style.left = `${this.forElement_.offsetLeft}px`;
          this.container_.style.bottom = `${forRect.bottom - rect.top}px`;
        } else if (this.element_.classList.contains(this.CssClasses_.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.forElement_.offsetLeft}px`;
          this.container_.style.top =
              `${this.forElement_.offsetTop + this.forElement_.offsetHeight}px`;
        }
      }
  
      this.toggle(evt);
    };
  
    /**
     * Handles a keyboard event on the "for" element.
     *
     * @param {Event} evt The event that fired.
     * @private
     */
    MaterialMenu.prototype.handleForKeyboardEvent_ = function (evt) {
      if (this.element_ && this.container_ && this.forElement_) {
        const items = this.element_.querySelectorAll(`.${this.CssClasses_.ITEM
          }:not([disabled])`);
  
        if (items && items.length > 0 &&
            this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)) {
          if (evt.keyCode === this.Keycodes_.UP_ARROW) {
            evt.preventDefault();
            items[items.length - 1].focus();
          } else if (evt.keyCode === this.Keycodes_.DOWN_ARROW) {
            evt.preventDefault();
            items[0].focus();
          }
        }
      }
    };
  
    /**
     * Handles a keyboard event on an item.
     *
     * @param {Event} evt The event that fired.
     * @private
     */
    MaterialMenu.prototype.handleItemKeyboardEvent_ = function (evt) {
      if (this.element_ && this.container_) {
        const items = this.element_.querySelectorAll(`.${this.CssClasses_.ITEM
          }:not([disabled])`);
  
        if (items && items.length > 0 &&
            this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)) {
          const currentIndex = Array.prototype.slice.call(items).indexOf(evt.target);
  
          if (evt.keyCode === this.Keycodes_.UP_ARROW) {
            evt.preventDefault();
            if (currentIndex > 0) {
              items[currentIndex - 1].focus();
            } else {
              items[items.length - 1].focus();
            }
          } else if (evt.keyCode === this.Keycodes_.DOWN_ARROW) {
            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) {
            evt.preventDefault();
            // Send mousedown and mouseup to trigger ripple.
            let 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) {
            evt.preventDefault();
            this.hide();
          }
        }
      }
    };
  
    /**
     * Handles a click event on an item.
     *
     * @param {Event} evt The event that fired.
     * @private
     */
    MaterialMenu.prototype.handleItemClick_ = function (evt) {
      if (evt.target.hasAttribute('disabled')) {
        evt.stopPropagation();
      } else {
        // Wait some time before closing menu, so the user can see the ripple.
        this.closing_ = true;
        window.setTimeout((evt) => {
          this.hide();
          this.closing_ = false;
        }, /** @type {number} */ (this.Constant_.CLOSE_TIMEOUT));
      }
    };
  
    /**
     * 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
     */
    MaterialMenu.prototype.applyClip_ = function (height, width) {
      if (this.element_.classList.contains(this.CssClasses_.UNALIGNED)) {
        // Do not clip.
        this.element_.style.clip = '';
      } else if (this.element_.classList.contains(this.CssClasses_.BOTTOM_RIGHT)) {
        // Clip to the top right corner of the menu.
        this.element_.style.clip =
            `rect(0 ${width}px ` + `0 ${width}px)`;
      } else if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT)) {
        // Clip to the bottom left corner of the menu.
        this.element_.style.clip =
            `rect(${height}px 0 ${height}px 0)`;
      } else if (this.element_.classList.contains(this.CssClasses_.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 = '';
      }
    };
  
    /**
     * Cleanup function to remove animation listeners.
     *
     * @param {Event} evt
     * @private
     */
  
    MaterialMenu.prototype.removeAnimationEndListener_ = function (evt) {
      evt.target.classList.remove(MaterialMenu.prototype.CssClasses_.IS_ANIMATING);
    };
  
    /**
     * Adds an event listener to clean up after the animation ends.
     *
     * @private
     */
    MaterialMenu.prototype.addAnimationEndListener_ = function () {
      this.element_.addEventListener('transitionend', this.removeAnimationEndListener_);
      this.element_.addEventListener('webkitTransitionEnd', this.removeAnimationEndListener_);
    };
  
    /**
     * Displays the menu.
     *
     * @public
     */
    MaterialMenu.prototype.show = function (evt) {
      if (this.element_ && this.container_ && this.outline_) {
        // Measure the inner element.
        const height = this.element_.getBoundingClientRect().height;
        const width = this.element_.getBoundingClientRect().width;
  
        // 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`;
  
        const transitionDuration = this.Constant_.TRANSITION_DURATION_SECONDS *
            this.Constant_.TRANSITION_DURATION_FRACTION;
  
        // Calculate transition delays for individual menu items, so that they fade
        // in one at a time.
        const items = this.element_.querySelectorAll(`.${this.CssClasses_.ITEM}`);
        for (let i = 0; i < items.length; i++) {
          let itemDelay = null;
          if (this.element_.classList.contains(this.CssClasses_.TOP_LEFT) ||
              this.element_.classList.contains(this.CssClasses_.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.
        window.requestAnimationFrame(() => {
          this.element_.classList.add(this.CssClasses_.IS_ANIMATING);
          this.element_.style.clip = `rect(0 ${width}px ${height}px 0)`;
          this.container_.classList.add(this.CssClasses_.IS_VISIBLE);
        });
  
        // Clean up after the animation is complete.
        this.addAnimationEndListener_();
  
        // Add a click listener to the document, to close the menu.
        var callback = function (e) {
          // Check to see if the document is processing the same event that
          // displayed the menu in the first place. If so, do nothing.
          // Also check to see if the menu is in the process of closing itself, and
          // do nothing in that case.
          // Also check if the clicked element is a menu item
          // if so, do nothing.
          if (e !== evt && !this.closing_ && e.target.parentNode !== this.element_) {
            document.removeEventListener('click', callback);
            this.hide();
          }
        }.bind(this);
        document.addEventListener('click', callback);
      }
    };
    MaterialMenu.prototype.show = MaterialMenu.prototype.show;
  
    /**
     * Hides the menu.
     *
     * @public
     */
    MaterialMenu.prototype.hide = function () {
      if (this.element_ && this.container_ && this.outline_) {
        const items = this.element_.querySelectorAll(`.${this.CssClasses_.ITEM}`);
  
        // Remove all transition delays; menu items fade out concurrently.
        for (let i = 0; i < items.length; i++) {
          items[i].style.removeProperty('transition-delay');
        }
  
        // Measure the inner element.
        const rect = this.element_.getBoundingClientRect();
        const height = rect.height;
        const width = rect.width;
  
        // Turn on animation, and apply the final clip. Also make invisible.
        // This triggers the transitions.
        this.element_.classList.add(this.CssClasses_.IS_ANIMATING);
        this.applyClip_(height, width);
        this.container_.classList.remove(this.CssClasses_.IS_VISIBLE);
  
        // Clean up after the animation is complete.
        this.addAnimationEndListener_();
      }
    };
    MaterialMenu.prototype.hide = MaterialMenu.prototype.hide;
  
    /**
     * Displays or hides the menu, depending on current state.
     *
     * @public
     */
    MaterialMenu.prototype.toggle = function (evt) {
      if (this.container_.classList.contains(this.CssClasses_.IS_VISIBLE)) {
        this.hide();
      } else {
        this.show(evt);
      }
    };
    MaterialMenu.prototype.toggle = MaterialMenu.prototype.toggle;
  
    // The component registers itself. It can assume componentHandler is available
    // in the global scope.
    componentHandler.register({
      constructor: MaterialMenu,
      classAsString: 'MaterialMenu',
      cssClass: 'mdl-js-menu',
      widget: true
    });
  }());
  