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