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