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