/**
@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
*/
import '../polymer/polymer_bundled.min.js';
import {IronA11yKeysBehavior} from '../iron-a11y-keys-behavior/iron-a11y-keys-behavior.js';
import {IronControlState} from '../iron-behaviors/iron-control-state.js';
import {IronOverlayBehavior, IronOverlayBehaviorImpl} from '../iron-overlay-behavior/iron-overlay-behavior.js';
import {NeonAnimationRunnerBehavior} from '../neon-animation/neon-animation-runner-behavior.js';
import {Polymer} from '../polymer/polymer_bundled.min.js';
import {dom} from '../polymer/polymer_bundled.min.js';
import {html} from '../polymer/polymer_bundled.min.js';
/**
`<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 slot="dropdown-content">Hello!</div>
</iron-dropdown>
In the above example, the `<div>` assigned to the `dropdown-content` slot 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
*/
Polymer({
_template: html`
<style>
:host {
position: fixed;
}
#contentWrapper ::slotted(*) {
overflow: auto;
}
#contentWrapper.animating ::slotted(*) {
overflow: hidden;
pointer-events: none;
}
</style>
<div id="contentWrapper">
<slot id="content" name="dropdown-content"></slot>
</div>
`,
is: 'iron-dropdown',
behaviors: [
IronControlState,
IronA11yKeysBehavior,
IronOverlayBehavior,
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.
* This property is a shortcut to set `scrollAction` to lock or refit.
* Prefer directly setting the `scrollAction` property.
*/
allowOutsideScroll:
{type: Boolean, value: false, observer: '_allowOutsideScrollChanged'}
},
listeners: {'neon-animation-finish': '_onNeonAnimationFinish'},
observers: [
'_updateOverlayPosition(positionTarget, verticalAlign, horizontalAlign, verticalOffset, horizontalOffset)'
],
/**
* The element that is contained by the dropdown, if any.
*/
get containedElement() {
// Polymer 2.x returns slot.assignedNodes which can contain text nodes.
var nodes = dom(this.$.content).getDistributedNodes();
for (var i = 0, l = nodes.length; i < l; i++) {
if (nodes[i].nodeType === Node.ELEMENT_NODE) {
return nodes[i];
}
}
},
ready: function() {
// Ensure scrollAction is set.
if (!this.scrollAction) {
this.scrollAction = this.allowOutsideScroll ? 'refit' : 'lock';
}
this._readied = true;
},
attached: function() {
if (!this.sizingTarget || this.sizingTarget === this) {
this.sizingTarget = this.containedElement || this;
}
},
detached: function() {
this.cancelAnimation();
},
/**
* Called when the value of `opened` changes.
* Overridden from `IronOverlayBehavior`
*/
_openedChanged: function() {
if (this.opened && this.disabled) {
this.cancel();
} else {
this.cancelAnimation();
this._updateAnimationConfig();
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 {
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 {
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();
}
},
/**
* 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();
}
},
/**
* Sets scrollAction according to the value of allowOutsideScroll.
* Prefer setting directly scrollAction.
*/
_allowOutsideScrollChanged: function(allowOutsideScroll) {
// Wait until initial values are all set.
if (!this._readied) {
return;
}
if (!allowOutsideScroll) {
this.scrollAction = 'lock';
} else if (!this.scrollAction || this.scrollAction === 'lock') {
this.scrollAction = 'refit';
}
},
/**
* Apply focus to focusTarget or containedElement
*/
_applyFocus: function() {
var focusTarget = this.focusTarget || this.containedElement;
if (focusTarget && this.opened && !this.noAutoFocus) {
focusTarget.focus();
} else {
IronOverlayBehaviorImpl._applyFocus.apply(this, arguments);
}
}
});