Home | History | Annotate | Download | only in iron-fit-behavior
      1 <!--
      2 @license
      3 Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
      4 This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
      5 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
      6 The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
      7 Code distributed by Google as part of the polymer project is also
      8 subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
      9 -->
     10 
     11 <link rel="import" href="../polymer/polymer.html">
     12 
     13 <script>
     14 /**
     15 `Polymer.IronFitBehavior` fits an element in another element using `max-height` and `max-width`, and
     16 optionally centers it in the window or another element.
     17 
     18 The element will only be sized and/or positioned if it has not already been sized and/or positioned
     19 by CSS.
     20 
     21 CSS properties               | Action
     22 -----------------------------|-------------------------------------------
     23 `position` set               | Element is not centered horizontally or vertically
     24 `top` or `bottom` set        | Element is not vertically centered
     25 `left` or `right` set        | Element is not horizontally centered
     26 `max-height` set             | Element respects `max-height`
     27 `max-width` set              | Element respects `max-width`
     28 
     29 `Polymer.IronFitBehavior` can position an element into another element using
     30 `verticalAlign` and `horizontalAlign`. This will override the element's css position.
     31 
     32       <div class="container">
     33         <iron-fit-impl vertical-align="top" horizontal-align="auto">
     34           Positioned into the container
     35         </iron-fit-impl>
     36       </div>
     37 
     38 Use `noOverlap` to position the element around another element without overlapping it.
     39 
     40       <div class="container">
     41         <iron-fit-impl no-overlap vertical-align="auto" horizontal-align="auto">
     42           Positioned around the container
     43         </iron-fit-impl>
     44       </div>
     45 
     46 @demo demo/index.html
     47 @polymerBehavior
     48 */
     49 
     50   Polymer.IronFitBehavior = {
     51 
     52     properties: {
     53 
     54       /**
     55        * The element that will receive a `max-height`/`width`. By default it is the same as `this`,
     56        * but it can be set to a child element. This is useful, for example, for implementing a
     57        * scrolling region inside the element.
     58        * @type {!Element}
     59        */
     60       sizingTarget: {
     61         type: Object,
     62         value: function() {
     63           return this;
     64         }
     65       },
     66 
     67       /**
     68        * The element to fit `this` into.
     69        */
     70       fitInto: {
     71         type: Object,
     72         value: window
     73       },
     74 
     75       /**
     76        * Will position the element around the positionTarget without overlapping it.
     77        */
     78       noOverlap: {
     79         type: Boolean
     80       },
     81 
     82       /**
     83        * The element that should be used to position the element. If not set, it will
     84        * default to the parent node.
     85        * @type {!Element}
     86        */
     87       positionTarget: {
     88         type: Element
     89       },
     90 
     91       /**
     92        * The orientation against which to align the element horizontally
     93        * relative to the `positionTarget`. Possible values are "left", "right", "auto".
     94        */
     95       horizontalAlign: {
     96         type: String
     97       },
     98 
     99       /**
    100        * The orientation against which to align the element vertically
    101        * relative to the `positionTarget`. Possible values are "top", "bottom", "auto".
    102        */
    103       verticalAlign: {
    104         type: String
    105       },
    106 
    107       /**
    108        * If true, it will use `horizontalAlign` and `verticalAlign` values as preferred alignment
    109        * and if there's not enough space, it will pick the values which minimize the cropping.
    110        */
    111       dynamicAlign: {
    112         type: Boolean
    113       },
    114 
    115       /**
    116        * The same as setting margin-left and margin-right css properties.
    117        * @deprecated
    118        */
    119       horizontalOffset: {
    120         type: Number,
    121         value: 0,
    122         notify: true
    123       },
    124 
    125       /**
    126        * The same as setting margin-top and margin-bottom css properties.
    127        * @deprecated
    128        */
    129       verticalOffset: {
    130         type: Number,
    131         value: 0,
    132         notify: true
    133       },
    134 
    135       /**
    136        * Set to true to auto-fit on attach.
    137        */
    138       autoFitOnAttach: {
    139         type: Boolean,
    140         value: false
    141       },
    142 
    143       /** @type {?Object} */
    144       _fitInfo: {
    145         type: Object
    146       }
    147     },
    148 
    149     get _fitWidth() {
    150       var fitWidth;
    151       if (this.fitInto === window) {
    152         fitWidth = this.fitInto.innerWidth;
    153       } else {
    154         fitWidth = this.fitInto.getBoundingClientRect().width;
    155       }
    156       return fitWidth;
    157     },
    158 
    159     get _fitHeight() {
    160       var fitHeight;
    161       if (this.fitInto === window) {
    162         fitHeight = this.fitInto.innerHeight;
    163       } else {
    164         fitHeight = this.fitInto.getBoundingClientRect().height;
    165       }
    166       return fitHeight;
    167     },
    168 
    169     get _fitLeft() {
    170       var fitLeft;
    171       if (this.fitInto === window) {
    172         fitLeft = 0;
    173       } else {
    174         fitLeft = this.fitInto.getBoundingClientRect().left;
    175       }
    176       return fitLeft;
    177     },
    178 
    179     get _fitTop() {
    180       var fitTop;
    181       if (this.fitInto === window) {
    182         fitTop = 0;
    183       } else {
    184         fitTop = this.fitInto.getBoundingClientRect().top;
    185       }
    186       return fitTop;
    187     },
    188 
    189     /**
    190      * The element that should be used to position the element,
    191      * if no position target is configured.
    192      */
    193     get _defaultPositionTarget() {
    194       var parent = Polymer.dom(this).parentNode;
    195 
    196       if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
    197         parent = parent.host;
    198       }
    199 
    200       return parent;
    201     },
    202 
    203     /**
    204      * The horizontal align value, accounting for the RTL/LTR text direction.
    205      */
    206     get _localeHorizontalAlign() {
    207       if (this._isRTL) {
    208         // In RTL, "left" becomes "right".
    209         if (this.horizontalAlign === 'right') {
    210           return 'left';
    211         }
    212         if (this.horizontalAlign === 'left') {
    213           return 'right';
    214         }
    215       }
    216       return this.horizontalAlign;
    217     },
    218 
    219     attached: function() {
    220       // Memoize this to avoid expensive calculations & relayouts.
    221       this._isRTL = window.getComputedStyle(this).direction == 'rtl';
    222       this.positionTarget = this.positionTarget || this._defaultPositionTarget;
    223       if (this.autoFitOnAttach) {
    224         if (window.getComputedStyle(this).display === 'none') {
    225           setTimeout(function() {
    226             this.fit();
    227           }.bind(this));
    228         } else {
    229           this.fit();
    230         }
    231       }
    232     },
    233 
    234     /**
    235      * Positions and fits the element into the `fitInto` element.
    236      */
    237     fit: function() {
    238       this._discoverInfo();
    239       this.position();
    240       this.constrain();
    241       this.center();
    242     },
    243 
    244     /**
    245      * Memoize information needed to position and size the target element.
    246      * @suppress {deprecated}
    247      */
    248     _discoverInfo: function() {
    249       if (this._fitInfo) {
    250         return;
    251       }
    252       var target = window.getComputedStyle(this);
    253       var sizer = window.getComputedStyle(this.sizingTarget);
    254 
    255       this._fitInfo = {
    256         inlineStyle: {
    257           top: this.style.top || '',
    258           left: this.style.left || '',
    259           position: this.style.position || ''
    260         },
    261         sizerInlineStyle: {
    262           maxWidth: this.sizingTarget.style.maxWidth || '',
    263           maxHeight: this.sizingTarget.style.maxHeight || '',
    264           boxSizing: this.sizingTarget.style.boxSizing || ''
    265         },
    266         positionedBy: {
    267           vertically: target.top !== 'auto' ? 'top' : (target.bottom !== 'auto' ?
    268             'bottom' : null),
    269           horizontally: target.left !== 'auto' ? 'left' : (target.right !== 'auto' ?
    270             'right' : null)
    271         },
    272         sizedBy: {
    273           height: sizer.maxHeight !== 'none',
    274           width: sizer.maxWidth !== 'none',
    275           minWidth: parseInt(sizer.minWidth, 10) || 0,
    276           minHeight: parseInt(sizer.minHeight, 10) || 0
    277         },
    278         margin: {
    279           top: parseInt(target.marginTop, 10) || 0,
    280           right: parseInt(target.marginRight, 10) || 0,
    281           bottom: parseInt(target.marginBottom, 10) || 0,
    282           left: parseInt(target.marginLeft, 10) || 0
    283         }
    284       };
    285 
    286       // Support these properties until they are removed.
    287       if (this.verticalOffset) {
    288         this._fitInfo.margin.top = this._fitInfo.margin.bottom = this.verticalOffset;
    289         this._fitInfo.inlineStyle.marginTop = this.style.marginTop || '';
    290         this._fitInfo.inlineStyle.marginBottom = this.style.marginBottom || '';
    291         this.style.marginTop = this.style.marginBottom = this.verticalOffset + 'px';
    292       }
    293       if (this.horizontalOffset) {
    294         this._fitInfo.margin.left = this._fitInfo.margin.right = this.horizontalOffset;
    295         this._fitInfo.inlineStyle.marginLeft = this.style.marginLeft || '';
    296         this._fitInfo.inlineStyle.marginRight = this.style.marginRight || '';
    297         this.style.marginLeft = this.style.marginRight = this.horizontalOffset + 'px';
    298       }
    299     },
    300 
    301     /**
    302      * Resets the target element's position and size constraints, and clear
    303      * the memoized data.
    304      */
    305     resetFit: function() {
    306       var info = this._fitInfo || {};
    307       for (var property in info.sizerInlineStyle) {
    308         this.sizingTarget.style[property] = info.sizerInlineStyle[property];
    309       }
    310       for (var property in info.inlineStyle) {
    311         this.style[property] = info.inlineStyle[property];
    312       }
    313 
    314       this._fitInfo = null;
    315     },
    316 
    317     /**
    318      * Equivalent to calling `resetFit()` and `fit()`. Useful to call this after
    319      * the element or the `fitInto` element has been resized, or if any of the
    320      * positioning properties (e.g. `horizontalAlign, verticalAlign`) is updated.
    321      * It preserves the scroll position of the sizingTarget.
    322      */
    323     refit: function() {
    324       var scrollLeft = this.sizingTarget.scrollLeft;
    325       var scrollTop = this.sizingTarget.scrollTop;
    326       this.resetFit();
    327       this.fit();
    328       this.sizingTarget.scrollLeft = scrollLeft;
    329       this.sizingTarget.scrollTop = scrollTop;
    330     },
    331 
    332     /**
    333      * Positions the element according to `horizontalAlign, verticalAlign`.
    334      */
    335     position: function() {
    336       if (!this.horizontalAlign && !this.verticalAlign) {
    337         // needs to be centered, and it is done after constrain.
    338         return;
    339       }
    340 
    341       this.style.position = 'fixed';
    342       // Need border-box for margin/padding.
    343       this.sizingTarget.style.boxSizing = 'border-box';
    344       // Set to 0, 0 in order to discover any offset caused by parent stacking contexts.
    345       this.style.left = '0px';
    346       this.style.top = '0px';
    347 
    348       var rect = this.getBoundingClientRect();
    349       var positionRect = this.__getNormalizedRect(this.positionTarget);
    350       var fitRect = this.__getNormalizedRect(this.fitInto);
    351 
    352       var margin = this._fitInfo.margin;
    353 
    354       // Consider the margin as part of the size for position calculations.
    355       var size = {
    356         width: rect.width + margin.left + margin.right,
    357         height: rect.height + margin.top + margin.bottom
    358       };
    359 
    360       var position = this.__getPosition(this._localeHorizontalAlign, this.verticalAlign, size, positionRect, fitRect);
    361 
    362       var left = position.left + margin.left;
    363       var top = position.top + margin.top;
    364 
    365       // Use original size (without margin).
    366       var right = Math.min(fitRect.right - margin.right, left + rect.width);
    367       var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height);
    368 
    369       var minWidth = this._fitInfo.sizedBy.minWidth;
    370       var minHeight = this._fitInfo.sizedBy.minHeight;
    371       if (left < margin.left) {
    372         left = margin.left;
    373         if (right - left < minWidth) {
    374           left = right - minWidth;
    375         }
    376       }
    377       if (top < margin.top) {
    378         top = margin.top;
    379         if (bottom - top < minHeight) {
    380           top = bottom - minHeight;
    381         }
    382       }
    383 
    384       this.sizingTarget.style.maxWidth = (right - left) + 'px';
    385       this.sizingTarget.style.maxHeight = (bottom - top) + 'px';
    386 
    387       // Remove the offset caused by any stacking context.
    388       this.style.left = (left - rect.left) + 'px';
    389       this.style.top = (top - rect.top) + 'px';
    390     },
    391 
    392     /**
    393      * Constrains the size of the element to `fitInto` by setting `max-height`
    394      * and/or `max-width`.
    395      */
    396     constrain: function() {
    397       if (this.horizontalAlign || this.verticalAlign) {
    398         return;
    399       }
    400       var info = this._fitInfo;
    401       // position at (0px, 0px) if not already positioned, so we can measure the natural size.
    402       if (!info.positionedBy.vertically) {
    403         this.style.position = 'fixed';
    404         this.style.top = '0px';
    405       }
    406       if (!info.positionedBy.horizontally) {
    407         this.style.position = 'fixed';
    408         this.style.left = '0px';
    409       }
    410 
    411       // need border-box for margin/padding
    412       this.sizingTarget.style.boxSizing = 'border-box';
    413       // constrain the width and height if not already set
    414       var rect = this.getBoundingClientRect();
    415       if (!info.sizedBy.height) {
    416         this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom', 'Height');
    417       }
    418       if (!info.sizedBy.width) {
    419         this.__sizeDimension(rect, info.positionedBy.horizontally, 'left', 'right', 'Width');
    420       }
    421     },
    422 
    423     /**
    424      * @protected
    425      * @deprecated
    426      */
    427     _sizeDimension: function(rect, positionedBy, start, end, extent) {
    428       this.__sizeDimension(rect, positionedBy, start, end, extent);
    429     },
    430 
    431     /**
    432      * @private
    433      */
    434     __sizeDimension: function(rect, positionedBy, start, end, extent) {
    435       var info = this._fitInfo;
    436       var fitRect = this.__getNormalizedRect(this.fitInto);
    437       var max = extent === 'Width' ? fitRect.width : fitRect.height;
    438       var flip = (positionedBy === end);
    439       var offset = flip ? max - rect[end] : rect[start];
    440       var margin = info.margin[flip ? start : end];
    441       var offsetExtent = 'offset' + extent;
    442       var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent];
    443       this.sizingTarget.style['max' + extent] = (max - margin - offset - sizingOffset) + 'px';
    444     },
    445 
    446     /**
    447      * Centers horizontally and vertically if not already positioned. This also sets
    448      * `position:fixed`.
    449      */
    450     center: function() {
    451       if (this.horizontalAlign || this.verticalAlign) {
    452         return;
    453       }
    454       var positionedBy = this._fitInfo.positionedBy;
    455       if (positionedBy.vertically && positionedBy.horizontally) {
    456         // Already positioned.
    457         return;
    458       }
    459       // Need position:fixed to center
    460       this.style.position = 'fixed';
    461       // Take into account the offset caused by parents that create stacking
    462       // contexts (e.g. with transform: translate3d). Translate to 0,0 and
    463       // measure the bounding rect.
    464       if (!positionedBy.vertically) {
    465         this.style.top = '0px';
    466       }
    467       if (!positionedBy.horizontally) {
    468         this.style.left = '0px';
    469       }
    470       // It will take in consideration margins and transforms
    471       var rect = this.getBoundingClientRect();
    472       var fitRect = this.__getNormalizedRect(this.fitInto);
    473       if (!positionedBy.vertically) {
    474         var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2;
    475         this.style.top = top + 'px';
    476       }
    477       if (!positionedBy.horizontally) {
    478         var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2;
    479         this.style.left = left + 'px';
    480       }
    481     },
    482 
    483     __getNormalizedRect: function(target) {
    484       if (target === document.documentElement || target === window) {
    485         return {
    486           top: 0,
    487           left: 0,
    488           width: window.innerWidth,
    489           height: window.innerHeight,
    490           right: window.innerWidth,
    491           bottom: window.innerHeight
    492         };
    493       }
    494       return target.getBoundingClientRect();
    495     },
    496 
    497     __getCroppedArea: function(position, size, fitRect) {
    498       var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom - (position.top + size.height));
    499       var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.right - (position.left + size.width));
    500       return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * size.height;
    501     },
    502 
    503 
    504     __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) {
    505       // All the possible configurations.
    506       // Ordered as top-left, top-right, bottom-left, bottom-right.
    507       var positions = [{
    508         verticalAlign: 'top',
    509         horizontalAlign: 'left',
    510         top: positionRect.top,
    511         left: positionRect.left
    512       }, {
    513         verticalAlign: 'top',
    514         horizontalAlign: 'right',
    515         top: positionRect.top,
    516         left: positionRect.right - size.width
    517       }, {
    518         verticalAlign: 'bottom',
    519         horizontalAlign: 'left',
    520         top: positionRect.bottom - size.height,
    521         left: positionRect.left
    522       }, {
    523         verticalAlign: 'bottom',
    524         horizontalAlign: 'right',
    525         top: positionRect.bottom - size.height,
    526         left: positionRect.right - size.width
    527       }];
    528 
    529       if (this.noOverlap) {
    530         // Duplicate.
    531         for (var i = 0, l = positions.length; i < l; i++) {
    532           var copy = {};
    533           for (var key in positions[i]) {
    534             copy[key] = positions[i][key];
    535           }
    536           positions.push(copy);
    537         }
    538         // Horizontal overlap only.
    539         positions[0].top = positions[1].top += positionRect.height;
    540         positions[2].top = positions[3].top -= positionRect.height;
    541         // Vertical overlap only.
    542         positions[4].left = positions[6].left += positionRect.width;
    543         positions[5].left = positions[7].left -= positionRect.width;
    544       }
    545 
    546       // Consider auto as null for coding convenience.
    547       vAlign = vAlign === 'auto' ? null : vAlign;
    548       hAlign = hAlign === 'auto' ? null : hAlign;
    549 
    550       var position;
    551       for (var i = 0; i < positions.length; i++) {
    552         var pos = positions[i];
    553 
    554         // If both vAlign and hAlign are defined, return exact match.
    555         // For dynamicAlign and noOverlap we'll have more than one candidate, so
    556         // we'll have to check the croppedArea to make the best choice.
    557         if (!this.dynamicAlign && !this.noOverlap &&
    558             pos.verticalAlign === vAlign && pos.horizontalAlign === hAlign) {
    559           position = pos;
    560           break;
    561         }
    562 
    563         // Align is ok if alignment preferences are respected. If no preferences,
    564         // it is considered ok.
    565         var alignOk = (!vAlign || pos.verticalAlign === vAlign) &&
    566                       (!hAlign || pos.horizontalAlign === hAlign);
    567 
    568         // Filter out elements that don't match the alignment (if defined).
    569         // With dynamicAlign, we need to consider all the positions to find the
    570         // one that minimizes the cropped area.
    571         if (!this.dynamicAlign && !alignOk) {
    572           continue;
    573         }
    574 
    575         position = position || pos;
    576         pos.croppedArea = this.__getCroppedArea(pos, size, fitRect);
    577         var diff = pos.croppedArea - position.croppedArea;
    578         // Check which crops less. If it crops equally, check if align is ok.
    579         if (diff < 0 || (diff === 0 && alignOk)) {
    580           position = pos;
    581         }
    582         // If not cropped and respects the align requirements, keep it.
    583         // This allows to prefer positions overlapping horizontally over the
    584         // ones overlapping vertically.
    585         if (position.croppedArea === 0 && alignOk) {
    586           break;
    587         }
    588       }
    589 
    590       return position;
    591     }
    592 
    593   };
    594 </script>
    595