@angular/cdk
Version:
Angular Material Component Development Kit
1,165 lines • 159 kB
JavaScript
/**
* @fileoverview added by tsickle
* Generated from: src/cdk/overlay/position/flexible-connected-position-strategy.ts
* @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import { ElementRef } from '@angular/core';
import { ConnectedOverlayPositionChange, validateHorizontalPosition, validateVerticalPosition, } from './connected-position';
import { Subscription, Subject } from 'rxjs';
import { isElementScrolledOutsideView, isElementClippedByScrolling } from './scroll-clip';
import { coerceCssPixelValue, coerceArray } from '@angular/cdk/coercion';
// TODO: refactor clipping detection into a separate thing (part of scrolling module)
// TODO: doesn't handle both flexible width and height when it has to scroll along both axis.
/**
* Class to be added to the overlay bounding box.
* @type {?}
*/
const boundingBoxClass = 'cdk-overlay-connected-position-bounding-box';
/**
* A strategy for positioning overlays. Using this strategy, an overlay is given an
* implicit position relative some origin element. The relative position is defined in terms of
* a point on the origin element that is connected to a point on the overlay element. For example,
* a basic dropdown is connecting the bottom-left corner of the origin to the top-left corner
* of the overlay.
*/
export class FlexibleConnectedPositionStrategy {
/**
* @param {?} connectedTo
* @param {?} _viewportRuler
* @param {?} _document
* @param {?} _platform
* @param {?} _overlayContainer
*/
constructor(connectedTo, _viewportRuler, _document, _platform, _overlayContainer) {
this._viewportRuler = _viewportRuler;
this._document = _document;
this._platform = _platform;
this._overlayContainer = _overlayContainer;
/**
* Last size used for the bounding box. Used to avoid resizing the overlay after open.
*/
this._lastBoundingBoxSize = { width: 0, height: 0 };
/**
* Whether the overlay was pushed in a previous positioning.
*/
this._isPushed = false;
/**
* Whether the overlay can be pushed on-screen on the initial open.
*/
this._canPush = true;
/**
* Whether the overlay can grow via flexible width/height after the initial open.
*/
this._growAfterOpen = false;
/**
* Whether the overlay's width and height can be constrained to fit within the viewport.
*/
this._hasFlexibleDimensions = true;
/**
* Whether the overlay position is locked.
*/
this._positionLocked = false;
/**
* Amount of space that must be maintained between the overlay and the edge of the viewport.
*/
this._viewportMargin = 0;
/**
* The Scrollable containers used to check scrollable view properties on position change.
*/
this._scrollables = [];
/**
* Ordered list of preferred positions, from most to least desirable.
*/
this._preferredPositions = [];
/**
* Subject that emits whenever the position changes.
*/
this._positionChanges = new Subject();
/**
* Subscription to viewport size changes.
*/
this._resizeSubscription = Subscription.EMPTY;
/**
* Default offset for the overlay along the x axis.
*/
this._offsetX = 0;
/**
* Default offset for the overlay along the y axis.
*/
this._offsetY = 0;
/**
* Keeps track of the CSS classes that the position strategy has applied on the overlay panel.
*/
this._appliedPanelClasses = [];
/**
* Observable sequence of position changes.
*/
this.positionChanges = this._positionChanges.asObservable();
this.setOrigin(connectedTo);
}
/**
* Ordered list of preferred positions, from most to least desirable.
* @return {?}
*/
get positions() {
return this._preferredPositions;
}
/**
* Attaches this position strategy to an overlay.
* @param {?} overlayRef
* @return {?}
*/
attach(overlayRef) {
if (this._overlayRef && overlayRef !== this._overlayRef) {
throw Error('This position strategy is already attached to an overlay');
}
this._validatePositions();
overlayRef.hostElement.classList.add(boundingBoxClass);
this._overlayRef = overlayRef;
this._boundingBox = overlayRef.hostElement;
this._pane = overlayRef.overlayElement;
this._isDisposed = false;
this._isInitialRender = true;
this._lastPosition = null;
this._resizeSubscription.unsubscribe();
this._resizeSubscription = this._viewportRuler.change().subscribe((/**
* @return {?}
*/
() => {
// When the window is resized, we want to trigger the next reposition as if it
// was an initial render, in order for the strategy to pick a new optimal position,
// otherwise position locking will cause it to stay at the old one.
this._isInitialRender = true;
this.apply();
}));
}
/**
* Updates the position of the overlay element, using whichever preferred position relative
* to the origin best fits on-screen.
*
* The selection of a position goes as follows:
* - If any positions fit completely within the viewport as-is,
* choose the first position that does so.
* - If flexible dimensions are enabled and at least one satifies the given minimum width/height,
* choose the position with the greatest available size modified by the positions' weight.
* - If pushing is enabled, take the position that went off-screen the least and push it
* on-screen.
* - If none of the previous criteria were met, use the position that goes off-screen the least.
* \@docs-private
* @return {?}
*/
apply() {
// We shouldn't do anything if the strategy was disposed or we're on the server.
if (this._isDisposed || !this._platform.isBrowser) {
return;
}
// If the position has been applied already (e.g. when the overlay was opened) and the
// consumer opted into locking in the position, re-use the old position, in order to
// prevent the overlay from jumping around.
if (!this._isInitialRender && this._positionLocked && this._lastPosition) {
this.reapplyLastPosition();
return;
}
this._clearPanelClasses();
this._resetOverlayElementStyles();
this._resetBoundingBoxStyles();
// We need the bounding rects for the origin and the overlay to determine how to position
// the overlay relative to the origin.
// We use the viewport rect to determine whether a position would go off-screen.
this._viewportRect = this._getNarrowedViewportRect();
this._originRect = this._getOriginRect();
this._overlayRect = this._pane.getBoundingClientRect();
/** @type {?} */
const originRect = this._originRect;
/** @type {?} */
const overlayRect = this._overlayRect;
/** @type {?} */
const viewportRect = this._viewportRect;
// Positions where the overlay will fit with flexible dimensions.
/** @type {?} */
const flexibleFits = [];
// Fallback if none of the preferred positions fit within the viewport.
/** @type {?} */
let fallback;
// Go through each of the preferred positions looking for a good fit.
// If a good fit is found, it will be applied immediately.
for (let pos of this._preferredPositions) {
// Get the exact (x, y) coordinate for the point-of-origin on the origin element.
/** @type {?} */
let originPoint = this._getOriginPoint(originRect, pos);
// From that point-of-origin, get the exact (x, y) coordinate for the top-left corner of the
// overlay in this position. We use the top-left corner for calculations and later translate
// this into an appropriate (top, left, bottom, right) style.
/** @type {?} */
let overlayPoint = this._getOverlayPoint(originPoint, overlayRect, pos);
// Calculate how well the overlay would fit into the viewport with this point.
/** @type {?} */
let overlayFit = this._getOverlayFit(overlayPoint, overlayRect, viewportRect, pos);
// If the overlay, without any further work, fits into the viewport, use this position.
if (overlayFit.isCompletelyWithinViewport) {
this._isPushed = false;
this._applyPosition(pos, originPoint);
return;
}
// If the overlay has flexible dimensions, we can use this position
// so long as there's enough space for the minimum dimensions.
if (this._canFitWithFlexibleDimensions(overlayFit, overlayPoint, viewportRect)) {
// Save positions where the overlay will fit with flexible dimensions. We will use these
// if none of the positions fit *without* flexible dimensions.
flexibleFits.push({
position: pos,
origin: originPoint,
overlayRect,
boundingBoxRect: this._calculateBoundingBoxRect(originPoint, pos)
});
continue;
}
// If the current preferred position does not fit on the screen, remember the position
// if it has more visible area on-screen than we've seen and move onto the next preferred
// position.
if (!fallback || fallback.overlayFit.visibleArea < overlayFit.visibleArea) {
fallback = { overlayFit, overlayPoint, originPoint, position: pos, overlayRect };
}
}
// If there are any positions where the overlay would fit with flexible dimensions, choose the
// one that has the greatest area available modified by the position's weight
if (flexibleFits.length) {
/** @type {?} */
let bestFit = null;
/** @type {?} */
let bestScore = -1;
for (const fit of flexibleFits) {
/** @type {?} */
const score = fit.boundingBoxRect.width * fit.boundingBoxRect.height * (fit.position.weight || 1);
if (score > bestScore) {
bestScore = score;
bestFit = fit;
}
}
this._isPushed = false;
this._applyPosition((/** @type {?} */ (bestFit)).position, (/** @type {?} */ (bestFit)).origin);
return;
}
// When none of the preferred positions fit within the viewport, take the position
// that went off-screen the least and attempt to push it on-screen.
if (this._canPush) {
// TODO(jelbourn): after pushing, the opening "direction" of the overlay might not make sense.
this._isPushed = true;
this._applyPosition((/** @type {?} */ (fallback)).position, (/** @type {?} */ (fallback)).originPoint);
return;
}
// All options for getting the overlay within the viewport have been exhausted, so go with the
// position that went off-screen the least.
this._applyPosition((/** @type {?} */ (fallback)).position, (/** @type {?} */ (fallback)).originPoint);
}
/**
* @return {?}
*/
detach() {
this._clearPanelClasses();
this._lastPosition = null;
this._previousPushAmount = null;
this._resizeSubscription.unsubscribe();
}
/**
* Cleanup after the element gets destroyed.
* @return {?}
*/
dispose() {
if (this._isDisposed) {
return;
}
// We can't use `_resetBoundingBoxStyles` here, because it resets
// some properties to zero, rather than removing them.
if (this._boundingBox) {
extendStyles(this._boundingBox.style, (/** @type {?} */ ({
top: '',
left: '',
right: '',
bottom: '',
height: '',
width: '',
alignItems: '',
justifyContent: '',
})));
}
if (this._pane) {
this._resetOverlayElementStyles();
}
if (this._overlayRef) {
this._overlayRef.hostElement.classList.remove(boundingBoxClass);
}
this.detach();
this._positionChanges.complete();
this._overlayRef = this._boundingBox = (/** @type {?} */ (null));
this._isDisposed = true;
}
/**
* This re-aligns the overlay element with the trigger in its last calculated position,
* even if a position higher in the "preferred positions" list would now fit. This
* allows one to re-align the panel without changing the orientation of the panel.
* @return {?}
*/
reapplyLastPosition() {
if (!this._isDisposed && (!this._platform || this._platform.isBrowser)) {
this._originRect = this._getOriginRect();
this._overlayRect = this._pane.getBoundingClientRect();
this._viewportRect = this._getNarrowedViewportRect();
/** @type {?} */
const lastPosition = this._lastPosition || this._preferredPositions[0];
/** @type {?} */
const originPoint = this._getOriginPoint(this._originRect, lastPosition);
this._applyPosition(lastPosition, originPoint);
}
}
/**
* Sets the list of Scrollable containers that host the origin element so that
* on reposition we can evaluate if it or the overlay has been clipped or outside view. Every
* Scrollable must be an ancestor element of the strategy's origin element.
* @template THIS
* @this {THIS}
* @param {?} scrollables
* @return {THIS}
*/
withScrollableContainers(scrollables) {
(/** @type {?} */ (this))._scrollables = scrollables;
return (/** @type {?} */ (this));
}
/**
* Adds new preferred positions.
* @template THIS
* @this {THIS}
* @param {?} positions List of positions options for this overlay.
* @return {THIS}
*/
withPositions(positions) {
(/** @type {?} */ (this))._preferredPositions = positions;
// If the last calculated position object isn't part of the positions anymore, clear
// it in order to avoid it being picked up if the consumer tries to re-apply.
if (positions.indexOf((/** @type {?} */ ((/** @type {?} */ (this))._lastPosition))) === -1) {
(/** @type {?} */ (this))._lastPosition = null;
}
(/** @type {?} */ (this))._validatePositions();
return (/** @type {?} */ (this));
}
/**
* Sets a minimum distance the overlay may be positioned to the edge of the viewport.
* @template THIS
* @this {THIS}
* @param {?} margin Required margin between the overlay and the viewport edge in pixels.
* @return {THIS}
*/
withViewportMargin(margin) {
(/** @type {?} */ (this))._viewportMargin = margin;
return (/** @type {?} */ (this));
}
/**
* Sets whether the overlay's width and height can be constrained to fit within the viewport.
* @template THIS
* @this {THIS}
* @param {?=} flexibleDimensions
* @return {THIS}
*/
withFlexibleDimensions(flexibleDimensions = true) {
(/** @type {?} */ (this))._hasFlexibleDimensions = flexibleDimensions;
return (/** @type {?} */ (this));
}
/**
* Sets whether the overlay can grow after the initial open via flexible width/height.
* @template THIS
* @this {THIS}
* @param {?=} growAfterOpen
* @return {THIS}
*/
withGrowAfterOpen(growAfterOpen = true) {
(/** @type {?} */ (this))._growAfterOpen = growAfterOpen;
return (/** @type {?} */ (this));
}
/**
* Sets whether the overlay can be pushed on-screen if none of the provided positions fit.
* @template THIS
* @this {THIS}
* @param {?=} canPush
* @return {THIS}
*/
withPush(canPush = true) {
(/** @type {?} */ (this))._canPush = canPush;
return (/** @type {?} */ (this));
}
/**
* Sets whether the overlay's position should be locked in after it is positioned
* initially. When an overlay is locked in, it won't attempt to reposition itself
* when the position is re-applied (e.g. when the user scrolls away).
* @template THIS
* @this {THIS}
* @param {?=} isLocked Whether the overlay should locked in.
* @return {THIS}
*/
withLockedPosition(isLocked = true) {
(/** @type {?} */ (this))._positionLocked = isLocked;
return (/** @type {?} */ (this));
}
/**
* Sets the origin, relative to which to position the overlay.
* Using an element origin is useful for building components that need to be positioned
* relatively to a trigger (e.g. dropdown menus or tooltips), whereas using a point can be
* used for cases like contextual menus which open relative to the user's pointer.
* @template THIS
* @this {THIS}
* @param {?} origin Reference to the new origin.
* @return {THIS}
*/
setOrigin(origin) {
(/** @type {?} */ (this))._origin = origin;
return (/** @type {?} */ (this));
}
/**
* Sets the default offset for the overlay's connection point on the x-axis.
* @template THIS
* @this {THIS}
* @param {?} offset New offset in the X axis.
* @return {THIS}
*/
withDefaultOffsetX(offset) {
(/** @type {?} */ (this))._offsetX = offset;
return (/** @type {?} */ (this));
}
/**
* Sets the default offset for the overlay's connection point on the y-axis.
* @template THIS
* @this {THIS}
* @param {?} offset New offset in the Y axis.
* @return {THIS}
*/
withDefaultOffsetY(offset) {
(/** @type {?} */ (this))._offsetY = offset;
return (/** @type {?} */ (this));
}
/**
* Configures that the position strategy should set a `transform-origin` on some elements
* inside the overlay, depending on the current position that is being applied. This is
* useful for the cases where the origin of an animation can change depending on the
* alignment of the overlay.
* @template THIS
* @this {THIS}
* @param {?} selector CSS selector that will be used to find the target
* elements onto which to set the transform origin.
* @return {THIS}
*/
withTransformOriginOn(selector) {
(/** @type {?} */ (this))._transformOriginSelector = selector;
return (/** @type {?} */ (this));
}
/**
* Gets the (x, y) coordinate of a connection point on the origin based on a relative position.
* @private
* @param {?} originRect
* @param {?} pos
* @return {?}
*/
_getOriginPoint(originRect, pos) {
/** @type {?} */
let x;
if (pos.originX == 'center') {
// Note: when centering we should always use the `left`
// offset, otherwise the position will be wrong in RTL.
x = originRect.left + (originRect.width / 2);
}
else {
/** @type {?} */
const startX = this._isRtl() ? originRect.right : originRect.left;
/** @type {?} */
const endX = this._isRtl() ? originRect.left : originRect.right;
x = pos.originX == 'start' ? startX : endX;
}
/** @type {?} */
let y;
if (pos.originY == 'center') {
y = originRect.top + (originRect.height / 2);
}
else {
y = pos.originY == 'top' ? originRect.top : originRect.bottom;
}
return { x, y };
}
/**
* Gets the (x, y) coordinate of the top-left corner of the overlay given a given position and
* origin point to which the overlay should be connected.
* @private
* @param {?} originPoint
* @param {?} overlayRect
* @param {?} pos
* @return {?}
*/
_getOverlayPoint(originPoint, overlayRect, pos) {
// Calculate the (overlayStartX, overlayStartY), the start of the
// potential overlay position relative to the origin point.
/** @type {?} */
let overlayStartX;
if (pos.overlayX == 'center') {
overlayStartX = -overlayRect.width / 2;
}
else if (pos.overlayX === 'start') {
overlayStartX = this._isRtl() ? -overlayRect.width : 0;
}
else {
overlayStartX = this._isRtl() ? 0 : -overlayRect.width;
}
/** @type {?} */
let overlayStartY;
if (pos.overlayY == 'center') {
overlayStartY = -overlayRect.height / 2;
}
else {
overlayStartY = pos.overlayY == 'top' ? 0 : -overlayRect.height;
}
// The (x, y) coordinates of the overlay.
return {
x: originPoint.x + overlayStartX,
y: originPoint.y + overlayStartY,
};
}
/**
* Gets how well an overlay at the given point will fit within the viewport.
* @private
* @param {?} point
* @param {?} overlay
* @param {?} viewport
* @param {?} position
* @return {?}
*/
_getOverlayFit(point, overlay, viewport, position) {
let { x, y } = point;
/** @type {?} */
let offsetX = this._getOffset(position, 'x');
/** @type {?} */
let offsetY = this._getOffset(position, 'y');
// Account for the offsets since they could push the overlay out of the viewport.
if (offsetX) {
x += offsetX;
}
if (offsetY) {
y += offsetY;
}
// How much the overlay would overflow at this position, on each side.
/** @type {?} */
let leftOverflow = 0 - x;
/** @type {?} */
let rightOverflow = (x + overlay.width) - viewport.width;
/** @type {?} */
let topOverflow = 0 - y;
/** @type {?} */
let bottomOverflow = (y + overlay.height) - viewport.height;
// Visible parts of the element on each axis.
/** @type {?} */
let visibleWidth = this._subtractOverflows(overlay.width, leftOverflow, rightOverflow);
/** @type {?} */
let visibleHeight = this._subtractOverflows(overlay.height, topOverflow, bottomOverflow);
/** @type {?} */
let visibleArea = visibleWidth * visibleHeight;
return {
visibleArea,
isCompletelyWithinViewport: (overlay.width * overlay.height) === visibleArea,
fitsInViewportVertically: visibleHeight === overlay.height,
fitsInViewportHorizontally: visibleWidth == overlay.width,
};
}
/**
* Whether the overlay can fit within the viewport when it may resize either its width or height.
* @private
* @param {?} fit How well the overlay fits in the viewport at some position.
* @param {?} point The (x, y) coordinates of the overlat at some position.
* @param {?} viewport The geometry of the viewport.
* @return {?}
*/
_canFitWithFlexibleDimensions(fit, point, viewport) {
if (this._hasFlexibleDimensions) {
/** @type {?} */
const availableHeight = viewport.bottom - point.y;
/** @type {?} */
const availableWidth = viewport.right - point.x;
/** @type {?} */
const minHeight = this._overlayRef.getConfig().minHeight;
/** @type {?} */
const minWidth = this._overlayRef.getConfig().minWidth;
/** @type {?} */
const verticalFit = fit.fitsInViewportVertically ||
(minHeight != null && minHeight <= availableHeight);
/** @type {?} */
const horizontalFit = fit.fitsInViewportHorizontally ||
(minWidth != null && minWidth <= availableWidth);
return verticalFit && horizontalFit;
}
return false;
}
/**
* Gets the point at which the overlay can be "pushed" on-screen. If the overlay is larger than
* the viewport, the top-left corner will be pushed on-screen (with overflow occuring on the
* right and bottom).
*
* @private
* @param {?} start Starting point from which the overlay is pushed.
* @param {?} overlay Dimensions of the overlay.
* @param {?} scrollPosition Current viewport scroll position.
* @return {?} The point at which to position the overlay after pushing. This is effectively a new
* originPoint.
*/
_pushOverlayOnScreen(start, overlay, scrollPosition) {
// If the position is locked and we've pushed the overlay already, reuse the previous push
// amount, rather than pushing it again. If we were to continue pushing, the element would
// remain in the viewport, which goes against the expectations when position locking is enabled.
if (this._previousPushAmount && this._positionLocked) {
return {
x: start.x + this._previousPushAmount.x,
y: start.y + this._previousPushAmount.y
};
}
/** @type {?} */
const viewport = this._viewportRect;
// Determine how much the overlay goes outside the viewport on each
// side, which we'll use to decide which direction to push it.
/** @type {?} */
const overflowRight = Math.max(start.x + overlay.width - viewport.right, 0);
/** @type {?} */
const overflowBottom = Math.max(start.y + overlay.height - viewport.bottom, 0);
/** @type {?} */
const overflowTop = Math.max(viewport.top - scrollPosition.top - start.y, 0);
/** @type {?} */
const overflowLeft = Math.max(viewport.left - scrollPosition.left - start.x, 0);
// Amount by which to push the overlay in each axis such that it remains on-screen.
/** @type {?} */
let pushX = 0;
/** @type {?} */
let pushY = 0;
// If the overlay fits completely within the bounds of the viewport, push it from whichever
// direction is goes off-screen. Otherwise, push the top-left corner such that its in the
// viewport and allow for the trailing end of the overlay to go out of bounds.
if (overlay.width <= viewport.width) {
pushX = overflowLeft || -overflowRight;
}
else {
pushX = start.x < this._viewportMargin ? (viewport.left - scrollPosition.left) - start.x : 0;
}
if (overlay.height <= viewport.height) {
pushY = overflowTop || -overflowBottom;
}
else {
pushY = start.y < this._viewportMargin ? (viewport.top - scrollPosition.top) - start.y : 0;
}
this._previousPushAmount = { x: pushX, y: pushY };
return {
x: start.x + pushX,
y: start.y + pushY,
};
}
/**
* Applies a computed position to the overlay and emits a position change.
* @private
* @param {?} position The position preference
* @param {?} originPoint The point on the origin element where the overlay is connected.
* @return {?}
*/
_applyPosition(position, originPoint) {
this._setTransformOrigin(position);
this._setOverlayElementStyles(originPoint, position);
this._setBoundingBoxStyles(originPoint, position);
if (position.panelClass) {
this._addPanelClasses(position.panelClass);
}
// Save the last connected position in case the position needs to be re-calculated.
this._lastPosition = position;
// Notify that the position has been changed along with its change properties.
// We only emit if we've got any subscriptions, because the scroll visibility
// calculcations can be somewhat expensive.
if (this._positionChanges.observers.length) {
/** @type {?} */
const scrollableViewProperties = this._getScrollVisibility();
/** @type {?} */
const changeEvent = new ConnectedOverlayPositionChange(position, scrollableViewProperties);
this._positionChanges.next(changeEvent);
}
this._isInitialRender = false;
}
/**
* Sets the transform origin based on the configured selector and the passed-in position.
* @private
* @param {?} position
* @return {?}
*/
_setTransformOrigin(position) {
if (!this._transformOriginSelector) {
return;
}
/** @type {?} */
const elements = (/** @type {?} */ (this._boundingBox)).querySelectorAll(this._transformOriginSelector);
/** @type {?} */
let xOrigin;
/** @type {?} */
let yOrigin = position.overlayY;
if (position.overlayX === 'center') {
xOrigin = 'center';
}
else if (this._isRtl()) {
xOrigin = position.overlayX === 'start' ? 'right' : 'left';
}
else {
xOrigin = position.overlayX === 'start' ? 'left' : 'right';
}
for (let i = 0; i < elements.length; i++) {
elements[i].style.transformOrigin = `${xOrigin} ${yOrigin}`;
}
}
/**
* Gets the position and size of the overlay's sizing container.
*
* This method does no measuring and applies no styles so that we can cheaply compute the
* bounds for all positions and choose the best fit based on these results.
* @private
* @param {?} origin
* @param {?} position
* @return {?}
*/
_calculateBoundingBoxRect(origin, position) {
/** @type {?} */
const viewport = this._viewportRect;
/** @type {?} */
const isRtl = this._isRtl();
/** @type {?} */
let height;
/** @type {?} */
let top;
/** @type {?} */
let bottom;
if (position.overlayY === 'top') {
// Overlay is opening "downward" and thus is bound by the bottom viewport edge.
top = origin.y;
height = viewport.height - top + this._viewportMargin;
}
else if (position.overlayY === 'bottom') {
// Overlay is opening "upward" and thus is bound by the top viewport edge. We need to add
// the viewport margin back in, because the viewport rect is narrowed down to remove the
// margin, whereas the `origin` position is calculated based on its `ClientRect`.
bottom = viewport.height - origin.y + this._viewportMargin * 2;
height = viewport.height - bottom + this._viewportMargin;
}
else {
// If neither top nor bottom, it means that the overlay is vertically centered on the
// origin point. Note that we want the position relative to the viewport, rather than
// the page, which is why we don't use something like `viewport.bottom - origin.y` and
// `origin.y - viewport.top`.
/** @type {?} */
const smallestDistanceToViewportEdge = Math.min(viewport.bottom - origin.y + viewport.top, origin.y);
/** @type {?} */
const previousHeight = this._lastBoundingBoxSize.height;
height = smallestDistanceToViewportEdge * 2;
top = origin.y - smallestDistanceToViewportEdge;
if (height > previousHeight && !this._isInitialRender && !this._growAfterOpen) {
top = origin.y - (previousHeight / 2);
}
}
// The overlay is opening 'right-ward' (the content flows to the right).
/** @type {?} */
const isBoundedByRightViewportEdge = (position.overlayX === 'start' && !isRtl) ||
(position.overlayX === 'end' && isRtl);
// The overlay is opening 'left-ward' (the content flows to the left).
/** @type {?} */
const isBoundedByLeftViewportEdge = (position.overlayX === 'end' && !isRtl) ||
(position.overlayX === 'start' && isRtl);
/** @type {?} */
let width;
/** @type {?} */
let left;
/** @type {?} */
let right;
if (isBoundedByLeftViewportEdge) {
right = viewport.width - origin.x + this._viewportMargin;
width = origin.x - this._viewportMargin;
}
else if (isBoundedByRightViewportEdge) {
left = origin.x;
width = viewport.right - origin.x;
}
else {
// If neither start nor end, it means that the overlay is horizontally centered on the
// origin point. Note that we want the position relative to the viewport, rather than
// the page, which is why we don't use something like `viewport.right - origin.x` and
// `origin.x - viewport.left`.
/** @type {?} */
const smallestDistanceToViewportEdge = Math.min(viewport.right - origin.x + viewport.left, origin.x);
/** @type {?} */
const previousWidth = this._lastBoundingBoxSize.width;
width = smallestDistanceToViewportEdge * 2;
left = origin.x - smallestDistanceToViewportEdge;
if (width > previousWidth && !this._isInitialRender && !this._growAfterOpen) {
left = origin.x - (previousWidth / 2);
}
}
return { top: (/** @type {?} */ (top)), left: (/** @type {?} */ (left)), bottom: (/** @type {?} */ (bottom)), right: (/** @type {?} */ (right)), width, height };
}
/**
* Sets the position and size of the overlay's sizing wrapper. The wrapper is positioned on the
* origin's connection point and stetches to the bounds of the viewport.
*
* @private
* @param {?} origin The point on the origin element where the overlay is connected.
* @param {?} position The position preference
* @return {?}
*/
_setBoundingBoxStyles(origin, position) {
/** @type {?} */
const boundingBoxRect = this._calculateBoundingBoxRect(origin, position);
// It's weird if the overlay *grows* while scrolling, so we take the last size into account
// when applying a new size.
if (!this._isInitialRender && !this._growAfterOpen) {
boundingBoxRect.height = Math.min(boundingBoxRect.height, this._lastBoundingBoxSize.height);
boundingBoxRect.width = Math.min(boundingBoxRect.width, this._lastBoundingBoxSize.width);
}
/** @type {?} */
const styles = (/** @type {?} */ ({}));
if (this._hasExactPosition()) {
styles.top = styles.left = '0';
styles.bottom = styles.right = styles.maxHeight = styles.maxWidth = '';
styles.width = styles.height = '100%';
}
else {
/** @type {?} */
const maxHeight = this._overlayRef.getConfig().maxHeight;
/** @type {?} */
const maxWidth = this._overlayRef.getConfig().maxWidth;
styles.height = coerceCssPixelValue(boundingBoxRect.height);
styles.top = coerceCssPixelValue(boundingBoxRect.top);
styles.bottom = coerceCssPixelValue(boundingBoxRect.bottom);
styles.width = coerceCssPixelValue(boundingBoxRect.width);
styles.left = coerceCssPixelValue(boundingBoxRect.left);
styles.right = coerceCssPixelValue(boundingBoxRect.right);
// Push the pane content towards the proper direction.
if (position.overlayX === 'center') {
styles.alignItems = 'center';
}
else {
styles.alignItems = position.overlayX === 'end' ? 'flex-end' : 'flex-start';
}
if (position.overlayY === 'center') {
styles.justifyContent = 'center';
}
else {
styles.justifyContent = position.overlayY === 'bottom' ? 'flex-end' : 'flex-start';
}
if (maxHeight) {
styles.maxHeight = coerceCssPixelValue(maxHeight);
}
if (maxWidth) {
styles.maxWidth = coerceCssPixelValue(maxWidth);
}
}
this._lastBoundingBoxSize = boundingBoxRect;
extendStyles((/** @type {?} */ (this._boundingBox)).style, styles);
}
/**
* Resets the styles for the bounding box so that a new positioning can be computed.
* @private
* @return {?}
*/
_resetBoundingBoxStyles() {
extendStyles((/** @type {?} */ (this._boundingBox)).style, (/** @type {?} */ ({
top: '0',
left: '0',
right: '0',
bottom: '0',
height: '',
width: '',
alignItems: '',
justifyContent: '',
})));
}
/**
* Resets the styles for the overlay pane so that a new positioning can be computed.
* @private
* @return {?}
*/
_resetOverlayElementStyles() {
extendStyles(this._pane.style, (/** @type {?} */ ({
top: '',
left: '',
bottom: '',
right: '',
position: '',
transform: '',
})));
}
/**
* Sets positioning styles to the overlay element.
* @private
* @param {?} originPoint
* @param {?} position
* @return {?}
*/
_setOverlayElementStyles(originPoint, position) {
/** @type {?} */
const styles = (/** @type {?} */ ({}));
/** @type {?} */
const hasExactPosition = this._hasExactPosition();
/** @type {?} */
const hasFlexibleDimensions = this._hasFlexibleDimensions;
/** @type {?} */
const config = this._overlayRef.getConfig();
if (hasExactPosition) {
/** @type {?} */
const scrollPosition = this._viewportRuler.getViewportScrollPosition();
extendStyles(styles, this._getExactOverlayY(position, originPoint, scrollPosition));
extendStyles(styles, this._getExactOverlayX(position, originPoint, scrollPosition));
}
else {
styles.position = 'static';
}
// Use a transform to apply the offsets. We do this because the `center` positions rely on
// being in the normal flex flow and setting a `top` / `left` at all will completely throw
// off the position. We also can't use margins, because they won't have an effect in some
// cases where the element doesn't have anything to "push off of". Finally, this works
// better both with flexible and non-flexible positioning.
/** @type {?} */
let transformString = '';
/** @type {?} */
let offsetX = this._getOffset(position, 'x');
/** @type {?} */
let offsetY = this._getOffset(position, 'y');
if (offsetX) {
transformString += `translateX(${offsetX}px) `;
}
if (offsetY) {
transformString += `translateY(${offsetY}px)`;
}
styles.transform = transformString.trim();
// If a maxWidth or maxHeight is specified on the overlay, we remove them. We do this because
// we need these values to both be set to "100%" for the automatic flexible sizing to work.
// The maxHeight and maxWidth are set on the boundingBox in order to enforce the constraint.
// Note that this doesn't apply when we have an exact position, in which case we do want to
// apply them because they'll be cleared from the bounding box.
if (config.maxHeight) {
if (hasExactPosition) {
styles.maxHeight = coerceCssPixelValue(config.maxHeight);
}
else if (hasFlexibleDimensions) {
styles.maxHeight = '';
}
}
if (config.maxWidth) {
if (hasExactPosition) {
styles.maxWidth = coerceCssPixelValue(config.maxWidth);
}
else if (hasFlexibleDimensions) {
styles.maxWidth = '';
}
}
extendStyles(this._pane.style, styles);
}
/**
* Gets the exact top/bottom for the overlay when not using flexible sizing or when pushing.
* @private
* @param {?} position
* @param {?} originPoint
* @param {?} scrollPosition
* @return {?}
*/
_getExactOverlayY(position, originPoint, scrollPosition) {
// Reset any existing styles. This is necessary in case the
// preferred position has changed since the last `apply`.
/** @type {?} */
let styles = (/** @type {?} */ ({ top: '', bottom: '' }));
/** @type {?} */
let overlayPoint = this._getOverlayPoint(originPoint, this._overlayRect, position);
if (this._isPushed) {
overlayPoint = this._pushOverlayOnScreen(overlayPoint, this._overlayRect, scrollPosition);
}
/** @type {?} */
let virtualKeyboardOffset = this._overlayContainer.getContainerElement().getBoundingClientRect().top;
// Normally this would be zero, however when the overlay is attached to an input (e.g. in an
// autocomplete), mobile browsers will shift everything in order to put the input in the middle
// of the screen and to make space for the virtual keyboard. We need to account for this offset,
// otherwise our positioning will be thrown off.
overlayPoint.y -= virtualKeyboardOffset;
// We want to set either `top` or `bottom` based on whether the overlay wants to appear
// above or below the origin and the direction in which the element will expand.
if (position.overlayY === 'bottom') {
// When using `bottom`, we adjust the y position such that it is the distance
// from the bottom of the viewport rather than the top.
/** @type {?} */
const documentHeight = (/** @type {?} */ (this._document.documentElement)).clientHeight;
styles.bottom = `${documentHeight - (overlayPoint.y + this._overlayRect.height)}px`;
}
else {
styles.top = coerceCssPixelValue(overlayPoint.y);
}
return styles;
}
/**
* Gets the exact left/right for the overlay when not using flexible sizing or when pushing.
* @private
* @param {?} position
* @param {?} originPoint
* @param {?} scrollPosition
* @return {?}
*/
_getExactOverlayX(position, originPoint, scrollPosition) {
// Reset any existing styles. This is necessary in case the preferred position has
// changed since the last `apply`.
/** @type {?} */
let styles = (/** @type {?} */ ({ left: '', right: '' }));
/** @type {?} */
let overlayPoint = this._getOverlayPoint(originPoint, this._overlayRect, position);
if (this._isPushed) {
overlayPoint = this._pushOverlayOnScreen(overlayPoint, this._overlayRect, scrollPosition);
}
// We want to set either `left` or `right` based on whether the overlay wants to appear "before"
// or "after" the origin, which determines the direction in which the element will expand.
// For the horizontal axis, the meaning of "before" and "after" change based on whether the
// page is in RTL or LTR.
/** @type {?} */
let horizontalStyleProperty;
if (this._isRtl()) {
horizontalStyleProperty = position.overlayX === 'end' ? 'left' : 'right';
}
else {
horizontalStyleProperty = position.overlayX === 'end' ? 'right' : 'left';
}
// When we're setting `right`, we adjust the x position such that it is the distance
// from the right edge of the viewport rather than the left edge.
if (horizontalStyleProperty === 'right') {
/** @type {?} */
const documentWidth = (/** @type {?} */ (this._document.documentElement)).clientWidth;
styles.right = `${documentWidth - (overlayPoint.x + this._overlayRect.width)}px`;
}
else {
styles.left = coerceCssPixelValue(overlayPoint.x);
}
return styles;
}
/**
* Gets the view properties of the trigger and overlay, including whether they are clipped
* or completely outside the view of any of the strategy's scrollables.
* @private
* @return {?}
*/
_getScrollVisibility() {
// Note: needs fresh rects since the position could've changed.
/** @type {?} */
const originBounds = this._getOriginRect();
/** @type {?} */
const overlayBounds = this._pane.getBoundingClientRect();
// TODO(jelbourn): instead of needing all of the client rects for these scrolling containers
// every time, we should be able to use the scrollTop of the containers if the size of those
// containers hasn't changed.
/** @type {?} */
const scrollContainerBounds = this._scrollables.map((/**
* @param {?} scrollable
* @return {?}
*/
scrollable => {
return scrollable.getElementRef().nativeElement.getBoundingClientRect();
}));
return {
isOriginClipped: isElementClippedByScrolling(originBounds, scrollContainerBounds),
isOriginOutsideView: isElementScrolledOutsideView(originBounds, scrollContainerBounds),
isOverlayClipped: isElementClippedByScrolling(overlayBounds, scrollContainerBounds),
isOverlayOutsideView: isElementScrolledOutsideView(overlayBounds, scrollContainerBounds),
};
}
/**
* Subtracts the amount that an element is overflowing on an axis from its length.
* @private
* @param {?} length
* @param {...?} overflows
* @return {?}
*/
_subtractOverflows(length, ...overflows) {
return overflows.reduce((/**
* @param {?} currentValue
* @param {?} currentOverflow
* @return {?}
*/
(currentValue, currentOverflow) => {
return currentValue - Math.max(currentOverflow, 0);
}), length);
}
/**
* Narrows the given viewport rect by the current _viewportMargin.
* @private
* @return {?}
*/
_getNarrowedViewportRect() {
// We recalculate the viewport rect here ourselves, rather than using the ViewportRuler,
// because we want to use the `clientWidth` and `clientHeight` as the base. The difference
// being that the client properties don't include the scrollbar, as opposed to `innerWidth`
// and `innerHeight` that do. This is necessary, because the overlay container uses
// 100% `width` and `height` which don't include the scrollbar either.
/** @type {?} */
const width = (/** @type {?} */ (this._document.documentElement)).clientWidth;
/** @type {?} */
const height = (/** @type {?} */ (this._document.documentElement)).clientHeight;
/** @type {?} */
const scrollPosition = this._viewportRuler.getViewportScrollPosition();
return {
top: scrollPosition.top + this._viewportMargin,
left: scrollPosition.left + this._viewportMargin,
right: scrollPosition.left + width - this._viewportMargin,
bottom: scrollPosition.top + height - this._viewportMargin,
width: width - (2 * this._viewportMargin),
height: height - (2 * this._viewportMargin),
};
}
/**
* Whether the we're dealing with an RTL context
* @private
* @return {?}
*/
_isRtl() {
return this._overlayRef.getDirection() === 'rtl';
}
/**
* Determines whether the overlay uses exact or flexible positioning.
* @private
* @return {?}
*/
_hasExactPosition() {
return !this._hasFlexibleDimensions || this._isPushed;
}
/**
* Retrieves the offset of a position along the x or y axis.
* @private
* @param {?} position
* @param {?} axis
* @return {?}
*/
_getOffset(position, axis) {
if (axis === 'x') {
// We don't do something like `position['offset' + axis]` in
// order to avoid breking minifiers that rename properties.
return position.offsetX == null ? this._offsetX : position.offsetX;
}
return position.offsetY == null ? this._offsetY : position.offsetY;
}
/**
* Validates that the current position match the expected values.
* @private
* @return {?}
*/
_validatePositions() {
if (!this._preferredPositions.length) {
throw Error('FlexibleConnectedPositionStrategy: At least one position is required.');
}
// TODO(crisbeto): remove these once Angular's template type
// checking is advanced enough to catch these cases.
this._preferredPositions.forEach((/**
* @param {?} pair
* @return {?}
*/
pair => {
validateHorizontalPosition('originX', pair.originX);
validateVerticalPosition