552 lines
18 KiB
HTML
552 lines
18 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">
|
|
|
|
<script>
|
|
/**
|
|
`Polymer.IronFitBehavior` fits an element in another element using `max-height` and `max-width`, and
|
|
optionally centers it in the window or another element.
|
|
|
|
The element will only be sized and/or positioned if it has not already been sized and/or positioned
|
|
by CSS.
|
|
|
|
CSS properties | Action
|
|
-----------------------------|-------------------------------------------
|
|
`position` set | Element is not centered horizontally or vertically
|
|
`top` or `bottom` set | Element is not vertically centered
|
|
`left` or `right` set | Element is not horizontally centered
|
|
`max-height` set | Element respects `max-height`
|
|
`max-width` set | Element respects `max-width`
|
|
|
|
`Polymer.IronFitBehavior` can position an element into another element using
|
|
`verticalAlign` and `horizontalAlign`. This will override the element's css position.
|
|
|
|
<div class="container">
|
|
<iron-fit-impl vertical-align="top" horizontal-align="auto">
|
|
Positioned into the container
|
|
</iron-fit-impl>
|
|
</div>
|
|
|
|
Use `noOverlap` to position the element around another element without overlapping it.
|
|
|
|
<div class="container">
|
|
<iron-fit-impl no-overlap vertical-align="auto" horizontal-align="auto">
|
|
Positioned around the container
|
|
</iron-fit-impl>
|
|
</div>
|
|
|
|
@demo demo/index.html
|
|
@polymerBehavior
|
|
*/
|
|
|
|
Polymer.IronFitBehavior = {
|
|
|
|
properties: {
|
|
|
|
/**
|
|
* The element that will receive a `max-height`/`width`. By default it is the same as `this`,
|
|
* but it can be set to a child element. This is useful, for example, for implementing a
|
|
* scrolling region inside the element.
|
|
* @type {!Element}
|
|
*/
|
|
sizingTarget: {
|
|
type: Object,
|
|
value: function() {
|
|
return this;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* The element to fit `this` into.
|
|
*/
|
|
fitInto: {
|
|
type: Object,
|
|
value: window
|
|
},
|
|
|
|
/**
|
|
* Will position the element around the positionTarget without overlapping it.
|
|
*/
|
|
noOverlap: {
|
|
type: Boolean
|
|
},
|
|
|
|
/**
|
|
* The element that should be used to position the element. If not set, it will
|
|
* default to the parent node.
|
|
* @type {!Element}
|
|
*/
|
|
positionTarget: {
|
|
type: Element
|
|
},
|
|
|
|
/**
|
|
* The orientation against which to align the element horizontally
|
|
* relative to the `positionTarget`. Possible values are "left", "right", "auto".
|
|
*/
|
|
horizontalAlign: {
|
|
type: String
|
|
},
|
|
|
|
/**
|
|
* The orientation against which to align the element vertically
|
|
* relative to the `positionTarget`. Possible values are "top", "bottom", "auto".
|
|
*/
|
|
verticalAlign: {
|
|
type: String
|
|
},
|
|
|
|
/**
|
|
* The same as setting margin-left and margin-right css properties.
|
|
* @deprecated
|
|
*/
|
|
horizontalOffset: {
|
|
type: Number,
|
|
value: 0,
|
|
notify: true
|
|
},
|
|
|
|
/**
|
|
* The same as setting margin-top and margin-bottom css properties.
|
|
* @deprecated
|
|
*/
|
|
verticalOffset: {
|
|
type: Number,
|
|
value: 0,
|
|
notify: true
|
|
},
|
|
|
|
/**
|
|
* Set to true to auto-fit on attach.
|
|
*/
|
|
autoFitOnAttach: {
|
|
type: Boolean,
|
|
value: false
|
|
},
|
|
|
|
/** @type {?Object} */
|
|
_fitInfo: {
|
|
type: Object
|
|
}
|
|
},
|
|
|
|
get _fitWidth() {
|
|
var fitWidth;
|
|
if (this.fitInto === window) {
|
|
fitWidth = this.fitInto.innerWidth;
|
|
} else {
|
|
fitWidth = this.fitInto.getBoundingClientRect().width;
|
|
}
|
|
return fitWidth;
|
|
},
|
|
|
|
get _fitHeight() {
|
|
var fitHeight;
|
|
if (this.fitInto === window) {
|
|
fitHeight = this.fitInto.innerHeight;
|
|
} else {
|
|
fitHeight = this.fitInto.getBoundingClientRect().height;
|
|
}
|
|
return fitHeight;
|
|
},
|
|
|
|
get _fitLeft() {
|
|
var fitLeft;
|
|
if (this.fitInto === window) {
|
|
fitLeft = 0;
|
|
} else {
|
|
fitLeft = this.fitInto.getBoundingClientRect().left;
|
|
}
|
|
return fitLeft;
|
|
},
|
|
|
|
get _fitTop() {
|
|
var fitTop;
|
|
if (this.fitInto === window) {
|
|
fitTop = 0;
|
|
} else {
|
|
fitTop = this.fitInto.getBoundingClientRect().top;
|
|
}
|
|
return fitTop;
|
|
},
|
|
|
|
/**
|
|
* The element that should be used to position the element,
|
|
* if no position target is configured.
|
|
*/
|
|
get _defaultPositionTarget() {
|
|
var parent = Polymer.dom(this).parentNode;
|
|
|
|
if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
parent = parent.host;
|
|
}
|
|
|
|
return parent;
|
|
},
|
|
|
|
/**
|
|
* The horizontal align value, accounting for the RTL/LTR text direction.
|
|
*/
|
|
get _localeHorizontalAlign() {
|
|
if (this._isRTL) {
|
|
// In RTL, "left" becomes "right".
|
|
if (this.horizontalAlign === 'right') {
|
|
return 'left';
|
|
}
|
|
if (this.horizontalAlign === 'left') {
|
|
return 'right';
|
|
}
|
|
}
|
|
return this.horizontalAlign;
|
|
},
|
|
|
|
attached: function() {
|
|
// Memoize this to avoid expensive calculations & relayouts.
|
|
this._isRTL = window.getComputedStyle(this).direction == 'rtl';
|
|
this.positionTarget = this.positionTarget || this._defaultPositionTarget;
|
|
if (this.autoFitOnAttach) {
|
|
if (window.getComputedStyle(this).display === 'none') {
|
|
setTimeout(function() {
|
|
this.fit();
|
|
}.bind(this));
|
|
} else {
|
|
this.fit();
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Positions and fits the element into the `fitInto` element.
|
|
*/
|
|
fit: function() {
|
|
this._discoverInfo();
|
|
this.position();
|
|
this.constrain();
|
|
this.center();
|
|
},
|
|
|
|
/**
|
|
* Memoize information needed to position and size the target element.
|
|
*/
|
|
_discoverInfo: function() {
|
|
if (this._fitInfo) {
|
|
return;
|
|
}
|
|
var target = window.getComputedStyle(this);
|
|
var sizer = window.getComputedStyle(this.sizingTarget);
|
|
|
|
this._fitInfo = {
|
|
inlineStyle: {
|
|
top: this.style.top || '',
|
|
left: this.style.left || '',
|
|
position: this.style.position || ''
|
|
},
|
|
sizerInlineStyle: {
|
|
maxWidth: this.sizingTarget.style.maxWidth || '',
|
|
maxHeight: this.sizingTarget.style.maxHeight || '',
|
|
boxSizing: this.sizingTarget.style.boxSizing || ''
|
|
},
|
|
positionedBy: {
|
|
vertically: target.top !== 'auto' ? 'top' : (target.bottom !== 'auto' ?
|
|
'bottom' : null),
|
|
horizontally: target.left !== 'auto' ? 'left' : (target.right !== 'auto' ?
|
|
'right' : null)
|
|
},
|
|
sizedBy: {
|
|
height: sizer.maxHeight !== 'none',
|
|
width: sizer.maxWidth !== 'none',
|
|
minWidth: parseInt(sizer.minWidth, 10) || 0,
|
|
minHeight: parseInt(sizer.minHeight, 10) || 0
|
|
},
|
|
margin: {
|
|
top: parseInt(target.marginTop, 10) || 0,
|
|
right: parseInt(target.marginRight, 10) || 0,
|
|
bottom: parseInt(target.marginBottom, 10) || 0,
|
|
left: parseInt(target.marginLeft, 10) || 0
|
|
}
|
|
};
|
|
|
|
// Support these properties until they are removed.
|
|
if (this.verticalOffset) {
|
|
this._fitInfo.margin.top = this._fitInfo.margin.bottom = this.verticalOffset;
|
|
this._fitInfo.inlineStyle.marginTop = this.style.marginTop || '';
|
|
this._fitInfo.inlineStyle.marginBottom = this.style.marginBottom || '';
|
|
this.style.marginTop = this.style.marginBottom = this.verticalOffset + 'px';
|
|
}
|
|
if (this.horizontalOffset) {
|
|
this._fitInfo.margin.left = this._fitInfo.margin.right = this.horizontalOffset;
|
|
this._fitInfo.inlineStyle.marginLeft = this.style.marginLeft || '';
|
|
this._fitInfo.inlineStyle.marginRight = this.style.marginRight || '';
|
|
this.style.marginLeft = this.style.marginRight = this.horizontalOffset + 'px';
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Resets the target element's position and size constraints, and clear
|
|
* the memoized data.
|
|
*/
|
|
resetFit: function() {
|
|
var info = this._fitInfo || {};
|
|
for (var property in info.sizerInlineStyle) {
|
|
this.sizingTarget.style[property] = info.sizerInlineStyle[property];
|
|
}
|
|
for (var property in info.inlineStyle) {
|
|
this.style[property] = info.inlineStyle[property];
|
|
}
|
|
|
|
this._fitInfo = null;
|
|
},
|
|
|
|
/**
|
|
* Equivalent to calling `resetFit()` and `fit()`. Useful to call this after
|
|
* the element or the `fitInto` element has been resized, or if any of the
|
|
* positioning properties (e.g. `horizontalAlign, verticalAlign`) is updated.
|
|
*/
|
|
refit: function() {
|
|
this.resetFit();
|
|
this.fit();
|
|
},
|
|
|
|
/**
|
|
* Positions the element according to `horizontalAlign, verticalAlign`.
|
|
*/
|
|
position: function() {
|
|
if (!this.horizontalAlign && !this.verticalAlign) {
|
|
// needs to be centered, and it is done after constrain.
|
|
return;
|
|
}
|
|
|
|
this.style.position = 'fixed';
|
|
// Need border-box for margin/padding.
|
|
this.sizingTarget.style.boxSizing = 'border-box';
|
|
// Set to 0, 0 in order to discover any offset caused by parent stacking contexts.
|
|
this.style.left = '0px';
|
|
this.style.top = '0px';
|
|
|
|
var rect = this.getBoundingClientRect();
|
|
var positionRect = this.__getNormalizedRect(this.positionTarget);
|
|
var fitRect = this.__getNormalizedRect(this.fitInto);
|
|
|
|
// Consider the margin as part of the size for position calculations.
|
|
var width = rect.width + this._fitInfo.margin.left + this._fitInfo.margin.right;
|
|
var height = rect.height + this._fitInfo.margin.top + this._fitInfo.margin.bottom;
|
|
|
|
var alignRight = this.__isAlignRight(this._localeHorizontalAlign, width, positionRect, fitRect);
|
|
var alignBottom = this.__isAlignBottom(this.verticalAlign, height, positionRect, fitRect);
|
|
|
|
var top = alignBottom ? positionRect.bottom - height : positionRect.top;
|
|
var left = alignRight ? positionRect.right - width : positionRect.left;
|
|
|
|
if (this.noOverlap) {
|
|
// We can overlap one of the dimensions, choose the one that minimizes the cropped area.
|
|
var noOverlapLeft = left + positionRect.width * (alignRight ? -1 : 1);
|
|
var noOverlapTop = top + positionRect.height * (alignBottom ? -1 : 1);
|
|
var areaOverlapLeft = this.__getCroppedArea({
|
|
top: noOverlapTop,
|
|
left: left,
|
|
width: width,
|
|
height: height
|
|
}, fitRect);
|
|
var areaOverlapTop = this.__getCroppedArea({
|
|
top: top,
|
|
left: noOverlapLeft,
|
|
width: width,
|
|
height: height
|
|
}, fitRect);
|
|
if (areaOverlapLeft >= areaOverlapTop) {
|
|
left = noOverlapLeft;
|
|
} else {
|
|
top = noOverlapTop;
|
|
}
|
|
}
|
|
|
|
left += this._fitInfo.margin.left;
|
|
top += this._fitInfo.margin.top;
|
|
|
|
// Use original size (without margin)
|
|
var right = left + rect.width;
|
|
var bottom = top + rect.height;
|
|
|
|
left = Math.max(left, this._fitInfo.margin.left);
|
|
top = Math.max(top, this._fitInfo.margin.top);
|
|
|
|
var maxWidth = Math.min(fitRect.right - this._fitInfo.margin.right, right) - left;
|
|
var maxHeight = Math.min(fitRect.bottom - this._fitInfo.margin.bottom, bottom) - top;
|
|
|
|
var minWidth = this._fitInfo.sizedBy.minWidth;
|
|
var minHeight = this._fitInfo.sizedBy.minHeight;
|
|
|
|
if (maxWidth < minWidth || right < minWidth) {
|
|
left = -9999;
|
|
maxWidth = 0;
|
|
}
|
|
if (maxHeight < minHeight || bottom < minHeight) {
|
|
top = -9999;
|
|
maxHeight = 0;
|
|
}
|
|
|
|
this.sizingTarget.style.maxWidth = maxWidth + 'px';
|
|
this.sizingTarget.style.maxHeight = maxHeight + 'px';
|
|
|
|
// Remove the offset caused by any stacking context.
|
|
this.style.left = (left - rect.left) + 'px';
|
|
this.style.top = (top - rect.top) + 'px';
|
|
},
|
|
|
|
/**
|
|
* Constrains the size of the element to `fitInto` by setting `max-height`
|
|
* and/or `max-width`.
|
|
*/
|
|
constrain: function() {
|
|
if (this.horizontalAlign || this.verticalAlign) {
|
|
return;
|
|
}
|
|
var info = this._fitInfo;
|
|
// position at (0px, 0px) if not already positioned, so we can measure the natural size.
|
|
if (!info.positionedBy.vertically) {
|
|
this.style.position = 'fixed';
|
|
this.style.top = '0px';
|
|
}
|
|
if (!info.positionedBy.horizontally) {
|
|
this.style.position = 'fixed';
|
|
this.style.left = '0px';
|
|
}
|
|
|
|
// need border-box for margin/padding
|
|
this.sizingTarget.style.boxSizing = 'border-box';
|
|
// constrain the width and height if not already set
|
|
var rect = this.getBoundingClientRect();
|
|
if (!info.sizedBy.height) {
|
|
this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom', 'Height');
|
|
}
|
|
if (!info.sizedBy.width) {
|
|
this.__sizeDimension(rect, info.positionedBy.horizontally, 'left', 'right', 'Width');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @protected
|
|
* @deprecated
|
|
*/
|
|
_sizeDimension: function(rect, positionedBy, start, end, extent) {
|
|
this.__sizeDimension(rect, positionedBy, start, end, extent);
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
__sizeDimension: function(rect, positionedBy, start, end, extent) {
|
|
var info = this._fitInfo;
|
|
var fitRect = this.__getNormalizedRect(this.fitInto);
|
|
var max = extent === 'Width' ? fitRect.width : fitRect.height;
|
|
var flip = (positionedBy === end);
|
|
var offset = flip ? max - rect[end] : rect[start];
|
|
var margin = info.margin[flip ? start : end];
|
|
var offsetExtent = 'offset' + extent;
|
|
var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent];
|
|
this.sizingTarget.style['max' + extent] = (max - margin - offset - sizingOffset) + 'px';
|
|
},
|
|
|
|
/**
|
|
* Centers horizontally and vertically if not already positioned. This also sets
|
|
* `position:fixed`.
|
|
*/
|
|
center: function() {
|
|
if (this.horizontalAlign || this.verticalAlign) {
|
|
return;
|
|
}
|
|
var positionedBy = this._fitInfo.positionedBy;
|
|
if (positionedBy.vertically && positionedBy.horizontally) {
|
|
// Already positioned.
|
|
return;
|
|
}
|
|
// Need position:fixed to center
|
|
this.style.position = 'fixed';
|
|
// Take into account the offset caused by parents that create stacking
|
|
// contexts (e.g. with transform: translate3d). Translate to 0,0 and
|
|
// measure the bounding rect.
|
|
if (!positionedBy.vertically) {
|
|
this.style.top = '0px';
|
|
}
|
|
if (!positionedBy.horizontally) {
|
|
this.style.left = '0px';
|
|
}
|
|
// It will take in consideration margins and transforms
|
|
var rect = this.getBoundingClientRect();
|
|
var fitRect = this.__getNormalizedRect(this.fitInto);
|
|
if (!positionedBy.vertically) {
|
|
var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2;
|
|
this.style.top = top + 'px';
|
|
}
|
|
if (!positionedBy.horizontally) {
|
|
var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2;
|
|
this.style.left = left + 'px';
|
|
}
|
|
},
|
|
|
|
__getNormalizedRect: function(target) {
|
|
if (target === document.documentElement || target === window) {
|
|
return {
|
|
top: 0,
|
|
left: 0,
|
|
width: window.innerWidth,
|
|
height: window.innerHeight,
|
|
right: window.innerWidth,
|
|
bottom: window.innerHeight
|
|
};
|
|
}
|
|
return target.getBoundingClientRect();
|
|
},
|
|
|
|
__isAlignRight: function(hAlign, size, positionRect, fitRect) {
|
|
if (hAlign === 'right') {
|
|
return true;
|
|
}
|
|
if (hAlign === 'auto') {
|
|
// We should align on the right if positionTarget is on the right of fitInto,
|
|
// or if we can overlap and aligning on the left would cause more cropping
|
|
// than aligning on the right.
|
|
var positionTargetCenter = positionRect.left + positionRect.width/2;
|
|
var fitIntoCenter = fitRect.left + fitRect.width/2;
|
|
var croppedLeft = Math.abs(Math.min(0, positionRect.left));
|
|
var croppedRight = Math.abs(Math.min(0, positionRect.right - size));
|
|
return positionTargetCenter > fitIntoCenter ||
|
|
(!this.noOverlap && croppedLeft > croppedRight);
|
|
}
|
|
return false;
|
|
},
|
|
|
|
__isAlignBottom: function(vAlign, size, positionRect, fitRect) {
|
|
if (vAlign === 'bottom') {
|
|
return true;
|
|
}
|
|
if (vAlign === 'auto') {
|
|
// We should align on the bottom if positionTarget is on the bottom of fitInto,
|
|
// or if we can overlap and aligning on the top would cause more cropping
|
|
// than aligning on the bottom.
|
|
var positionTargetCenter = positionRect.top + positionRect.height/2;
|
|
var fitIntoCenter = fitRect.top + fitRect.height/2;
|
|
var croppedTop = Math.abs(Math.min(0, positionRect.top));
|
|
var croppedBottom = Math.abs(Math.min(0, positionRect.bottom - size));
|
|
return positionTargetCenter > fitIntoCenter ||
|
|
(!this.noOverlap && croppedTop > croppedBottom);
|
|
}
|
|
return false;
|
|
},
|
|
|
|
__getCroppedArea: function(rect, fitRect) {
|
|
var verticalCrop = Math.min(0, rect.top) + Math.min(0, fitRect.bottom - (rect.top + rect.height));
|
|
var horizontalCrop = Math.min(0, rect.left) + Math.min(0, fitRect.right - (rect.left + rect.width));
|
|
return Math.abs(verticalCrop) * rect.width + Math.abs(horizontalCrop) * rect.height;
|
|
}
|
|
|
|
};
|
|
</script>
|