• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1<!--
2@license
3Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
4This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
5The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
6The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
7Code distributed by Google as part of the polymer project is also
8subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
9-->
10
11<link rel="import" href="../polymer/polymer.html">
12<link rel="import" href="../iron-resizable-behavior/iron-resizable-behavior.html">
13<link rel="import" href="../iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
14<link rel="import" href="../iron-behaviors/iron-control-state.html">
15<link rel="import" href="../iron-overlay-behavior/iron-overlay-behavior.html">
16<link rel="import" href="../neon-animation/neon-animation-runner-behavior.html">
17<link rel="import" href="../neon-animation/animations/opaque-animation.html">
18<link rel="import" href="iron-dropdown-scroll-manager.html">
19
20<!--
21`<iron-dropdown>` is a generalized element that is useful when you have
22hidden content (`.dropdown-content`) that is revealed due to some change in
23state that should cause it to do so.
24
25Note that this is a low-level element intended to be used as part of other
26composite elements that cause dropdowns to be revealed.
27
28Examples of elements that might be implemented using an `iron-dropdown`
29include comboboxes, menubuttons, selects. The list goes on.
30
31The `<iron-dropdown>` element exposes attributes that allow the position
32of the `.dropdown-content` relative to the `.dropdown-trigger` to be
33configured.
34
35    <iron-dropdown horizontal-align="right" vertical-align="top">
36      <div class="dropdown-content">Hello!</div>
37    </iron-dropdown>
38
39In the above example, the `<div>` with class `.dropdown-content` will be
40hidden until the dropdown element has `opened` set to true, or when the `open`
41method is called on the element.
42
43@demo demo/index.html
44-->
45
46<dom-module id="iron-dropdown">
47  <template>
48    <style>
49      :host {
50        position: fixed;
51      }
52
53      #contentWrapper ::content > * {
54        overflow: auto;
55      }
56
57      #contentWrapper.animating ::content > * {
58        overflow: hidden;
59      }
60    </style>
61
62    <div id="contentWrapper">
63      <content id="content" select=".dropdown-content"></content>
64    </div>
65  </template>
66
67  <script>
68    (function() {
69      'use strict';
70
71      Polymer({
72        is: 'iron-dropdown',
73
74        behaviors: [
75          Polymer.IronControlState,
76          Polymer.IronA11yKeysBehavior,
77          Polymer.IronOverlayBehavior,
78          Polymer.NeonAnimationRunnerBehavior
79        ],
80
81        properties: {
82          /**
83           * The orientation against which to align the dropdown content
84           * horizontally relative to the dropdown trigger.
85           * Overridden from `Polymer.IronFitBehavior`.
86           */
87          horizontalAlign: {
88            type: String,
89            value: 'left',
90            reflectToAttribute: true
91          },
92
93          /**
94           * The orientation against which to align the dropdown content
95           * vertically relative to the dropdown trigger.
96           * Overridden from `Polymer.IronFitBehavior`.
97           */
98          verticalAlign: {
99            type: String,
100            value: 'top',
101            reflectToAttribute: true
102          },
103
104          /**
105           * An animation config. If provided, this will be used to animate the
106           * opening of the dropdown. Pass an Array for multiple animations.
107           * See `neon-animation` documentation for more animation configuration
108           * details.
109           */
110          openAnimationConfig: {
111            type: Object
112          },
113
114          /**
115           * An animation config. If provided, this will be used to animate the
116           * closing of the dropdown. Pass an Array for multiple animations.
117           * See `neon-animation` documentation for more animation configuration
118           * details.
119           */
120          closeAnimationConfig: {
121            type: Object
122          },
123
124          /**
125           * If provided, this will be the element that will be focused when
126           * the dropdown opens.
127           */
128          focusTarget: {
129            type: Object
130          },
131
132          /**
133           * Set to true to disable animations when opening and closing the
134           * dropdown.
135           */
136          noAnimations: {
137            type: Boolean,
138            value: false
139          },
140
141          /**
142           * By default, the dropdown will constrain scrolling on the page
143           * to itself when opened.
144           * Set to true in order to prevent scroll from being constrained
145           * to the dropdown when it opens.
146           */
147          allowOutsideScroll: {
148            type: Boolean,
149            value: false
150          },
151
152          /**
153           * Callback for scroll events.
154           * @type {Function}
155           * @private
156           */
157          _boundOnCaptureScroll: {
158            type: Function,
159            value: function() {
160              return this._onCaptureScroll.bind(this);
161            }
162          }
163        },
164
165        listeners: {
166          'neon-animation-finish': '_onNeonAnimationFinish'
167        },
168
169        observers: [
170          '_updateOverlayPosition(positionTarget, verticalAlign, horizontalAlign, verticalOffset, horizontalOffset)'
171        ],
172
173        /**
174         * The element that is contained by the dropdown, if any.
175         */
176        get containedElement() {
177          return Polymer.dom(this.$.content).getDistributedNodes()[0];
178        },
179
180        /**
181         * The element that should be focused when the dropdown opens.
182         * @deprecated
183         */
184        get _focusTarget() {
185          return this.focusTarget || this.containedElement;
186        },
187
188        ready: function() {
189          // Memoized scrolling position, used to block scrolling outside.
190          this._scrollTop = 0;
191          this._scrollLeft = 0;
192          // Used to perform a non-blocking refit on scroll.
193          this._refitOnScrollRAF = null;
194        },
195
196        attached: function () {
197          if (!this.sizingTarget || this.sizingTarget === this) {
198            this.sizingTarget = this.containedElement || this;
199          }
200        },
201
202        detached: function() {
203          this.cancelAnimation();
204          document.removeEventListener('scroll', this._boundOnCaptureScroll);
205          Polymer.IronDropdownScrollManager.removeScrollLock(this);
206        },
207
208        /**
209         * Called when the value of `opened` changes.
210         * Overridden from `IronOverlayBehavior`
211         */
212        _openedChanged: function() {
213          if (this.opened && this.disabled) {
214            this.cancel();
215          } else {
216            this.cancelAnimation();
217            this._updateAnimationConfig();
218            this._saveScrollPosition();
219            if (this.opened) {
220              document.addEventListener('scroll', this._boundOnCaptureScroll);
221              !this.allowOutsideScroll && Polymer.IronDropdownScrollManager.pushScrollLock(this);
222            } else {
223              document.removeEventListener('scroll', this._boundOnCaptureScroll);
224              Polymer.IronDropdownScrollManager.removeScrollLock(this);
225            }
226            Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments);
227          }
228        },
229
230        /**
231         * Overridden from `IronOverlayBehavior`.
232         */
233        _renderOpened: function() {
234          if (!this.noAnimations && this.animationConfig.open) {
235            this.$.contentWrapper.classList.add('animating');
236            this.playAnimation('open');
237          } else {
238            Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments);
239          }
240        },
241
242        /**
243         * Overridden from `IronOverlayBehavior`.
244         */
245        _renderClosed: function() {
246
247          if (!this.noAnimations && this.animationConfig.close) {
248            this.$.contentWrapper.classList.add('animating');
249            this.playAnimation('close');
250          } else {
251            Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments);
252          }
253        },
254
255        /**
256         * Called when animation finishes on the dropdown (when opening or
257         * closing). Responsible for "completing" the process of opening or
258         * closing the dropdown by positioning it or setting its display to
259         * none.
260         */
261        _onNeonAnimationFinish: function() {
262          this.$.contentWrapper.classList.remove('animating');
263          if (this.opened) {
264            this._finishRenderOpened();
265          } else {
266            this._finishRenderClosed();
267          }
268        },
269
270        _onCaptureScroll: function() {
271          if (!this.allowOutsideScroll) {
272            this._restoreScrollPosition();
273          } else {
274            this._refitOnScrollRAF && window.cancelAnimationFrame(this._refitOnScrollRAF);
275            this._refitOnScrollRAF = window.requestAnimationFrame(this.refit.bind(this));
276          }
277        },
278
279        /**
280         * Memoizes the scroll position of the outside scrolling element.
281         * @private
282         */
283        _saveScrollPosition: function() {
284          if (document.scrollingElement) {
285            this._scrollTop = document.scrollingElement.scrollTop;
286            this._scrollLeft = document.scrollingElement.scrollLeft;
287          } else {
288            // Since we don't know if is the body or html, get max.
289            this._scrollTop = Math.max(document.documentElement.scrollTop, document.body.scrollTop);
290            this._scrollLeft = Math.max(document.documentElement.scrollLeft, document.body.scrollLeft);
291          }
292        },
293
294        /**
295         * Resets the scroll position of the outside scrolling element.
296         * @private
297         */
298        _restoreScrollPosition: function() {
299          if (document.scrollingElement) {
300            document.scrollingElement.scrollTop = this._scrollTop;
301            document.scrollingElement.scrollLeft = this._scrollLeft;
302          } else {
303            // Since we don't know if is the body or html, set both.
304            document.documentElement.scrollTop = this._scrollTop;
305            document.documentElement.scrollLeft = this._scrollLeft;
306            document.body.scrollTop = this._scrollTop;
307            document.body.scrollLeft = this._scrollLeft;
308          }
309        },
310
311        /**
312         * Constructs the final animation config from different properties used
313         * to configure specific parts of the opening and closing animations.
314         */
315        _updateAnimationConfig: function() {
316          // Update the animation node to be the containedElement.
317          var animationNode = this.containedElement;
318          var animations = [].concat(this.openAnimationConfig || []).concat(this.closeAnimationConfig || []);
319          for (var i = 0; i < animations.length; i++) {
320            animations[i].node = animationNode;
321          }
322          this.animationConfig = {
323            open: this.openAnimationConfig,
324            close: this.closeAnimationConfig
325          };
326        },
327
328        /**
329         * Updates the overlay position based on configured horizontal
330         * and vertical alignment.
331         */
332        _updateOverlayPosition: function() {
333          if (this.isAttached) {
334            // This triggers iron-resize, and iron-overlay-behavior will call refit if needed.
335            this.notifyResize();
336          }
337        },
338
339        /**
340         * Apply focus to focusTarget or containedElement
341         */
342        _applyFocus: function () {
343          var focusTarget = this.focusTarget || this.containedElement;
344          if (focusTarget && this.opened && !this.noAutoFocus) {
345            focusTarget.focus();
346          } else {
347            Polymer.IronOverlayBehaviorImpl._applyFocus.apply(this, arguments);
348          }
349        }
350      });
351    })();
352  </script>
353</dom-module>
354