• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1<!--
2@license
3Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
4This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
5The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
6The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
7Code distributed by Google as part of the polymer project is also
8subject 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
16optionally centers it in the window or another element.
17
18The element will only be sized and/or positioned if it has not already been sized and/or positioned
19by CSS.
20
21CSS 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
38Use `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