| <!-- |
| @license |
| Copyright (c) 2015 The Polymer Project Authors. All rights reserved. |
| This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt |
| The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt |
| The complete set of contributors may be found at http://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 http://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. Pass an Array for multiple animations. |
| * See `neon-animation` documentation for more animation configuration |
| * details. |
| */ |
| openAnimationConfig: { |
| type: Object |
| }, |
| |
| /** |
| * An animation config. If provided, this will be used to animate the |
| * closing of the dropdown. Pass an Array for multiple animations. |
| * See `neon-animation` documentation for more animation configuration |
| * details. |
| */ |
| 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 || this; |
| } |
| }, |
| |
| 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() { |
| // Update the animation node to be the containedElement. |
| var animationNode = this.containedElement; |
| var animations = [].concat(this.openAnimationConfig || []).concat(this.closeAnimationConfig || []); |
| for (var i = 0; i < animations.length; i++) { |
| animations[i].node = animationNode; |
| } |
| 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> |