jellyfin-web/dashboard-ui/bower_components/iron-dropdown/iron-dropdown.html
2015-08-19 00:08:03 -04:00

442 lines
14 KiB
HTML

<!--
@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">
<style>
:host {
position: fixed;
}
#contentWrapper ::content > * {
overflow: auto;
}
#contentWrapper.animating ::content > * {
overflow: hidden;
}
</style>
<template>
<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.
*/
horizontalAlign: {
type: String,
value: 'left',
reflectToAttribute: true
},
/**
* The orientation against which to align the dropdown content
* vertically relative to the dropdown trigger.
*/
verticalAlign: {
type: String,
value: 'top',
reflectToAttribute: true
},
/**
* A pixel value that will be added to the position calculated for the
* given `horizontalAlign`. Use a negative value to offset to the
* left, or a positive value to offset to the right.
*/
horizontalOffset: {
type: Number,
value: 0,
notify: true
},
/**
* A pixel value that will be added to the position calculated for the
* given `verticalAlign`. Use a negative value to offset towards the
* top, or a positive value to offset towards the bottom.
*/
verticalOffset: {
type: Number,
value: 0,
notify: true
},
/**
* The element that should be used to position the dropdown when
* it is opened.
*/
positionTarget: {
type: Object,
observer: '_positionTargetChanged'
},
/**
* 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
},
/**
* We memoize the positionTarget bounding rectangle so that we can
* limit the number of times it is queried per resize / relayout.
* @type {?Object}
*/
_positionRectMemo: {
type: Object
}
},
listeners: {
'neon-animation-finish': '_onNeonAnimationFinish'
},
observers: [
'_updateOverlayPosition(verticalAlign, horizontalAlign, verticalOffset, horizontalOffset)'
],
attached: function() {
if (this.positionTarget === undefined) {
this.positionTarget = this._defaultPositionTarget;
}
},
/**
* 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.
*/
get _focusTarget() {
return this.focusTarget || this.containedElement;
},
/**
* The element that should be used to position the dropdown when
* it opens, if no position target is configured.
*/
get _defaultPositionTarget() {
var parent = Polymer.dom(this).parentNode;
if (parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
parent = parent.host;
}
return parent;
},
/**
* The bounding rect of the position target.
*/
get _positionRect() {
if (!this._positionRectMemo && this.positionTarget) {
this._positionRectMemo = this.positionTarget.getBoundingClientRect();
}
return this._positionRectMemo;
},
/**
* The horizontal offset value used to position the dropdown.
*/
get _horizontalAlignTargetValue() {
var target;
if (this.horizontalAlign === 'right') {
target = document.documentElement.clientWidth - this._positionRect.right;
} else {
target = this._positionRect.left;
}
target += this.horizontalOffset;
return Math.max(target, 0);
},
/**
* The vertical offset value used to position the dropdown.
*/
get _verticalAlignTargetValue() {
var target;
if (this.verticalAlign === 'bottom') {
target = document.documentElement.clientHeight - this._positionRect.bottom;
} else {
target = this._positionRect.top;
}
target += this.verticalOffset;
return Math.max(target, 0);
},
/**
* Called when the value of `opened` changes.
*
* @param {boolean} opened True if the dropdown is opened.
*/
_openedChanged: function(opened) {
if (opened && this.disabled) {
this.cancel();
} else {
this.cancelAnimation();
this._prepareDropdown();
Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments);
}
if (this.opened) {
this._focusContent();
}
},
/**
* Overridden from `IronOverlayBehavior`.
*/
_renderOpened: function() {
Polymer.IronDropdownScrollManager.pushScrollLock(this);
if (!this.noAnimations && this.animationConfig && this.animationConfig.open) {
this.$.contentWrapper.classList.add('animating');
this.playAnimation('open');
} else {
Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments);
}
},
/**
* Overridden from `IronOverlayBehavior`.
*/
_renderClosed: function() {
Polymer.IronDropdownScrollManager.removeScrollLock(this);
if (!this.noAnimations && this.animationConfig && 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) {
Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this);
} else {
Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this);
}
},
/**
* Called when an `iron-resize` event fires.
*/
_onIronResize: function() {
var containedElement = this.containedElement;
var scrollTop;
var scrollLeft;
if (containedElement) {
scrollTop = containedElement.scrollTop;
scrollLeft = containedElement.scrollLeft;
}
if (this.opened) {
this._updateOverlayPosition();
}
Polymer.IronOverlayBehaviorImpl._onIronResize.apply(this, arguments);
if (containedElement) {
containedElement.scrollTop = scrollTop;
containedElement.scrollLeft = scrollLeft;
}
},
/**
* Called when the `positionTarget` property changes.
*/
_positionTargetChanged: function() {
this._updateOverlayPosition();
},
/**
* Constructs the final animation config from different properties used
* to configure specific parts of the opening and closing animations.
*/
_updateAnimationConfig: function() {
var animationConfig = {};
var animations = [];
if (this.openAnimationConfig) {
// NOTE(cdata): When making `display:none` elements visible in Safari,
// the element will paint once in a fully visible state, causing the
// dropdown to flash before it fades in. We prepend an
// `opaque-animation` to fix this problem:
animationConfig.open = [{
name: 'opaque-animation',
}].concat(this.openAnimationConfig);
animations = animations.concat(animationConfig.open);
}
if (this.closeAnimationConfig) {
animationConfig.close = this.closeAnimationConfig;
animations = animations.concat(animationConfig.close);
}
animations.forEach(function(animation) {
animation.node = this.containedElement;
}, this);
this.animationConfig = animationConfig;
},
/**
* Prepares the dropdown for opening by updating measured layout
* values.
*/
_prepareDropdown: function() {
this.sizingTarget = this.containedElement || this.sizingTarget;
this._updateAnimationConfig();
this._updateOverlayPosition();
},
/**
* Updates the overlay position based on configured horizontal
* and vertical alignment, and re-memoizes these values for the sake
* of behavior in `IronFitBehavior`.
*/
_updateOverlayPosition: function() {
this._positionRectMemo = null;
if (!this.positionTarget) {
return;
}
this.style[this.horizontalAlign] =
this._horizontalAlignTargetValue + 'px';
this.style[this.verticalAlign] =
this._verticalAlignTargetValue + 'px';
// NOTE(cdata): We re-memoize inline styles here, otherwise
// calling `refit` from `IronFitBehavior` will reset inline styles
// to whatever they were when the dropdown first opened.
if (this._fitInfo) {
this._fitInfo.inlineStyle[this.horizontalAlign] =
this.style[this.horizontalAlign];
this._fitInfo.inlineStyle[this.verticalAlign] =
this.style[this.verticalAlign];
}
},
/**
* Focuses the configured focus target.
*/
_focusContent: function() {
// NOTE(cdata): This is async so that it can attempt the focus after
// `display: none` is removed from the element.
this.async(function() {
if (this._focusTarget) {
this._focusTarget.focus();
}
});
}
});
})();
</script>
</dom-module>