Index: third_party/polymer/components/iron-fit-behavior/iron-fit-behavior.html |
diff --git a/third_party/polymer/components/iron-fit-behavior/iron-fit-behavior.html b/third_party/polymer/components/iron-fit-behavior/iron-fit-behavior.html |
index 1705f31a20edf1e1685917ec88aa79a974e8f2bb..d51d87dfe44f135dce3cdb01d95536709d68923f 100644 |
--- a/third_party/polymer/components/iron-fit-behavior/iron-fit-behavior.html |
+++ b/third_party/polymer/components/iron-fit-behavior/iron-fit-behavior.html |
@@ -11,9 +11,8 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN |
<link rel="import" href="../polymer/polymer.html"> |
<script> |
- |
/** |
-Polymer.IronFitBehavior fits an element in another element using `max-height` and `max-width`, and |
+`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 |
@@ -24,8 +23,25 @@ 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` or `height` set | Element respects `max-height` or `height` |
-`max-width` or `width` set | Element respects `max-width` or `width` |
+`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 |
@@ -57,6 +73,66 @@ CSS properties | Action |
}, |
/** |
+ * 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 |
+ }, |
+ |
+ /** |
+ * If true, it will use `horizontalAlign` and `verticalAlign` values as preferred alignment |
+ * and if there's not enough space, it will pick the values which minimize the cropping. |
+ */ |
+ dynamicAlign: { |
+ type: Boolean |
+ }, |
+ |
+ /** |
+ * 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: { |
@@ -68,7 +144,6 @@ CSS properties | Action |
_fitInfo: { |
type: Object |
} |
- |
}, |
get _fitWidth() { |
@@ -111,7 +186,40 @@ CSS properties | Action |
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() { |
@@ -124,16 +232,18 @@ CSS properties | Action |
}, |
/** |
- * Fits and optionally centers the element into the window, or `fitInfo` if specified. |
+ * 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. |
+ * @suppress {deprecated} |
*/ |
_discoverInfo: function() { |
if (this._fitInfo) { |
@@ -141,21 +251,29 @@ CSS properties | Action |
} |
var target = window.getComputedStyle(this); |
var sizer = window.getComputedStyle(this.sizingTarget); |
+ |
this._fitInfo = { |
inlineStyle: { |
top: this.style.top || '', |
- left: this.style.left || '' |
+ 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), |
- css: target.position |
+ 'right' : null) |
}, |
sizedBy: { |
height: sizer.maxHeight !== 'none', |
- width: sizer.maxWidth !== 'none' |
+ width: sizer.maxWidth !== 'none', |
+ minWidth: parseInt(sizer.minWidth, 10) || 0, |
+ minHeight: parseInt(sizer.minHeight, 10) || 0 |
}, |
margin: { |
top: parseInt(target.marginTop, 10) || 0, |
@@ -164,6 +282,20 @@ CSS properties | Action |
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'; |
+ } |
}, |
/** |
@@ -171,61 +303,138 @@ CSS properties | Action |
* the memoized data. |
*/ |
resetFit: function() { |
- if (!this._fitInfo || !this._fitInfo.sizedBy.width) { |
- this.sizingTarget.style.maxWidth = ''; |
- } |
- if (!this._fitInfo || !this._fitInfo.sizedBy.height) { |
- this.sizingTarget.style.maxHeight = ''; |
+ var info = this._fitInfo || {}; |
+ for (var property in info.sizerInlineStyle) { |
+ this.sizingTarget.style[property] = info.sizerInlineStyle[property]; |
} |
- this.style.top = this._fitInfo ? this._fitInfo.inlineStyle.top : ''; |
- this.style.left = this._fitInfo ? this._fitInfo.inlineStyle.left : ''; |
- if (this._fitInfo) { |
- this.style.position = this._fitInfo.positionedBy.css; |
+ 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, |
- * the window, or the `fitInfo` element has been resized. |
+ * 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. |
+ * It preserves the scroll position of the sizingTarget. |
*/ |
refit: function() { |
+ var scrollLeft = this.sizingTarget.scrollLeft; |
+ var scrollTop = this.sizingTarget.scrollTop; |
this.resetFit(); |
this.fit(); |
+ this.sizingTarget.scrollLeft = scrollLeft; |
+ this.sizingTarget.scrollTop = scrollTop; |
+ }, |
+ |
+ /** |
+ * 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); |
+ |
+ var margin = this._fitInfo.margin; |
+ |
+ // Consider the margin as part of the size for position calculations. |
+ var size = { |
+ width: rect.width + margin.left + margin.right, |
+ height: rect.height + margin.top + margin.bottom |
+ }; |
+ |
+ var position = this.__getPosition(this._localeHorizontalAlign, this.verticalAlign, size, positionRect, fitRect); |
+ |
+ var left = position.left + margin.left; |
+ var top = position.top + margin.top; |
+ |
+ // Use original size (without margin). |
+ var right = Math.min(fitRect.right - margin.right, left + rect.width); |
+ var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height); |
+ |
+ var minWidth = this._fitInfo.sizedBy.minWidth; |
+ var minHeight = this._fitInfo.sizedBy.minHeight; |
+ if (left < margin.left) { |
+ left = margin.left; |
+ if (right - left < minWidth) { |
+ left = right - minWidth; |
+ } |
+ } |
+ if (top < margin.top) { |
+ top = margin.top; |
+ if (bottom - top < minHeight) { |
+ top = bottom - minHeight; |
+ } |
+ } |
+ |
+ this.sizingTarget.style.maxWidth = (right - left) + 'px'; |
+ this.sizingTarget.style.maxHeight = (bottom - top) + '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 the window or `fitInfo` by setting `max-height` |
+ * 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 (!this._fitInfo.positionedBy.vertically) { |
+ if (!info.positionedBy.vertically) { |
+ this.style.position = 'fixed'; |
this.style.top = '0px'; |
} |
- if (!this._fitInfo.positionedBy.horizontally) { |
- this.style.left = '0px'; |
- } |
- if (!this._fitInfo.positionedBy.vertically || !this._fitInfo.positionedBy.horizontally) { |
- // need position:fixed to properly size the element |
+ 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'); |
+ this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom', 'Height'); |
} |
if (!info.sizedBy.width) { |
- this._sizeDimension(rect, info.positionedBy.horizontally, 'left', 'right', '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 max = extent === 'Width' ? this._fitWidth : this._fitHeight; |
+ 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]; |
@@ -239,6 +448,9 @@ CSS properties | Action |
* `position:fixed`. |
*/ |
center: function() { |
+ if (this.horizontalAlign || this.verticalAlign) { |
+ return; |
+ } |
var positionedBy = this._fitInfo.positionedBy; |
if (positionedBy.vertically && positionedBy.horizontally) { |
// Already positioned. |
@@ -257,16 +469,126 @@ CSS properties | Action |
} |
// It will take in consideration margins and transforms |
var rect = this.getBoundingClientRect(); |
+ var fitRect = this.__getNormalizedRect(this.fitInto); |
if (!positionedBy.vertically) { |
- var top = this._fitTop - rect.top + (this._fitHeight - rect.height) / 2; |
+ var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2; |
this.style.top = top + 'px'; |
} |
if (!positionedBy.horizontally) { |
- var left = this._fitLeft - rect.left + (this._fitWidth - rect.width) / 2; |
+ 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(); |
+ }, |
+ |
+ __getCroppedArea: function(position, size, fitRect) { |
+ var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom - (position.top + size.height)); |
+ var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.right - (position.left + size.width)); |
+ return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * size.height; |
+ }, |
+ |
+ |
+ __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) { |
+ // All the possible configurations. |
+ // Ordered as top-left, top-right, bottom-left, bottom-right. |
+ var positions = [{ |
+ verticalAlign: 'top', |
+ horizontalAlign: 'left', |
+ top: positionRect.top, |
+ left: positionRect.left |
+ }, { |
+ verticalAlign: 'top', |
+ horizontalAlign: 'right', |
+ top: positionRect.top, |
+ left: positionRect.right - size.width |
+ }, { |
+ verticalAlign: 'bottom', |
+ horizontalAlign: 'left', |
+ top: positionRect.bottom - size.height, |
+ left: positionRect.left |
+ }, { |
+ verticalAlign: 'bottom', |
+ horizontalAlign: 'right', |
+ top: positionRect.bottom - size.height, |
+ left: positionRect.right - size.width |
+ }]; |
+ |
+ if (this.noOverlap) { |
+ // Duplicate. |
+ for (var i = 0, l = positions.length; i < l; i++) { |
+ var copy = {}; |
+ for (var key in positions[i]) { |
+ copy[key] = positions[i][key]; |
+ } |
+ positions.push(copy); |
+ } |
+ // Horizontal overlap only. |
+ positions[0].top = positions[1].top += positionRect.height; |
+ positions[2].top = positions[3].top -= positionRect.height; |
+ // Vertical overlap only. |
+ positions[4].left = positions[6].left += positionRect.width; |
+ positions[5].left = positions[7].left -= positionRect.width; |
+ } |
+ |
+ // Consider auto as null for coding convenience. |
+ vAlign = vAlign === 'auto' ? null : vAlign; |
+ hAlign = hAlign === 'auto' ? null : hAlign; |
+ |
+ var position; |
+ for (var i = 0; i < positions.length; i++) { |
+ var pos = positions[i]; |
+ |
+ // If both vAlign and hAlign are defined, return exact match. |
+ // For dynamicAlign and noOverlap we'll have more than one candidate, so |
+ // we'll have to check the croppedArea to make the best choice. |
+ if (!this.dynamicAlign && !this.noOverlap && |
+ pos.verticalAlign === vAlign && pos.horizontalAlign === hAlign) { |
+ position = pos; |
+ break; |
+ } |
+ |
+ // Align is ok if alignment preferences are respected. If no preferences, |
+ // it is considered ok. |
+ var alignOk = (!vAlign || pos.verticalAlign === vAlign) && |
+ (!hAlign || pos.horizontalAlign === hAlign); |
+ |
+ // Filter out elements that don't match the alignment (if defined). |
+ // With dynamicAlign, we need to consider all the positions to find the |
+ // one that minimizes the cropped area. |
+ if (!this.dynamicAlign && !alignOk) { |
+ continue; |
+ } |
+ |
+ position = position || pos; |
+ pos.croppedArea = this.__getCroppedArea(pos, size, fitRect); |
+ var diff = pos.croppedArea - position.croppedArea; |
+ // Check which crops less. If it crops equally, check if align is ok. |
+ if (diff < 0 || (diff === 0 && alignOk)) { |
+ position = pos; |
+ } |
+ // If not cropped and respects the align requirements, keep it. |
+ // This allows to prefer positions overlapping horizontally over the |
+ // ones overlapping vertically. |
+ if (position.croppedArea === 0 && alignOk) { |
+ break; |
+ } |
+ } |
+ |
+ return position; |
} |
}; |
- |
</script> |