From a52d50e788792a63e97a9176ab319d53db7a2853 Mon Sep 17 00:00:00 2001 From: vempo Date: Tue, 24 Jul 2018 17:34:04 +0300 Subject: Replaced old implementation at root Old project files and directories has been moved under 'deprecated-workflow-designer'. The old project is not built by the CI anymore, but can be still built manually. New modules/directories have been moved up and integrated with the CI system. Change-Id: I1528c792bcbcce9e50bfc294a1328a20e72c91cf Issue-ID: SDC-1559 Signed-off-by: vempo --- .../position/connected-position-strategy.ts | 478 +++++++++++++++++++++ 1 file changed, 478 insertions(+) create mode 100644 deprecated-workflow-designer/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/connected-position-strategy.ts (limited to 'deprecated-workflow-designer/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/connected-position-strategy.ts') diff --git a/deprecated-workflow-designer/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/connected-position-strategy.ts b/deprecated-workflow-designer/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/connected-position-strategy.ts new file mode 100644 index 00000000..d144c81f --- /dev/null +++ b/deprecated-workflow-designer/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/connected-position-strategy.ts @@ -0,0 +1,478 @@ +/** + * @license + * Copyright Google Inc. 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 + */ +/* tslint:disable:array-type member-access variable-name typedef + only-arrow-functions directive-class-suffix component-class-suffix + component-selector no-unnecessary-type-assertion arrow-parens + no-unused-variable*/ +import {ElementRef} from '@angular/core'; +import {Observable} from 'rxjs/Observable'; +import {Subject} from 'rxjs/Subject'; + +import {Scrollable} from '../scroll/scrollable'; + +import {ConnectedOverlayPositionChange, ConnectionPositionPair, OriginConnectionPosition, OverlayConnectionPosition, ScrollableViewProperties} from './connected-position'; +import {PositionStrategy} from './position-strategy'; +import {ViewportRuler} from './viewport-ruler'; + +/** + * Container to hold the bounding positions of a particular element with respect + * to the viewport, where top and bottom are the y-axis coordinates of the + * bounding rectangle and left and right are the x-axis coordinates. + */ +interface ElementBoundingPositions { + top: number; + right: number; + bottom: number; + left: number; +} + +/** + * 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 ConnectedPositionStrategy implements PositionStrategy { + private _dir = 'ltr'; + + /** The offset in pixels for the overlay connection point on the x-axis */ + private _offsetX = 0; + + /** The offset in pixels for the overlay connection point on the y-axis */ + private _offsetY = 0; + + /** The Scrollable containers used to check scrollable view properties on + * position change. */ + private scrollables: Scrollable[] = []; + + /** Whether the we're dealing with an RTL context */ + get _isRtl() { + return this._dir === 'rtl'; + } + + /** Ordered list of preferred positions, from most to least desirable. */ + _preferredPositions: ConnectionPositionPair[] = []; + + /** The origin element against which the overlay will be positioned. */ + private _origin: HTMLElement; + + /** The overlay pane element. */ + private _pane: HTMLElement; + + /** The last position to have been calculated as the best fit position. */ + private _lastConnectedPosition: ConnectionPositionPair; + + _onPositionChange: Subject = + new Subject(); + + /** Emits an event when the connection point changes. */ + get onPositionChange(): Observable { + return this._onPositionChange.asObservable(); + } + + constructor( + private _connectedTo: ElementRef, + private _originPos: OriginConnectionPosition, + private _overlayPos: OverlayConnectionPosition, + private _viewportRuler: ViewportRuler) { + this._origin = this._connectedTo.nativeElement; + this.withFallbackPosition(this._originPos, this._overlayPos); + } + + /** Ordered list of preferred positions, from most to least desirable. */ + get positions() { + return this._preferredPositions; + } + + /** + * To be used to for any cleanup after the element gets destroyed. + */ + dispose() { + // + } + + /** + * Updates the position of the overlay element, using whichever preferred + * position relative to the origin fits on-screen. + * @docs-private + * + * @param element Element to which to apply the CSS styles. + * @returns Resolves when the styles have been applied. + */ + apply(element: HTMLElement): void { + // Cache the overlay pane element in case re-calculating position is + // necessary + this._pane = element; + + // We need the bounding rects for the origin and the overlay to determine + // how to position the overlay relative to the origin. + const originRect = this._origin.getBoundingClientRect(); + const overlayRect = element.getBoundingClientRect(); + + // We use the viewport rect to determine whether a position would go + // off-screen. + const viewportRect = this._viewportRuler.getViewportRect(); + + // Fallback point if none of the fallbacks fit into the viewport. + let fallbackPoint: OverlayPoint|undefined; + let fallbackPosition: ConnectionPositionPair|undefined; + + // We want to place the overlay in the first of the preferred positions such + // that the overlay fits on-screen. + for (const pos of this._preferredPositions) { + // Get the (x, y) point of connection on the origin, and then use that to + // get the (top, left) coordinate for the overlay at `pos`. + const originPoint = this._getOriginConnectionPoint(originRect, pos); + const overlayPoint = + this._getOverlayPoint(originPoint, overlayRect, viewportRect, pos); + + // If the overlay in the calculated position fits on-screen, put it there + // and we're done. + if (overlayPoint.fitsInViewport) { + this._setElementPosition(element, overlayRect, overlayPoint, pos); + + // Save the last connected position in case the position needs to be + // re-calculated. + this._lastConnectedPosition = pos; + + // Notify that the position has been changed along with its change + // properties. + const scrollableViewProperties = + this.getScrollableViewProperties(element); + const positionChange = + new ConnectedOverlayPositionChange(pos, scrollableViewProperties); + this._onPositionChange.next(positionChange); + + return; + } else if ( + !fallbackPoint || + fallbackPoint.visibleArea < overlayPoint.visibleArea) { + fallbackPoint = overlayPoint; + fallbackPosition = pos; + } + } + + // If none of the preferred positions were in the viewport, take the one + // with the largest visible area. + this._setElementPosition( + element, overlayRect, fallbackPoint, fallbackPosition); + } + + /** + * 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. + */ + recalculateLastPosition(): void { + const originRect = this._origin.getBoundingClientRect(); + const overlayRect = this._pane.getBoundingClientRect(); + const viewportRect = this._viewportRuler.getViewportRect(); + const lastPosition = + this._lastConnectedPosition || this._preferredPositions[0]; + + const originPoint = + this._getOriginConnectionPoint(originRect, lastPosition); + const overlayPoint = this._getOverlayPoint( + originPoint, overlayRect, viewportRect, lastPosition); + this._setElementPosition( + this._pane, overlayRect, overlayPoint, lastPosition); + } + + /** + * 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. + */ + withScrollableContainers(scrollables: Scrollable[]) { + this.scrollables = scrollables; + } + + /** + * Adds a new preferred fallback position. + * @param originPos + * @param overlayPos + */ + withFallbackPosition( + originPos: OriginConnectionPosition, + overlayPos: OverlayConnectionPosition): this { + this._preferredPositions.push( + new ConnectionPositionPair(originPos, overlayPos)); + return this; + } + + /** + * Sets the layout direction so the overlay's position can be adjusted to + * match. + * @param dir New layout direction. + */ + withDirection(dir: 'ltr'|'rtl'): this { + this._dir = dir; + return this; + } + + /** + * Sets an offset for the overlay's connection point on the x-axis + * @param offset New offset in the X axis. + */ + withOffsetX(offset: number): this { + this._offsetX = offset; + return this; + } + + /** + * Sets an offset for the overlay's connection point on the y-axis + * @param offset New offset in the Y axis. + */ + withOffsetY(offset: number): this { + this._offsetY = offset; + return this; + } + + /** + * Gets the horizontal (x) "start" dimension based on whether the overlay is + * in an RTL context. + * @param rect + */ + private _getStartX(rect: ClientRect): number { + return this._isRtl ? rect.right : rect.left; + } + + /** + * Gets the horizontal (x) "end" dimension based on whether the overlay is in + * an RTL context. + * @param rect + */ + private _getEndX(rect: ClientRect): number { + return this._isRtl ? rect.left : rect.right; + } + + + /** + * Gets the (x, y) coordinate of a connection point on the origin based on a + * relative position. + * @param originRect + * @param pos + */ + private _getOriginConnectionPoint( + originRect: ClientRect, pos: ConnectionPositionPair): Point { + const originStartX = this._getStartX(originRect); + const originEndX = this._getEndX(originRect); + + let x: number; + if (pos.originX === 'center') { + x = originStartX + (originRect.width / 2); + } else { + x = pos.originX === 'start' ? originStartX : originEndX; + } + + let y: number; + 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, + * as well as how much of the element would be inside the viewport at that + * position. + */ + private _getOverlayPoint( + originPoint: Point, overlayRect: ClientRect, viewportRect: ClientRect, + pos: ConnectionPositionPair): OverlayPoint { + // Calculate the (overlayStartX, overlayStartY), the start of the potential + // overlay position relative to the origin point. + let overlayStartX: number; + 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; + } + + let overlayStartY: number; + if (pos.overlayY === 'center') { + overlayStartY = -overlayRect.height / 2; + } else { + overlayStartY = pos.overlayY === 'top' ? 0 : -overlayRect.height; + } + + // The (x, y) coordinates of the overlay. + const x = originPoint.x + overlayStartX + this._offsetX; + const y = originPoint.y + overlayStartY + this._offsetY; + + // How much the overlay would overflow at this position, on each side. + const leftOverflow = 0 - x; + const rightOverflow = (x + overlayRect.width) - viewportRect.width; + const topOverflow = 0 - y; + const bottomOverflow = (y + overlayRect.height) - viewportRect.height; + + // Visible parts of the element on each axis. + const visibleWidth = + this._subtractOverflows(overlayRect.width, leftOverflow, rightOverflow); + const visibleHeight = this._subtractOverflows( + overlayRect.height, topOverflow, bottomOverflow); + + // The area of the element that's within the viewport. + const visibleArea = visibleWidth * visibleHeight; + const fitsInViewport = + (overlayRect.width * overlayRect.height) === visibleArea; + + return {x, y, fitsInViewport, visibleArea}; + } + + /** + * 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 getScrollableViewProperties(overlay: HTMLElement): + ScrollableViewProperties { + const originBounds = this._getElementBounds(this._origin); + const overlayBounds = this._getElementBounds(overlay); + const scrollContainerBounds = + this.scrollables.map((scrollable: Scrollable) => { + return this._getElementBounds( + scrollable.getElementRef().nativeElement); + }); + + return { + isOriginClipped: + this.isElementClipped(originBounds, scrollContainerBounds), + isOriginOutsideView: + this.isElementOutsideView(originBounds, scrollContainerBounds), + isOverlayClipped: + this.isElementClipped(overlayBounds, scrollContainerBounds), + isOverlayOutsideView: + this.isElementOutsideView(overlayBounds, scrollContainerBounds), + }; + } + + /** Whether the element is completely out of the view of any of the + * containers. */ + private isElementOutsideView( + elementBounds: ElementBoundingPositions, + containersBounds: ElementBoundingPositions[]): boolean { + return containersBounds.some( + (containerBounds: ElementBoundingPositions) => { + const outsideAbove = elementBounds.bottom < containerBounds.top; + const outsideBelow = elementBounds.top > containerBounds.bottom; + const outsideLeft = elementBounds.right < containerBounds.left; + const outsideRight = elementBounds.left > containerBounds.right; + + return outsideAbove || outsideBelow || outsideLeft || outsideRight; + }); + } + + /** Whether the element is clipped by any of the containers. */ + private isElementClipped( + elementBounds: ElementBoundingPositions, + containersBounds: ElementBoundingPositions[]): boolean { + return containersBounds.some( + (containerBounds: ElementBoundingPositions) => { + const clippedAbove = elementBounds.top < containerBounds.top; + const clippedBelow = elementBounds.bottom > containerBounds.bottom; + const clippedLeft = elementBounds.left < containerBounds.left; + const clippedRight = elementBounds.right > containerBounds.right; + + return clippedAbove || clippedBelow || clippedLeft || clippedRight; + }); + } + + /** Physically positions the overlay element to the given coordinate. */ + private _setElementPosition( + element: HTMLElement, overlayRect: ClientRect, overlayPoint: Point, + pos: ConnectionPositionPair) { + // 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. + const verticalStyleProperty = pos.overlayY === 'bottom' ? 'bottom' : 'top'; + + // When using `bottom`, we adjust the y position such that it is the + // distance from the bottom of the viewport rather than the top. + const y = verticalStyleProperty === 'top' ? + overlayPoint.y : + document.documentElement.clientHeight - + (overlayPoint.y + overlayRect.height); + + // 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. + let horizontalStyleProperty: any; + if (this._dir === 'rtl') { + horizontalStyleProperty = pos.overlayX === 'end' ? 'left' : 'right'; + } else { + horizontalStyleProperty = pos.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. + const x = horizontalStyleProperty === 'left' ? + overlayPoint.x : + document.documentElement.clientWidth - + (overlayPoint.x + overlayRect.width); + + + // Reset any existing styles. This is necessary in case the preferred + // position has changed since the last `apply`. + ['top', 'bottom', 'left', 'right'].forEach( + (p: any) => element.style[p] = null); + + element.style[verticalStyleProperty] = `${y}px`; + element.style[horizontalStyleProperty] = `${x}px`; + } + + /** Returns the bounding positions of the provided element with respect to the + * viewport. */ + private _getElementBounds(element: HTMLElement): ElementBoundingPositions { + const boundingClientRect = element.getBoundingClientRect(); + return { + top: boundingClientRect.top, + right: boundingClientRect.left + boundingClientRect.width, + bottom: boundingClientRect.top + boundingClientRect.height, + left: boundingClientRect.left + }; + } + + /** + * Subtracts the amount that an element is overflowing on an axis from it's + * length. + */ + private _subtractOverflows(length: number, ...overflows: number[]): number { + return overflows.reduce((currentValue: number, currentOverflow: number) => { + return currentValue - Math.max(currentOverflow, 0); + }, length); + } +} + +/** A simple (x, y) coordinate. */ +interface Point { + x: number; + y: number; +} + +/** + * Expands the simple (x, y) coordinate by adding info about whether the + * element would fit inside the viewport at that position, as well as + * how much of the element would be visible. + */ +interface OverlayPoint extends Point { + visibleArea: number; + fitsInViewport: boolean; +} -- cgit 1.2.3-korg