UNPKG

spincycle

Version:

A reactive message router and object manager that lets clients subscribe to object property changes on the server

599 lines (529 loc) 19.5 kB
<!-- @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 }, /** * 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: { 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.position(); this.constrain(); this.center(); }, /** * Memoize information needed to position and size the target element. * @suppress {deprecated} */ _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. * 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._discoverInfo(); 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 `fitInto` by setting `max-height` * and/or `max-width`. */ constrain: function() { if (this.horizontalAlign || this.verticalAlign) { return; } this._discoverInfo(); 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; } this._discoverInfo(); 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(); }, __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>