<!– @license Copyright © 2015 The Polymer Project Authors. All rights reserved. This code may only be used under the BSD style license found at polymer.github.io/LICENSE.txt The complete set of authors may be found at polymer.github.io/AUTHORS.txt The complete set of contributors may be found at polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as part of the polymer project is also subject to an additional IP rights grant found at polymer.github.io/PATENTS.txt –>

<link rel=“import” href=“../polymer/polymer.html”> <link rel=“import” href=“../iron-resizable-behavior/iron-resizable-behavior.html”> <link rel=“import” href=“../iron-a11y-keys-behavior/iron-a11y-keys-behavior.html”> <link rel=“import” href=“../iron-behaviors/iron-control-state.html”> <link rel=“import” href=“../iron-overlay-behavior/iron-overlay-behavior.html”> <link rel=“import” href=“../neon-animation/neon-animation-runner-behavior.html”> <link rel=“import” href=“../neon-animation/animations/opaque-animation.html”> <link rel=“import” href=“iron-dropdown-scroll-manager.html”>

<!– `<iron-dropdown>` is a generalized element that is useful when you have hidden content (`.dropdown-content`) that is revealed due to some change in state that should cause it to do so.

Note that this is a low-level element intended to be used as part of other composite elements that cause dropdowns to be revealed.

Examples of elements that might be implemented using an `iron-dropdown` include comboboxes, menubuttons, selects. The list goes on.

The `<iron-dropdown>` element exposes attributes that allow the position of the `.dropdown-content` relative to the `.dropdown-trigger` to be configured.

<iron-dropdown horizontal-align="right" vertical-align="top">
  <div class="dropdown-content">Hello!</div>
</iron-dropdown>

In the above example, the `<div>` with class `.dropdown-content` will be hidden until the dropdown element has `opened` set to true, or when the `open` method is called on the element.

@demo demo/index.html –>

<dom-module id=“iron-dropdown”>

<template>
  <style>
    :host {
      position: fixed;
    }

    #contentWrapper ::content > * {
      overflow: auto;
    }

    #contentWrapper.animating ::content > * {
      overflow: hidden;
    }
  </style>

  <div id="contentWrapper">
    <content id="content" select=".dropdown-content"></content>
  </div>
</template>

<script>
  (function() {
    'use strict';

    Polymer({
      is: 'iron-dropdown',

      behaviors: [
        Polymer.IronControlState,
        Polymer.IronA11yKeysBehavior,
        Polymer.IronOverlayBehavior,
        Polymer.NeonAnimationRunnerBehavior
      ],

      properties: {
        /**
         * The orientation against which to align the dropdown content
         * horizontally relative to the dropdown trigger.
         * Overridden from `Polymer.IronFitBehavior`.
         */
        horizontalAlign: {
          type: String,
          value: 'left',
          reflectToAttribute: true
        },

        /**
         * The orientation against which to align the dropdown content
         * vertically relative to the dropdown trigger.
         * Overridden from `Polymer.IronFitBehavior`.
         */
        verticalAlign: {
          type: String,
          value: 'top',
          reflectToAttribute: true
        },

        /**
         * An animation config. If provided, this will be used to animate the
         * opening of the dropdown.
         */
        openAnimationConfig: {
          type: Object
        },

        /**
         * An animation config. If provided, this will be used to animate the
         * closing of the dropdown.
         */
        closeAnimationConfig: {
          type: Object
        },

        /**
         * If provided, this will be the element that will be focused when
         * the dropdown opens.
         */
        focusTarget: {
          type: Object
        },

        /**
         * Set to true to disable animations when opening and closing the
         * dropdown.
         */
        noAnimations: {
          type: Boolean,
          value: false
        },

        /**
         * By default, the dropdown will constrain scrolling on the page
         * to itself when opened.
         * Set to true in order to prevent scroll from being constrained
         * to the dropdown when it opens.
         */
        allowOutsideScroll: {
          type: Boolean,
          value: false
        },

        /**
         * Callback for scroll events.
         * @type {Function}
         * @private
         */
        _boundOnCaptureScroll: {
          type: Function,
          value: function() {
            return this._onCaptureScroll.bind(this);
          }
        }
      },

      listeners: {
        'neon-animation-finish': '_onNeonAnimationFinish'
      },

      observers: [
        '_updateOverlayPosition(positionTarget, verticalAlign, horizontalAlign, verticalOffset, horizontalOffset)'
      ],

      /**
       * The element that is contained by the dropdown, if any.
       */
      get containedElement() {
        return Polymer.dom(this.$.content).getDistributedNodes()[0];
      },

      /**
       * The element that should be focused when the dropdown opens.
       * @deprecated
       */
      get _focusTarget() {
        return this.focusTarget || this.containedElement;
      },

      ready: function() {
        // Memoized scrolling position, used to block scrolling outside.
        this._scrollTop = 0;
        this._scrollLeft = 0;
        // Used to perform a non-blocking refit on scroll.
        this._refitOnScrollRAF = null;
      },

      attached: function () {
        if (!this.sizingTarget || this.sizingTarget === this) {
          this.sizingTarget = this.containedElement;
        }
      },

      detached: function() {
        this.cancelAnimation();
        document.removeEventListener('scroll', this._boundOnCaptureScroll);
        Polymer.IronDropdownScrollManager.removeScrollLock(this);
      },

      /**
       * Called when the value of `opened` changes.
       * Overridden from `IronOverlayBehavior`
       */
      _openedChanged: function() {
        if (this.opened && this.disabled) {
          this.cancel();
        } else {
          this.cancelAnimation();
          this._updateAnimationConfig();
          this._saveScrollPosition();
          if (this.opened) {
            document.addEventListener('scroll', this._boundOnCaptureScroll);
            !this.allowOutsideScroll && Polymer.IronDropdownScrollManager.pushScrollLock(this);
          } else {
            document.removeEventListener('scroll', this._boundOnCaptureScroll);
            Polymer.IronDropdownScrollManager.removeScrollLock(this);
          }
          Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments);
        }
      },

      /**
       * Overridden from `IronOverlayBehavior`.
       */
      _renderOpened: function() {
        if (!this.noAnimations && this.animationConfig.open) {
          this.$.contentWrapper.classList.add('animating');
          this.playAnimation('open');
        } else {
          Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments);
        }
      },

      /**
       * Overridden from `IronOverlayBehavior`.
       */
      _renderClosed: function() {

        if (!this.noAnimations && this.animationConfig.close) {
          this.$.contentWrapper.classList.add('animating');
          this.playAnimation('close');
        } else {
          Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments);
        }
      },

      /**
       * Called when animation finishes on the dropdown (when opening or
       * closing). Responsible for "completing" the process of opening or
       * closing the dropdown by positioning it or setting its display to
       * none.
       */
      _onNeonAnimationFinish: function() {
        this.$.contentWrapper.classList.remove('animating');
        if (this.opened) {
          this._finishRenderOpened();
        } else {
          this._finishRenderClosed();
        }
      },

      _onCaptureScroll: function() {
        if (!this.allowOutsideScroll) {
          this._restoreScrollPosition();
        } else {
          this._refitOnScrollRAF && window.cancelAnimationFrame(this._refitOnScrollRAF);
          this._refitOnScrollRAF = window.requestAnimationFrame(this.refit.bind(this));
        }
      },

      /**
       * Memoizes the scroll position of the outside scrolling element.
       * @private
       */
      _saveScrollPosition: function() {
        if (document.scrollingElement) {
          this._scrollTop = document.scrollingElement.scrollTop;
          this._scrollLeft = document.scrollingElement.scrollLeft;
        } else {
          // Since we don't know if is the body or html, get max.
          this._scrollTop = Math.max(document.documentElement.scrollTop, document.body.scrollTop);
          this._scrollLeft = Math.max(document.documentElement.scrollLeft, document.body.scrollLeft);
        }
      },

      /**
       * Resets the scroll position of the outside scrolling element.
       * @private
       */
      _restoreScrollPosition: function() {
        if (document.scrollingElement) {
          document.scrollingElement.scrollTop = this._scrollTop;
          document.scrollingElement.scrollLeft = this._scrollLeft;
        } else {
          // Since we don't know if is the body or html, set both.
          document.documentElement.scrollTop = this._scrollTop;
          document.documentElement.scrollLeft = this._scrollLeft;
          document.body.scrollTop = this._scrollTop;
          document.body.scrollLeft = this._scrollLeft;
        }
      },

      /**
       * Constructs the final animation config from different properties used
       * to configure specific parts of the opening and closing animations.
       */
      _updateAnimationConfig: function() {
        var animations = (this.openAnimationConfig || []).concat(this.closeAnimationConfig || []);
        for (var i = 0; i < animations.length; i++) {
          animations[i].node = this.containedElement;
        }
        this.animationConfig = {
          open: this.openAnimationConfig,
          close: this.closeAnimationConfig
        };
      },

      /**
       * Updates the overlay position based on configured horizontal
       * and vertical alignment.
       */
      _updateOverlayPosition: function() {
        if (this.isAttached) {
          // This triggers iron-resize, and iron-overlay-behavior will call refit if needed.
          this.notifyResize();
        }
      },

      /**
       * Apply focus to focusTarget or containedElement
       */
      _applyFocus: function () {
        var focusTarget = this.focusTarget || this.containedElement;
        if (focusTarget && this.opened && !this.noAutoFocus) {
          focusTarget.focus();
        } else {
          Polymer.IronOverlayBehaviorImpl._applyFocus.apply(this, arguments);
        }
      }
    });
  })();
</script>

</dom-module>