From 68aa26bd0f6ba2f738e81e310322a3d396945b9f Mon Sep 17 00:00:00 2001 From: Valdrin Koshi Date: Wed, 25 Oct 2017 20:24:46 -0700 Subject: [PATCH] horizontalAlign = center, verticalAlign = middle (#78) * horizontalAlign = center, verticalAlign = middle * rename variables & functions --- demo/index.html | 2 + iron-fit-behavior.html | 97 ++++++++++++------ test/iron-fit-behavior.html | 192 ++++++++++++++++++++++++++++++++++++ 3 files changed, 263 insertions(+), 28 deletions(-) diff --git a/demo/index.html b/demo/index.html index dde1d55..3b166d9 100644 --- a/demo/index.html +++ b/demo/index.html @@ -80,12 +80,14 @@

Align

+

+ diff --git a/iron-fit-behavior.html b/iron-fit-behavior.html index 3b44d46..304ae7a 100644 --- a/iron-fit-behavior.html +++ b/iron-fit-behavior.html @@ -100,7 +100,7 @@ /** * The orientation against which to align the element horizontally - * relative to the `positionTarget`. Possible values are "left", "right", "auto". + * relative to the `positionTarget`. Possible values are "left", "right", "center", "auto". */ horizontalAlign: { type: String @@ -108,7 +108,7 @@ /** * The orientation against which to align the element vertically - * relative to the `positionTarget`. Possible values are "top", "bottom", "auto". + * relative to the `positionTarget`. Possible values are "top", "bottom", "middle", "auto". */ verticalAlign: { type: String @@ -128,8 +128,8 @@ * of it as increasing or decreasing the distance to the side of the * screen given by `horizontalAlign`. * - * If `horizontalAlign` is "left", this offset will increase or decrease - * the distance to the left side of the screen: a negative offset will + * If `horizontalAlign` is "left" or "center", this offset will increase or + * decrease the distance to the left side of the screen: a negative offset will * move the dropdown to the left; a positive one, to the right. * * Conversely if `horizontalAlign` is "right", this offset will increase @@ -148,8 +148,8 @@ * of it as increasing or decreasing the distance to the side of the * screen given by `verticalAlign`. * - * If `verticalAlign` is "top", this offset will increase or decrease - * the distance to the top side of the screen: a negative offset will + * If `verticalAlign` is "top" or "middle", this offset will increase or + * decrease the distance to the top side of the screen: a negative offset will * move the dropdown upwards; a positive one, downwards. * * Conversely if `verticalAlign` is "bottom", this offset will increase @@ -246,6 +246,15 @@ return this.horizontalAlign; }, + /** + * True if the element should be positioned instead of centered. + * @private + */ + get __shouldPosition() { + return (this.horizontalAlign || this.verticalAlign) && + (this.horizontalAlign !== 'center' || this.verticalAlign !== 'middle'); + }, + attached: function() { // Memoize this to avoid expensive calculations & relayouts. // Make sure we do it only once @@ -362,7 +371,7 @@ * Positions the element according to `horizontalAlign, verticalAlign`. */ position: function() { - if (!this.horizontalAlign && !this.verticalAlign) { + if (!this.__shouldPosition) { // needs to be centered, and it is done after constrain. return; } @@ -387,7 +396,7 @@ height: rect.height + margin.top + margin.bottom }; - var position = this.__getPosition(this._localeHorizontalAlign, this.verticalAlign, size, positionRect, + var position = this.__getPosition(this._localeHorizontalAlign, this.verticalAlign, size, rect, positionRect, fitRect); var left = position.left + margin.left; @@ -418,7 +427,7 @@ * and/or `max-width`. */ constrain: function() { - if (this.horizontalAlign || this.verticalAlign) { + if (this.__shouldPosition) { return; } this._discoverInfo(); @@ -474,7 +483,7 @@ * `position:fixed`. */ center: function() { - if (this.horizontalAlign || this.verticalAlign) { + if (this.__shouldPosition) { return; } this._discoverInfo(); @@ -522,14 +531,14 @@ return target.getBoundingClientRect(); }, - __getCroppedArea: function(position, size, fitRect) { + __getOffscreenArea: function(position, size, fitRect) { var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom - (position.top + size.height)); var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.right - (position.left + size.width)); return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * size.height; }, - __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) { + __getPosition: function(hAlign, vAlign, size, sizeNoMargins, positionRect, fitRect) { // All the possible configurations. // Ordered as top-left, top-right, bottom-left, bottom-right. var positions = [{ @@ -575,23 +584,53 @@ vAlign = vAlign === 'auto' ? null : vAlign; hAlign = hAlign === 'auto' ? null : hAlign; + if (!hAlign || hAlign === 'center') { + positions.push({ + verticalAlign: 'top', + horizontalAlign: 'center', + top: positionRect.top + this.verticalOffset + (this.noOverlap ? positionRect.height : 0), + left: positionRect.left - sizeNoMargins.width / 2 + positionRect.width / 2 + this.horizontalOffset + }); + positions.push({ + verticalAlign: 'bottom', + horizontalAlign: 'center', + top: positionRect.bottom - size.height - this.verticalOffset - (this.noOverlap ? positionRect.height : 0), + left: positionRect.left - sizeNoMargins.width / 2 + positionRect.width / 2 + this.horizontalOffset + }); + } + + if (!vAlign || vAlign === 'middle') { + positions.push({ + verticalAlign: 'middle', + horizontalAlign: 'left', + top: positionRect.top - sizeNoMargins.height / 2 + positionRect.height / 2 + this.verticalOffset, + left: positionRect.left + this.horizontalOffset + (this.noOverlap ? positionRect.width : 0) + }); + positions.push({ + verticalAlign: 'middle', + horizontalAlign: 'right', + top: positionRect.top - sizeNoMargins.height / 2 + positionRect.height / 2 + this.verticalOffset, + left: positionRect.right - size.width - this.horizontalOffset - (this.noOverlap ? positionRect.width : 0) + }); + } + var position; for (var i = 0; i < positions.length; i++) { - var pos = positions[i]; + var candidate = positions[i]; + var vAlignOk = candidate.verticalAlign === vAlign; + var hAlignOk = candidate.horizontalAlign === hAlign; // If both vAlign and hAlign are defined, return exact match. // For dynamicAlign and noOverlap we'll have more than one candidate, so - // we'll have to check the croppedArea to make the best choice. - if (!this.dynamicAlign && !this.noOverlap && - pos.verticalAlign === vAlign && pos.horizontalAlign === hAlign) { - position = pos; + // we'll have to check the offscreenArea to make the best choice. + if (!this.dynamicAlign && !this.noOverlap && vAlignOk && hAlignOk) { + position = candidate; break; } // Align is ok if alignment preferences are respected. If no preferences, // it is considered ok. - var alignOk = (!vAlign || pos.verticalAlign === vAlign) && - (!hAlign || pos.horizontalAlign === hAlign); + var alignOk = (!vAlign || vAlignOk) && (!hAlign || hAlignOk); // Filter out elements that don't match the alignment (if defined). // With dynamicAlign, we need to consider all the positions to find the @@ -600,23 +639,25 @@ continue; } - position = position || pos; - pos.croppedArea = this.__getCroppedArea(pos, size, fitRect); - var diff = pos.croppedArea - position.croppedArea; - // Check which crops less. If it crops equally, check if align is ok. - if (diff < 0 || (diff === 0 && alignOk)) { - position = pos; - } + candidate.offscreenArea = this.__getOffscreenArea(candidate, size, fitRect); // If not cropped and respects the align requirements, keep it. // This allows to prefer positions overlapping horizontally over the // ones overlapping vertically. - if (position.croppedArea === 0 && alignOk) { + if (candidate.offscreenArea === 0 && alignOk) { + position = candidate; break; } + position = position || candidate; + var diff = candidate.offscreenArea - position.offscreenArea; + // Check which crops less. If it crops equally, check if at least one + // align setting is ok. + if (diff < 0 || (diff === 0 && (vAlignOk || hAlignOk))) { + position = candidate; + } } return position; } }; - + \ No newline at end of file diff --git a/test/iron-fit-behavior.html b/test/iron-fit-behavior.html index 7dcc15d..8dde82b 100644 --- a/test/iron-fit-behavior.html +++ b/test/iron-fit-behavior.html @@ -691,6 +691,102 @@ }); }); + suite('when verticalAlign is middle', function() { + test('element is aligned to the positionTarget middle', function() { + el.verticalAlign = 'middle'; + el.refit(); + var rect = el.getBoundingClientRect(); + assert.equal(rect.top, parentRect.top + (parentRect.height - rect.height) / 2, 'top ok'); + assert.equal(rect.height, elRect.height, 'no cropping'); + }); + + test('element is aligned to the positionTarget top without overlapping it', function() { + // Allow enough space on the parent's bottom & right. + el.verticalAlign = 'middle'; + el.noOverlap = true; + el.refit(); + var rect = el.getBoundingClientRect(); + assert.isFalse(intersects(rect, parentRect), 'no overlap'); + assert.equal(rect.height, elRect.height, 'no cropping'); + }); + + test('element margin is considered as offset', function() { + el.verticalAlign = 'middle'; + el.style.marginTop = '10px'; + el.refit(); + var rect = el.getBoundingClientRect(); + assert.equal(rect.top, parentRect.top + (parentRect.height - rect.height) / 2 + 10, 'top ok'); + assert.equal(rect.height, elRect.height, 'no cropping'); + + el.style.marginTop = '-10px'; + el.refit(); + rect = el.getBoundingClientRect(); + assert.equal(rect.top, parentRect.top + (parentRect.height - rect.height) / 2 - 10, 'top ok'); + assert.equal(rect.height, elRect.height, 'no cropping'); + }); + + test('verticalOffset is applied', function() { + el.verticalAlign = 'middle'; + el.verticalOffset = 10; + el.refit(); + var rect = el.getBoundingClientRect(); + assert.equal(rect.top, parentRect.top + (parentRect.height - rect.height) / 2 + 10, 'top ok'); + assert.equal(rect.height, elRect.height, 'no cropping'); + }); + + test('element is kept in viewport', function() { + el.verticalAlign = 'middle'; + // Make it go out of screen + el.verticalOffset = -1000; + el.refit(); + var rect = el.getBoundingClientRect(); + assert.equal(rect.top, 0, 'top in viewport'); + assert.isTrue(rect.height < elRect.height, 'reduced size'); + }); + + test('negative verticalOffset does not crop element', function() { + // Push to the bottom of the screen. + parent.style.top = (window.innerHeight - 50) +'px'; + el.verticalAlign = 'middle'; + el.verticalOffset = -10; + el.refit(); + var rect = el.getBoundingClientRect(); + assert.equal(rect.top, window.innerHeight - 35, 'top ok'); + assert.equal(rect.bottom, window.innerHeight, 'bottom ok'); + }); + + test('max-height is updated', function() { + parent.style.top = '-50px'; + el.verticalAlign = 'middle'; + el.refit(); + var rect = el.getBoundingClientRect(); + assert.equal(rect.top, 0, 'top ok'); + assert.isBelow(rect.height, elRect.height, 'height ok'); + }); + + test('min-height is preserved: element is displayed even if partially', function() { + parent.style.top = '-50px'; + el.verticalAlign = 'middle'; + el.style.minHeight = elRect.height + 'px'; + el.refit(); + var rect = el.getBoundingClientRect(); + assert.equal(rect.top, 0, 'top ok'); + assert.equal(rect.height, elRect.height, 'min-height ok'); + assert.isTrue(intersects(rect, fitRect), 'partially visible'); + }); + + test('dynamicAlign will prefer bottom align if it minimizes the cropping', function() { + parent.style.top = '-50px'; + parentRect = parent.getBoundingClientRect(); + el.verticalAlign = 'middle'; + el.dynamicAlign = true; + el.refit(); + var rect = el.getBoundingClientRect(); + assert.equal(rect.bottom, parentRect.bottom, 'bottom ok'); + assert.equal(rect.height, elRect.height, 'no cropping'); + }); + }); + suite('when verticalAlign is auto', function() { test('element is aligned to the positionTarget top', function() { el.verticalAlign = 'auto'; @@ -926,6 +1022,102 @@ }); + suite('when horizontalAlign is center', function() { + test('element is aligned to the positionTarget center', function() { + el.horizontalAlign = 'center'; + el.refit(); + var rect = el.getBoundingClientRect(); + assert.equal(rect.left, parentRect.left + (parentRect.width - rect.width) / 2, 'left ok'); + assert.equal(rect.width, elRect.width, 'no cropping'); + }); + + test('element is aligned to the positionTarget left without overlapping it', function() { + el.horizontalAlign = 'center'; + el.noOverlap = true; + el.refit(); + var rect = el.getBoundingClientRect(); + assert.isFalse(intersects(rect, parentRect), 'no overlap'); + assert.equal(rect.width, elRect.width, 'no cropping'); + }); + + test('element margin is considered as offset', function() { + el.horizontalAlign = 'center'; + el.style.marginLeft = '10px'; + el.refit(); + var rect = el.getBoundingClientRect(); + assert.equal(rect.left, parentRect.left + (parentRect.width - rect.width) / 2 + 10, 'left ok'); + assert.equal(rect.width, elRect.width, 'no cropping'); + + el.style.marginLeft = '-10px'; + el.refit(); + rect = el.getBoundingClientRect(); + assert.equal(rect.left, parentRect.left + (parentRect.width - rect.width) / 2 - 10, 'left ok'); + assert.equal(rect.width, elRect.width, 'no cropping'); + }); + + test('horizontalOffset is applied', function() { + el.horizontalAlign = 'center'; + el.horizontalOffset = 10; + el.refit(); + var rect = el.getBoundingClientRect(); + assert.equal(rect.left, parentRect.left + (parentRect.width - rect.width) / 2 + 10, 'left ok'); + assert.equal(rect.width, elRect.width, 'no cropping'); + }); + + test('element is kept in viewport', function() { + el.horizontalAlign = 'center'; + // Make it go out of screen. + el.horizontalOffset = -1000; + el.refit(); + var rect = el.getBoundingClientRect(); + assert.equal(rect.left, 0, 'left in viewport'); + assert.isTrue(rect.width < elRect.width, 'reduced size'); + }); + + test('negative horizontalOffset does not crop element', function() { + // Push to the bottom of the screen. + parent.style.left = (window.innerWidth - 50) +'px'; + el.horizontalAlign = 'center'; + el.horizontalOffset = -10; + el.refit(); + var rect = el.getBoundingClientRect(); + assert.equal(rect.left, window.innerWidth - 35, 'left ok'); + assert.equal(rect.right, window.innerWidth, 'right ok'); + }); + + test('element max-width is updated', function() { + parent.style.left = '-50px'; + el.horizontalAlign = 'center'; + el.refit(); + var rect = el.getBoundingClientRect(); + assert.equal(rect.left, 0, 'left ok'); + assert.isBelow(rect.width, elRect.width, 'width ok'); + }); + + test('min-width is preserved: element is displayed even if partially', function() { + parent.style.left = '-50px'; + el.style.minWidth = elRect.width + 'px'; + el.horizontalAlign = 'center'; + el.refit(); + var rect = el.getBoundingClientRect(); + assert.equal(rect.left, 0, 'left ok'); + assert.equal(rect.width, elRect.width, 'min-width ok'); + assert.isTrue(intersects(rect, fitRect), 'partially visible'); + }); + + test('dynamicAlign will prefer right align if it minimizes the cropping', function() { + parent.style.left = '-50px'; + parentRect = parent.getBoundingClientRect(); + el.horizontalAlign = 'center'; + el.dynamicAlign = true; + el.refit(); + var rect = el.getBoundingClientRect(); + assert.equal(rect.right, parentRect.right, 'right ok'); + assert.equal(rect.height, elRect.height, 'no cropping'); + }); + + }); + suite('when horizontalAlign is auto', function() { test('element is aligned to the positionTarget left', function() { el.horizontalAlign = 'auto';