blob: b1063490b6641ef2fd90b7aaa45a888035ac8a81 [file] [log] [blame]
<!--
Copyright (c) 2014 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
-->
<!--
`core-dropdown` is an element that is initially hidden and is positioned relatively to another
element, usually the element that triggers the dropdown. The dropdown and the triggering element
should be children of the same offsetParent, e.g. the same `<div>` with `position: relative`.
It can be used to implement dropdown menus, menu buttons, etc..
Example:
<template is="auto-binding">
<div relative>
<core-icon-button id="trigger" icon="menu"></core-icon-button>
<core-dropdown relatedTarget="{{$.trigger}}">
<core-menu>
<core-item>Cut</core-item>
<core-item>Copy</core-item>
<core-item>Paste</core-item>
</core-menu>
</core-dropdown>
</div>
</template>
Positioning
-----------
By default, the dropdown is absolutely positioned on top of the `relatedTarget` with the top and
left edges aligned. The `halign` and `valign` properties controls the various alignments. The size
of the dropdown is automatically restrained such that it is entirely visible on the screen. Use the
`margin`
If you need more control over the dropdown's position, use CSS. The `halign` and `valign` properties are
ignored if the dropdown is positioned with CSS.
Example:
<style>
/* manually position the dropdown below the trigger */
core-dropdown {
position: absolute;
top: 38px;
left: 0;
}
</style>
<template is="auto-binding">
<div relative>
<core-icon-button id="trigger" icon="menu"></core-icon-button>
<core-dropdown relatedTarget="{{$.trigger}}">
<core-menu>
<core-item>Cut</core-item>
<core-item>Copy</core-item>
<core-item>Paste</core-item>
</core-menu>
</core-dropdown>
</div>
</template>
The `layered` property
----------------------
Sometimes you may need to render the dropdown in a separate layer. For example,
it may be nested inside an element that needs to be `overflow: hidden`, or
its parent may be overlapped by elements above it in stacking order.
The `layered` property will place the dropdown in a separate layer to ensure
it appears on top of everything else. Note that this implies the dropdown will
not scroll with its container.
@group Polymer Core Elements
@element core-dropdown
@extends core-overlay
@homepage github.io
-->
<link href="../polymer/polymer.html" rel="import">
<link href="../core-overlay/core-overlay.html" rel="import">
<style shim-shadowdom>
html /deep/ core-dropdown {
position: absolute;
overflow: auto;
background-color: #fff;
}
</style>
<polymer-element name="core-dropdown" extends="core-overlay">
<script>
(function() {
function docElem(property) {
var t;
return ((t = document.documentElement) || (t = document.body.parentNode)) && (typeof t[property] === 'number') ? t : document.body;
}
// View width and height excluding any visible scrollbars
// http://www.highdots.com/forums/javascript/faq-topic-how-do-i-296669.html
// 1) document.client[Width|Height] always reliable when available, including Safari2
// 2) document.documentElement.client[Width|Height] reliable in standards mode DOCTYPE, except for Safari2, Opera<9.5
// 3) document.body.client[Width|Height] is gives correct result when #2 does not, except for Safari2
// 4) When document.documentElement.client[Width|Height] is unreliable, it will be size of <html> element either greater or less than desired view size
// https://bugzilla.mozilla.org/show_bug.cgi?id=156388#c7
// 5) When document.body.client[Width|Height] is unreliable, it will be size of <body> element less than desired view size
function viewSize() {
// This algorithm avoids creating test page to determine if document.documentElement.client[Width|Height] is greater then view size,
// will succeed where such test page wouldn't detect dynamic unreliability,
// and will only fail in the case the right or bottom edge is within the width of a scrollbar from edge of the viewport that has visible scrollbar(s).
var doc = docElem('clientWidth');
var body = document.body;
var w, h;
return typeof document.clientWidth === 'number' ?
{w: document.clientWidth, h: document.clientHeight} :
doc === body || (w = Math.max( doc.clientWidth, body.clientWidth )) > self.innerWidth || (h = Math.max( doc.clientHeight, body.clientHeight )) > self.innerHeight ?
{w: body.clientWidth, h: body.clientHeight} : {w: w, h: h };
}
Polymer({
publish: {
/**
* The element associated with this dropdown, usually the element that triggers
* the menu. If unset, this property will default to the target's parent node
* or shadow host.
*
* @attribute relatedTarget
* @type Node
*/
relatedTarget: null,
/**
* The horizontal alignment of the popup relative to `relatedTarget`. `left`
* means the left edges are aligned together. `right` means the right edges
* are aligned together.
*
* Accepted values: 'left', 'right'
*
* @attribute halign
* @type String
* @default 'left'
*/
halign: 'left',
/**
* The vertical alignment of the popup relative to `relatedTarget`. `top` means
* the top edges are aligned together. `bottom` means the bottom edges are
* aligned together.
*
* Accepted values: 'top', 'bottom'
*
* @attribute valign
* @type String
* @default 'top'
*/
valign: 'top'
},
measure: function() {
var target = this.target;
// remember position, because core-overlay may have set the property
var pos = target.style.position;
// get the size of the target as if it's positioned in the top left
// corner of the screen
target.style.position = 'fixed';
target.style.left = '0px';
target.style.top = '0px';
var rect = target.getBoundingClientRect();
target.style.position = pos;
target.style.left = null;
target.style.top = null;
return rect;
},
resetTargetDimensions: function() {
var dims = this.dimensions;
var style = this.target.style;
if (dims.position.h_by === this.localName) {
style[dims.position.h] = null;
dims.position.h_by = null;
}
if (dims.position.v_by === this.localName) {
style[dims.position.v] = null;
dims.position.v_by = null;
}
var style = this.sizingTarget.style;
style.width = null;
style.height = null;
this.super();
},
positionTarget: function() {
if (!this.relatedTarget) {
this.relatedTarget = this.target.parentElement || (this.target.parentNode && this.target.parentNode.host);
if (!this.relatedTarget) {
this.super();
return;
}
}
// explicitly set width/height, because we don't want it constrained
// to the offsetParent
var target = this.sizingTarget;
var rect = this.measure();
target.style.width = Math.ceil(rect.width) + 'px';
target.style.height = Math.ceil(rect.height) + 'px';
if (this.layered) {
this.positionLayeredTarget();
} else {
this.positionNestedTarget();
}
},
positionLayeredTarget: function() {
var target = this.target;
var rect = this.relatedTarget.getBoundingClientRect();
var dims = this.dimensions;
var margin = dims.margin;
var vp = viewSize();
if (!dims.position.h) {
if (this.halign === 'right') {
target.style.right = vp.w - rect.right - margin.right + 'px';
dims.position.h = 'right';
} else {
target.style.left = rect.left - margin.left + 'px';
dims.position.h = 'left';
}
dims.position.h_by = this.localName;
}
if (!dims.position.v) {
if (this.valign === 'bottom') {
target.style.bottom = vp.h - rect.bottom - margin.bottom + 'px';
dims.position.v = 'bottom';
} else {
target.style.top = rect.top - margin.top + 'px';
dims.position.v = 'top';
}
dims.position.v_by = this.localName;
}
if (dims.position.h_by || dims.position.v_by) {
target.style.position = 'fixed';
}
},
positionNestedTarget: function() {
var target = this.target;
var related = this.relatedTarget;
var t_op = target.offsetParent;
var r_op = related.offsetParent;
if (window.ShadowDOMPolyfill) {
t_op = wrap(t_op);
r_op = wrap(r_op);
}
if (t_op !== r_op && t_op !== related) {
console.warn('core-dropdown-overlay: dropdown\'s offsetParent must be the relatedTarget or the relatedTarget\'s offsetParent!');
}
// Don't use CSS to handle halign/valign so we can use
// dimensions.position to detect custom positioning
var dims = this.dimensions;
var margin = dims.margin;
var inside = t_op === related;
if (!dims.position.h) {
if (this.halign === 'right') {
target.style.right = ((inside ? 0 : t_op.offsetWidth - related.offsetLeft - related.offsetWidth) - margin.right) + 'px';
dims.position.h = 'right';
} else {
target.style.left = ((inside ? 0 : related.offsetLeft) - margin.left) + 'px';
dims.position.h = 'left';
}
dims.position.h_by = this.localName;
}
if (!dims.position.v) {
if (this.valign === 'bottom') {
target.style.bottom = ((inside ? 0 : t_op.offsetHeight - related.offsetTop - related.offsetHeight) - margin.bottom) + 'px';
dims.position.v = 'bottom';
} else {
target.style.top = ((inside ? 0 : related.offsetTop) - margin.top) + 'px';
dims.position.v = 'top';
}
dims.position.v_by = this.localName;
}
}
});
})();
</script>
</polymer-element>