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