diff options
author | 2018-03-27 17:24:38 +0800 | |
---|---|---|
committer | 2018-03-27 17:24:38 +0800 | |
commit | 573f32b362f4639928485d66feb1c0721109716b (patch) | |
tree | 39ab8771c9ef87ee2b61abe1e126e31629bdf9eb /sdc-workflow-designer-ui/src/app/paletx | |
parent | 99fcc841c86367722097f54497dbc7c856c19251 (diff) |
Include paletx core
Include paletx core to WF Designer UI.
Issue-ID: SDC-1130,SDC-1131
Change-Id: I9a2591e022b5ff118cccbbc839796be19d70df84
Signed-off-by: YuanHu <yuan.hu1@zte.com.cn>
Diffstat (limited to 'sdc-workflow-designer-ui/src/app/paletx')
37 files changed, 3611 insertions, 0 deletions
diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/boolean-field-value.ts b/sdc-workflow-designer-ui/src/app/paletx/core/boolean-field-value.ts new file mode 100644 index 00000000..dc1f86e2 --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/boolean-field-value.ts @@ -0,0 +1,19 @@ +/* tslint:disable:array-type member-access variable-name */ +function booleanFieldValueFactory() { + return function booleanFieldValueMetadata(target: any, key: string): void { + const defaultValue = target[key]; + const localKey = `__ky_private_symbol_${key}`; + target[localKey] = defaultValue; + + Object.defineProperty(target, key, { + get() { + return (this)[localKey]; + }, + set(value: boolean) { + (this)[localKey] = value !== null && `${value}` !== 'false'; + } + }); + }; +} + +export {booleanFieldValueFactory as BooleanFieldValue}; diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/domhandler.ts b/sdc-workflow-designer-ui/src/app/paletx/core/domhandler.ts new file mode 100644 index 00000000..fd700a9b --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/domhandler.ts @@ -0,0 +1,432 @@ +import {Injectable} from '@angular/core'; + +@Injectable() +export class DomHandler { + static zindex: number = 1000; + + public addClass(element: any, className: string): void { + if (element.classList) { + element.classList.add(className); + } else { + element.className += ' ' + className; + } + } + + public addMultipleClasses(element: any, className: string): void { + if (element.classList) { + let styles: string[] = className.split(' '); + // for (let i = 0; i < styles.length; i++) { + // element.classList.add(styles[i]); + // } + for (let style of styles) { + element.classList.add(style); + } + + } else { + let styles: string[] = className.split(' '); + // for (let i = 0; i < styles.length; i++) { + // element.className += ' ' + styles[i]; + // } + for (let style of styles) { + element.className += ' ' + style; + } + } + } + + public removeClass(element: any, className: string): void { + if (element.classList) { + element.classList.remove(className); + } else { + element.className = element.className.replace( + new RegExp( + '(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), + ' '); + } + } + + public hasClass(element: any, className: string): boolean { + if (element.classList) { + return element.classList.contains(className); + } else { + return new RegExp('(^| )' + className + '( |$)', 'gi') + .test(element.className); + } + } + + public siblings(element: any): any { + return Array.prototype.filter.call( + element.parentNode.children, (child: any) => { + return child !== element; + }); + } + + public find(element: any, selector: string): any[] { + return element.querySelectorAll(selector); + } + + public findSingle(element: any, selector: string): any { + return element.querySelector(selector); + } + + public index(element: any): number { + let children = element.parentNode.childNodes; + let num = 0; + // for (let i = 0; i < children.length; i++) { + // if (children[i] == element) { + // return num; + // } + // if (children[i].nodeType == 1) { + // num++; + // } + // } + for (let child of children) { + if (child === element) { + return num; + } + if (child.nodeType === 1) { + num++; + } + } + return -1; + } + + public relativePosition(element: any, target: any): void { + let elementDimensions = element.offsetParent ? + {width: element.outerWidth, height: element.outerHeight} : + this.getHiddenElementDimensions(element); + let targetHeight = target.offsetHeight; + let targetWidth = target.offsetWidth; + let targetOffset = target.getBoundingClientRect(); + let viewport = this.getViewport(); + let top; + let left; + + if ((targetOffset.top + targetHeight + elementDimensions.height) > + viewport.height) { + top = -1 * (elementDimensions.height); + } else { + top = targetHeight; + } + + if ((targetOffset.left + elementDimensions.width) > viewport.width) { + left = targetWidth - elementDimensions.width; + } else { + left = 0; + } + + element.style.top = top + 'px'; + element.style.left = left + 'px'; + } + + public absolutePosition(element: any, target: any): void { + let elementDimensions = element.offsetParent ? + {width: element.offsetWidth, height: element.offsetHeight} : + this.getHiddenElementDimensions(element); + let elementOuterHeight = elementDimensions.height; + let elementOuterWidth = elementDimensions.width; + let targetOuterHeight = target.offsetHeight; + let targetOuterWidth = target.offsetWidth; + let targetOffset = target.getBoundingClientRect(); + let windowScrollTop = this.getWindowScrollTop(); + let windowScrollLeft = this.getWindowScrollLeft(); + let viewport = this.getViewport(); + let top; + let left; + + if (targetOffset.top + targetOuterHeight + elementOuterHeight > + viewport.height) { + top = targetOffset.top + windowScrollTop - elementOuterHeight; + if (top < 0) { + top = 0 + windowScrollTop; + } + } else { + top = targetOuterHeight + targetOffset.top + windowScrollTop; + } + + if (targetOffset.left + targetOuterWidth + elementOuterWidth > + viewport.width) { + left = targetOffset.left + windowScrollLeft + targetOuterWidth - + elementOuterWidth; + } else { + left = targetOffset.left + windowScrollLeft; + } + + element.style.top = top + 'px'; + element.style.left = left + 'px'; + } + + public getHiddenElementOuterHeight(element: any): number { + element.style.visibility = 'hidden'; + element.style.display = 'block'; + let elementHeight = element.offsetHeight; + element.style.display = 'none'; + element.style.visibility = 'visible'; + + return elementHeight; + } + + public getHiddenElementOuterWidth(element: any): number { + element.style.visibility = 'hidden'; + element.style.display = 'block'; + let elementWidth = element.offsetWidth; + element.style.display = 'none'; + element.style.visibility = 'visible'; + + return elementWidth; + } + + public getHiddenElementDimensions(element: any): any { + let dimensions: any = {}; + element.style.visibility = 'hidden'; + element.style.display = 'block'; + dimensions.width = element.offsetWidth; + dimensions.height = element.offsetHeight; + element.style.display = 'none'; + element.style.visibility = 'visible'; + + return dimensions; + } + + public scrollInView(container: any, item: any) { + let borderTopValue: string = + getComputedStyle(container).getPropertyValue('borderTopWidth'); + let borderTop: number = borderTopValue ? parseFloat(borderTopValue) : 0; + let paddingTopValue: string = + getComputedStyle(container).getPropertyValue('paddingTop'); + let paddingTop: number = paddingTopValue ? parseFloat(paddingTopValue) : 0; + let containerRect = container.getBoundingClientRect(); + let itemRect = item.getBoundingClientRect(); + let offset = (itemRect.top + document.body.scrollTop) - + (containerRect.top + document.body.scrollTop) - borderTop - paddingTop; + let scroll = container.scrollTop; + let elementHeight = container.clientHeight; + let itemHeight = this.getOuterHeight(item); + + if (offset < 0) { + container.scrollTop = scroll + offset; + } else if ((offset + itemHeight) > elementHeight) { + container.scrollTop = scroll + offset - elementHeight + itemHeight; + } + } + + public fadeIn(element: any, duration: number): void { + element.style.opacity = 0; + + let last = +new Date(); + let opacity = 0; + let tick = () => { + opacity = + +element.style.opacity + (new Date().getTime() - last) / duration; + element.style.opacity = opacity; + last = +new Date(); + + if (+opacity < 1) { + if (!window.requestAnimationFrame || !requestAnimationFrame(tick)) { + setTimeout(tick, 16); + } + + /*(window.requestAnimationFrame && requestAnimationFrame(tick)) || + setTimeout(tick, 16);*/ + } + }; + + tick(); + } + + public fadeOut(element: any, ms: any) { + let opacity = 1; + let interval = 50; + let duration = ms; + let gap = interval / duration; + + let fading = setInterval(() => { + opacity = opacity - gap; + + if (opacity <= 0) { + opacity = 0; + clearInterval(fading); + } + + element.style.opacity = opacity; + }, interval); + } + + public getWindowScrollTop(): number { + let doc = document.documentElement; + return (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); + } + + public getWindowScrollLeft(): number { + let doc = document.documentElement; + return (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); + } + + public matches(element: any, selector: string): boolean { + let p: any = Element.prototype; + let f: any = p['matches'] || p.webkitMatchesSelector || + p['mozMatchesSelector'] || p.msMatchesSelector || function(s: any) { + return [].indexOf.call(document.querySelectorAll(s), this) !== -1; + }; + return f.call(element, selector); + } + + public getOuterWidth(el: any, margin?: any) { + let width = el.offsetWidth; + + if (margin) { + let style = getComputedStyle(el); + width += parseFloat(style.marginLeft) + parseFloat(style.marginRight); + } + + return width; + } + + public getHorizontalPadding(el: any) { + let style = getComputedStyle(el); + return parseFloat(style.paddingLeft) + parseFloat(style.paddingRight); + } + + public getHorizontalMargin(el: any) { + let style = getComputedStyle(el); + return parseFloat(style.marginLeft) + parseFloat(style.marginRight); + } + + public innerWidth(el: any) { + let width = el.offsetWidth; + let style = getComputedStyle(el); + + width += parseFloat(style.paddingLeft) + parseFloat(style.paddingRight); + return width; + } + + public width(el: any) { + let width = el.offsetWidth; + let style = getComputedStyle(el); + + width -= parseFloat(style.paddingLeft) + parseFloat(style.paddingRight); + return width; + } + + public getOuterHeight(el: any, margin?: any) { + let height = el.offsetHeight; + + if (margin) { + let style = getComputedStyle(el); + height += parseFloat(style.marginTop) + parseFloat(style.marginBottom); + } + + return height; + } + + public getHeight(el: any): number { + let height = el.offsetHeight; + let style = getComputedStyle(el); + + height -= parseFloat(style.paddingTop) + parseFloat(style.paddingBottom) + + parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth); + + return height; + } + + public getWidth(el: any): number { + let width = el.offsetWidth; + let style = getComputedStyle(el); + + width -= parseFloat(style.paddingLeft) + parseFloat(style.paddingRight) + + parseFloat(style.borderLeftWidth) + parseFloat(style.borderRightWidth); + + return width; + } + + public getViewport(): any { + let win = window; + let d = document; + let e = d.documentElement; + let g = d.getElementsByTagName('body')[0]; + let w = win.innerWidth || e.clientWidth || g.clientWidth; + let h = win.innerHeight || e.clientHeight || g.clientHeight; + + return {width: w, height: h}; + } + + public getOffset(el: any) { + let x = el.offsetLeft; + let y = el.offsetTop; + + while (el = el.offsetParent) { + x += el.offsetLeft; + y += el.offsetTop; + } + + return {left: x, top: y}; + } + + public getUserAgent(): string { + return navigator.userAgent; + } + + public isIE() { + let ua = window.navigator.userAgent; + + let msie = ua.indexOf('MSIE '); + if (msie > 0) { + // IE 10 or older => return version number + return true; + } + + let trident = ua.indexOf('Trident/'); + if (trident > 0) { + // IE 11 => return version number + /* let rv = ua.indexOf('rv:');*/ + return true; + } + + let edge = ua.indexOf('Edge/'); + if (edge > 0) { + // Edge (IE 12+) => return version number + return true; + } + + // other browser + return false; + } + + public appendChild(element: any, target: any) { + if (this.isElement(target)) { + target.appendChild(element); + } else if (target.el && target.el.nativeElement) { + target.el.nativeElement.appendChild(element); + } else { + throw 'Cannot append ' + target + ' to ' + element; + } + } + + public removeChild(element: any, target: any) { + if (this.isElement(target)) { + target.removeChild(element); + } else if (target.el && target.el.nativeElement) { + target.el.nativeElement.removeChild(element); + } else { + throw 'Cannot remove ' + element + ' from ' + target; + } + } + + public isElement(obj: any) { + return ( + typeof HTMLElement === 'object' ? + obj instanceof HTMLElement : + obj && typeof obj === 'object' && obj !== null && + obj.nodeType === 1 && typeof obj.nodeName === 'string'); + } + + public calculateScrollbarWidth(): number { + let scrollDiv = document.createElement('div'); + scrollDiv.className = 'ui-scrollbar-measure'; + document.body.appendChild(scrollDiv); + + let scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth; + document.body.removeChild(scrollDiv); + + return scrollbarWidth; + } +}
\ No newline at end of file diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/number-wrapper-parse.ts b/sdc-workflow-designer-ui/src/app/paletx/core/number-wrapper-parse.ts new file mode 100644 index 00000000..ceccd92e --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/number-wrapper-parse.ts @@ -0,0 +1,10 @@ +/* tslint:disable:array-type member-access variable-name */ +export function NumberWrapperParseFloat(text: any) { + if (/^(\-|\+)?[0-9]+$/.test(text)) { + return parseInt(text); + } else if (/^(\-|\+)?[0-9]+\.[0-9]+$/.test(text)) { + return parseFloat(text); + } else { + return 0; + } +} diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/fullscreen-overlay-container.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/fullscreen-overlay-container.ts new file mode 100644 index 00000000..0eca202d --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/fullscreen-overlay-container.ts @@ -0,0 +1,62 @@ +/** + * @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*/ +import {Injectable} from '@angular/core'; +import {OverlayContainer} from './overlay-container'; + +/** + * The FullscreenOverlayContainer is the alternative to OverlayContainer + * that supports correct displaying of overlay elements in Fullscreen mode + * https://developer.mozilla.org/en-US/docs/Web/API/Element/requestFullScreen + * It should be provided in the root component that way: + * providers: [ + * {provide: OverlayContainer, useClass: FullscreenOverlayContainer} + * ], + */ +@Injectable() +export class FullscreenOverlayContainer extends OverlayContainer { + protected _createContainer(): void { + super._createContainer(); + this._adjustParentForFullscreenChange(); + this._addFullscreenChangeListener( + () => this._adjustParentForFullscreenChange()); + } + + private _adjustParentForFullscreenChange(): void { + if (!this._containerElement) { + return; + } + const fullscreenElement = this.getFullscreenElement(); + const parent = fullscreenElement || document.body; + parent.appendChild(this._containerElement); + } + + private _addFullscreenChangeListener(fn: () => void) { + if (document.fullscreenEnabled) { + document.addEventListener('fullscreenchange', fn); + } else if (document.webkitFullscreenEnabled) { + document.addEventListener('webkitfullscreenchange', fn); + } else if ((document as any).mozFullScreenEnabled) { + document.addEventListener('mozfullscreenchange', fn); + } else if ((document as any).msFullscreenEnabled) { + document.addEventListener('MSFullscreenChange', fn); + } + } + + /** + * When the page is put into fullscreen mode, a specific element is specified. + * Only that element and its children are visible when in fullscreen mode. + */ + getFullscreenElement(): Element { + return document.fullscreenElement || document.webkitFullscreenElement || + (document as any).mozFullScreenElement || + (document as any).msFullscreenElement || null; + } +} diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/generic-component-type.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/generic-component-type.ts new file mode 100644 index 00000000..523bd428 --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/generic-component-type.ts @@ -0,0 +1,9 @@ +/** + * @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 + */ + +export interface ComponentType<T> { new(...args: any[]): T; } diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/index.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/index.ts new file mode 100644 index 00000000..e02bc3cc --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/index.ts @@ -0,0 +1,49 @@ +/** + * @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 + */ +import {NgModule, Provider} from '@angular/core'; + +import {Overlay} from './overlay'; +import {OVERLAY_CONTAINER_PROVIDER} from './overlay-container'; +import {ConnectedOverlayDirective, OverlayOrigin} from './overlay-directives'; +import {OverlayPositionBuilder} from './position/overlay-position-builder'; +import {VIEWPORT_RULER_PROVIDER} from './position/viewport-ruler'; +import {ScrollDispatchModule} from './scroll/index'; + + +export const OVERLAY_PROVIDERS: Provider[] = [ + Overlay, + OverlayPositionBuilder, + VIEWPORT_RULER_PROVIDER, + OVERLAY_CONTAINER_PROVIDER, +]; + +@NgModule({ + imports: [ScrollDispatchModule], + exports: [ConnectedOverlayDirective, OverlayOrigin, ScrollDispatchModule], + declarations: [ConnectedOverlayDirective, OverlayOrigin], + providers: [OVERLAY_PROVIDERS], +}) +export class OverlayModule { +} + + +export {Overlay} from './overlay'; +export {OverlayContainer} from './overlay-container'; +export {FullscreenOverlayContainer} from './fullscreen-overlay-container'; +export {OverlayRef} from './overlay-ref'; +export {OverlayState} from './overlay-state'; +export {ConnectedOverlayDirective, OverlayOrigin} from './overlay-directives'; +export {ViewportRuler} from './position/viewport-ruler'; + +export * from './position/connected-position'; +export * from './scroll/index'; + +// Export pre-defined position strategies and interface to build custom ones. +export {PositionStrategy} from './position/position-strategy'; +export {GlobalPositionStrategy} from './position/global-position-strategy'; +export {ConnectedPositionStrategy} from './position/connected-position-strategy'; diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/overlay-container.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/overlay-container.ts new file mode 100644 index 00000000..fbb37c7e --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/overlay-container.ts @@ -0,0 +1,83 @@ +/** + * @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*/ +import {Injectable, Optional, SkipSelf} from '@angular/core'; + + +/** + * The OverlayContainer is the container in which all overlays will load. + * It should be provided in the root component to ensure it is properly shared. + */ +@Injectable() +export class OverlayContainer { + protected _containerElement: HTMLElement; + + private _themeClass: string; + + /** + * Base theme to be applied to all overlay-based components. + */ + get themeClass(): string { + return this._themeClass; + } + set themeClass(value: string) { + if (this._containerElement) { + this._containerElement.classList.remove(this._themeClass); + + if (value) { + this._containerElement.classList.add(value); + } + } + + this._themeClass = value; + } + + /** + * This method returns the overlay container element. It will lazily + * create the element the first time it is called to facilitate using + * the container in non-browser environments. + * @returns the container element + */ + getContainerElement(): HTMLElement { + if (!this._containerElement) { + this._createContainer(); + } + return this._containerElement; + } + + /** + * Create the overlay container element, which is simply a div + * with the 'cdk-overlay-container' class on the document body. + */ + protected _createContainer(): void { + const container = document.createElement('div'); + container.classList.add('nz-overlay-container'); + + if (this._themeClass) { + container.classList.add(this._themeClass); + } + + document.body.appendChild(container); + this._containerElement = container; + } +} + +export function OVERLAY_CONTAINER_PROVIDER_FACTORY( + parentContainer: OverlayContainer) { + return parentContainer || new OverlayContainer(); +} + +export const OVERLAY_CONTAINER_PROVIDER = { + // If there is already an OverlayContainer available, use that. Otherwise, + // provide a new one. + provide: OverlayContainer, + deps: [[new Optional(), new SkipSelf(), OverlayContainer]], + useFactory: OVERLAY_CONTAINER_PROVIDER_FACTORY +}; diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/overlay-directives.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/overlay-directives.ts new file mode 100644 index 00000000..5b8c1623 --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/overlay-directives.ts @@ -0,0 +1,329 @@ +/** + * @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*/ +import {TemplatePortal} from '@angular/cdk'; +import {Direction, Directionality} from '@angular/cdk'; +import {ESCAPE} from '@angular/cdk'; +import {Directive, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, Optional, Output, Renderer2, SimpleChanges, TemplateRef, ViewContainerRef} from '@angular/core'; +import {Subscription} from 'rxjs/Subscription'; + +import {Overlay} from './overlay'; +import {OverlayRef} from './overlay-ref'; +import {OverlayState} from './overlay-state'; +import {ConnectedOverlayPositionChange, ConnectionPositionPair} from './position/connected-position'; +import {ConnectedPositionStrategy} from './position/connected-position-strategy'; +import {ScrollStrategy} from './scroll/scroll-strategy'; + +/** Coerces a data-bound value (typically a string) to a boolean. */ +export function coerceBooleanProperty(value: any): boolean { + return value !== null && `${value}` !== 'false'; +} + + +/** Default set of positions for the overlay. Follows the behavior of a + * dropdown. */ +const defaultPositionList = [ + new ConnectionPositionPair( + {originX: 'start', originY: 'bottom'}, + {overlayX: 'start', overlayY: 'top'}), + new ConnectionPositionPair( + {originX: 'start', originY: 'top'}, + {overlayX: 'start', overlayY: 'bottom'}), +]; + + +/** + * Directive applied to an element to make it usable as an origin for an Overlay + * using a ConnectedPositionStrategy. + */ +@Directive({ + selector: '[nz-overlay-origin]', + exportAs: 'nzOverlayOrigin', +}) +export class OverlayOrigin { + constructor(public elementRef: ElementRef) {} +} + + +/** + * Directive to facilitate declarative creation of an Overlay using a + * ConnectedPositionStrategy. + */ +@Directive({selector: '[nz-connected-overlay]', exportAs: 'nzConnectedOverlay'}) +export class ConnectedOverlayDirective implements OnDestroy, OnChanges { + private _overlayRef: OverlayRef; + private _templatePortal: TemplatePortal; + private _hasBackdrop = false; + private _backdropSubscription: Subscription|null; + private _positionSubscription: Subscription; + private _offsetX = 0; + private _offsetY = 0; + private _position: ConnectedPositionStrategy; + private _escapeListener: Function; + + /** Origin for the connected overlay. */ + @Input() origin: OverlayOrigin; + + /** Registered connected position pairs. */ + @Input() positions: ConnectionPositionPair[]; + + /** The offset in pixels for the overlay connection point on the x-axis */ + @Input() + get offsetX(): number { + return this._offsetX; + } + + set offsetX(offsetX: number) { + this._offsetX = offsetX; + if (this._position) { + this._position.withOffsetX(offsetX); + } + } + + /** The offset in pixels for the overlay connection point on the y-axis */ + @Input() + get offsetY() { + return this._offsetY; + } + + set offsetY(offsetY: number) { + this._offsetY = offsetY; + if (this._position) { + this._position.withOffsetY(offsetY); + } + } + + /** The width of the overlay panel. */ + @Input() width: number|string; + + /** The height of the overlay panel. */ + @Input() height: number|string; + + /** The min width of the overlay panel. */ + @Input() minWidth: number|string; + + /** The min height of the overlay panel. */ + @Input() minHeight: number|string; + + /** The custom class to be set on the backdrop element. */ + @Input() backdropClass: string; + + /** The custom class to be set on the pane element. */ + @Input() paneClass: string; + + /** Strategy to be used when handling scroll events while the overlay is open. + */ + @Input() + scrollStrategy: ScrollStrategy = this._overlay.scrollStrategies.reposition(); + + /** Whether the overlay is open. */ + @Input() open = false; + + /** Whether or not the overlay should attach a backdrop. */ + @Input() + get hasBackdrop() { + return this._hasBackdrop; + } + + set hasBackdrop(value: any) { + this._hasBackdrop = coerceBooleanProperty(value); + } + + /** Event emitted when the backdrop is clicked. */ + @Output() backdropClick = new EventEmitter<void>(); + + /** Event emitted when the position has changed. */ + @Output() positionChange = new EventEmitter<ConnectedOverlayPositionChange>(); + + /** Event emitted when the overlay has been attached. */ + @Output() attach = new EventEmitter<void>(); + + /** Event emitted when the overlay has been detached. */ + @Output() detach = new EventEmitter<void>(); + + // TODO(jelbourn): inputs for size, scroll behavior, animation, etc. + + constructor( + private _overlay: Overlay, private _renderer: Renderer2, + templateRef: TemplateRef<any>, viewContainerRef: ViewContainerRef, + @Optional() private _dir: Directionality) { + this._templatePortal = new TemplatePortal(templateRef, viewContainerRef); + } + + /** The associated overlay reference. */ + get overlayRef(): OverlayRef { + return this._overlayRef; + } + + /** The element's layout direction. */ + get dir(): Direction { + return this._dir ? this._dir.value : 'ltr'; + } + + ngOnDestroy() { + this._destroyOverlay(); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['open']) { + this.open ? this._attachOverlay() : this._detachOverlay(); + } + } + + /** Creates an overlay */ + private _createOverlay() { + if (!this.positions || !this.positions.length) { + this.positions = defaultPositionList; + } + + this._overlayRef = + this._overlay.create(this._buildConfig(), this.paneClass); + } + + /** Builds the overlay config based on the directive's inputs */ + private _buildConfig(): OverlayState { + const overlayConfig = new OverlayState(); + + if (this.width || this.width === 0) { + overlayConfig.width = this.width; + } + + if (this.height || this.height === 0) { + overlayConfig.height = this.height; + } + + if (this.minWidth || this.minWidth === 0) { + overlayConfig.minWidth = this.minWidth; + } + + if (this.minHeight || this.minHeight === 0) { + overlayConfig.minHeight = this.minHeight; + } + + overlayConfig.hasBackdrop = this.hasBackdrop; + + if (this.backdropClass) { + overlayConfig.backdropClass = this.backdropClass; + } + + this._position = + this._createPositionStrategy() as ConnectedPositionStrategy; + overlayConfig.positionStrategy = this._position; + overlayConfig.scrollStrategy = this.scrollStrategy; + + return overlayConfig; + } + + /** Returns the position strategy of the overlay to be set on the overlay + * config */ + private _createPositionStrategy(): ConnectedPositionStrategy { + const pos = this.positions[0]; + const originPoint = {originX: pos.originX, originY: pos.originY}; + const overlayPoint = {overlayX: pos.overlayX, overlayY: pos.overlayY}; + + const strategy = + this._overlay.position() + .connectedTo(this.origin.elementRef, originPoint, overlayPoint) + .withOffsetX(this.offsetX) + .withOffsetY(this.offsetY); + + this._handlePositionChanges(strategy); + + return strategy; + } + + private _handlePositionChanges(strategy: ConnectedPositionStrategy): void { + for (let i = 1; i < this.positions.length; i++) { + strategy.withFallbackPosition( + { + originX: this.positions[i].originX, + originY: this.positions[i].originY + }, + { + overlayX: this.positions[i].overlayX, + overlayY: this.positions[i].overlayY + }); + } + + this._positionSubscription = strategy.onPositionChange.subscribe( + pos => this.positionChange.emit(pos)); + } + + /** Attaches the overlay and subscribes to backdrop clicks if backdrop exists + */ + private _attachOverlay() { + if (!this._overlayRef) { + this._createOverlay(); + } + + this._position.withDirection(this.dir); + this._overlayRef.getState().direction = this.dir; + this._initEscapeListener(); + + if (!this._overlayRef.hasAttached()) { + this._overlayRef.attach(this._templatePortal); + this.attach.emit(); + } + + if (this.hasBackdrop) { + this._backdropSubscription = + this._overlayRef.backdropClick().subscribe(() => { + this.backdropClick.emit(); + }); + } + } + + /** Detaches the overlay and unsubscribes to backdrop clicks if backdrop + * exists */ + private _detachOverlay() { + if (this._overlayRef) { + this._overlayRef.detach(); + this.detach.emit(); + } + + if (this._backdropSubscription) { + this._backdropSubscription.unsubscribe(); + this._backdropSubscription = null; + } + + if (this._escapeListener) { + this._escapeListener(); + } + } + + /** Destroys the overlay created by this directive. */ + private _destroyOverlay() { + if (this._overlayRef) { + this._overlayRef.dispose(); + } + + if (this._backdropSubscription) { + this._backdropSubscription.unsubscribe(); + } + + if (this._positionSubscription) { + this._positionSubscription.unsubscribe(); + } + + if (this._escapeListener) { + this._escapeListener(); + } + } + + /** Sets the event listener that closes the overlay when pressing Escape. */ + private _initEscapeListener() { + this._escapeListener = + this._renderer.listen('document', 'keydown', (event: KeyboardEvent) => { + if (event.keyCode === ESCAPE) { + this._detachOverlay(); + } + }); + } +} diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/overlay-position-map.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/overlay-position-map.ts new file mode 100644 index 00000000..8ce53850 --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/overlay-position-map.ts @@ -0,0 +1,124 @@ +/* tslint:disable:array-type member-access variable-name typedef + only-arrow-functions directive-class-suffix component-class-suffix + component-selector one-variable-per-declaration + no-attribute-parameter-decorator*/ +import {ConnectionPositionPair} from './index'; + +export const POSITION_MAP: any = { + 'top': { + originX: 'center', + originY: 'top', + overlayX: 'center', + overlayY: 'bottom' + }, + 'topCenter': { + originX: 'center', + originY: 'top', + overlayX: 'center', + overlayY: 'bottom' + }, + 'topLeft': + {originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom'}, + 'topRight': + {originX: 'end', originY: 'top', overlayX: 'end', overlayY: 'bottom'}, + 'right': { + originX: 'end', + originY: 'center', + overlayX: 'start', + overlayY: 'center', + }, + 'rightTop': { + originX: 'end', + originY: 'top', + overlayX: 'start', + overlayY: 'top', + }, + 'rightBottom': { + originX: 'end', + originY: 'bottom', + overlayX: 'start', + overlayY: 'bottom', + }, + 'bottom': { + originX: 'center', + originY: 'bottom', + overlayX: 'center', + overlayY: 'top', + }, + 'bottomCenter': { + originX: 'center', + originY: 'bottom', + overlayX: 'center', + overlayY: 'top', + }, + 'bottomLeft': { + originX: 'start', + originY: 'bottom', + overlayX: 'start', + overlayY: 'top', + }, + 'bottomRight': { + originX: 'end', + originY: 'bottom', + overlayX: 'end', + overlayY: 'top', + }, + 'left': { + originX: 'start', + originY: 'center', + overlayX: 'end', + overlayY: 'center', + }, + 'leftTop': { + originX: 'start', + originY: 'top', + overlayX: 'end', + overlayY: 'top', + }, + 'leftBottom': { + originX: 'start', + originY: 'bottom', + overlayX: 'end', + overlayY: 'bottom', + }, +}; +export const DEFAULT_4_POSITIONS = _objectValues([ + POSITION_MAP['top'], POSITION_MAP['right'], POSITION_MAP['bottom'], + POSITION_MAP['left'] +]); +export const DEFAULT_DROPDOWN_POSITIONS = + _objectValues([POSITION_MAP['bottomLeft'], POSITION_MAP['topLeft']]); +export const DEFAULT_DATEPICKER_POSITIONS = [ + { + originX: 'start', + originY: 'top', + overlayX: 'start', + overlayY: 'top', + }, + { + originX: 'start', + originY: 'bottom', + overlayX: 'start', + overlayY: 'bottom', + } +] as ConnectionPositionPair[]; + +function arrayMap(array: any, iteratee: any) { + let index = -1; + const length = array === null ? 0 : array.length, result = Array(length); + + while (++index < length) { + result[index] = iteratee(array[index], index, array); + } + return result; +} + +function baseValues(object: any, props: any) { + return arrayMap(props, function(key: any) { + return object[key]; + }); +} + +function _objectValues(object: any) { + return object === null ? [] : baseValues(object, Object.keys(object)); +} diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/overlay-ref.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/overlay-ref.ts new file mode 100644 index 00000000..03c8c2b5 --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/overlay-ref.ts @@ -0,0 +1,271 @@ +/** + * @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 + promise-function-async*/ +import {Portal, PortalHost} from '@angular/cdk'; +import {NgZone} from '@angular/core'; +import {Observable} from 'rxjs/Observable'; +import {Subject} from 'rxjs/Subject'; + +import {OverlayState} from './overlay-state'; +import {ScrollStrategy} from './scroll/scroll-strategy'; + + +/** + * Reference to an overlay that has been created with the Overlay service. + * Used to manipulate or dispose of said overlay. + */ +export class OverlayRef implements PortalHost { + private _backdropElement: HTMLElement|null = null; + private _backdropClick: Subject<any> = new Subject(); + private _attachments = new Subject<void>(); + private _detachments = new Subject<void>(); + + constructor( + private _portalHost: PortalHost, private _pane: HTMLElement, + private _state: OverlayState, private _scrollStrategy: ScrollStrategy, + private _ngZone: NgZone) { + _scrollStrategy.attach(this); + } + + /** The overlay's HTML element */ + get overlayElement(): HTMLElement { + return this._pane; + } + + /** + * Attaches the overlay to a portal instance and adds the backdrop. + * @param portal Portal instance to which to attach the overlay. + * @returns The portal attachment result. + */ + attach(portal: Portal<any>): any { + const attachResult = this._portalHost.attach(portal); + + // Update the pane element with the given state configuration. + this._updateStackingOrder(); + this.updateSize(); + this.updateDirection(); + this.updatePosition(); + this._scrollStrategy.enable(); + + // Enable pointer events for the overlay pane element. + this._togglePointerEvents(true); + + if (this._state.hasBackdrop) { + this._attachBackdrop(); + } + + if (this._state.panelClass) { + this._pane.classList.add(this._state.panelClass); + } + + // Only emit the `attachments` event once all other setup is done. + this._attachments.next(); + + return attachResult; + } + + /** + * Detaches an overlay from a portal. + * @returns Resolves when the overlay has been detached. + */ + detach(): Promise<any> { + this.detachBackdrop(); + + // When the overlay is detached, the pane element should disable pointer + // events. This is necessary because otherwise the pane element will cover + // the page and disable pointer events therefore. Depends on the position + // strategy and the applied pane boundaries. + this._togglePointerEvents(false); + this._scrollStrategy.disable(); + + const detachmentResult = this._portalHost.detach(); + + // Only emit after everything is detached. + this._detachments.next(); + + return detachmentResult; + } + + /** + * Cleans up the overlay from the DOM. + */ + dispose(): void { + if (this._state.positionStrategy) { + this._state.positionStrategy.dispose(); + } + + if (this._scrollStrategy) { + this._scrollStrategy.disable(); + } + + this.detachBackdrop(); + this._portalHost.dispose(); + this._attachments.complete(); + this._backdropClick.complete(); + this._detachments.next(); + this._detachments.complete(); + } + + /** + * Checks whether the overlay has been attached. + */ + hasAttached(): boolean { + return this._portalHost.hasAttached(); + } + + /** + * Returns an observable that emits when the backdrop has been clicked. + */ + backdropClick(): Observable<void> { + return this._backdropClick.asObservable(); + } + + /** Returns an observable that emits when the overlay has been attached. */ + attachments(): Observable<void> { + return this._attachments.asObservable(); + } + + /** Returns an observable that emits when the overlay has been detached. */ + detachments(): Observable<void> { + return this._detachments.asObservable(); + } + + /** + * Gets the current state config of the overlay. + */ + getState(): OverlayState { + return this._state; + } + + /** Updates the position of the overlay based on the position strategy. */ + updatePosition() { + if (this._state.positionStrategy) { + this._state.positionStrategy.apply(this._pane); + } + } + + /** Updates the text direction of the overlay panel. */ + private updateDirection() { + this._pane.setAttribute('dir', this._state.direction); + } + + + /** Updates the size of the overlay based on the overlay config. */ + updateSize() { + if (this._state.width || this._state.width === 0) { + this._pane.style.width = formatCssUnit(this._state.width); + } + + if (this._state.height || this._state.height === 0) { + this._pane.style.height = formatCssUnit(this._state.height); + } + + if (this._state.minWidth || this._state.minWidth === 0) { + this._pane.style.minWidth = formatCssUnit(this._state.minWidth); + } + + if (this._state.minHeight || this._state.minHeight === 0) { + this._pane.style.minHeight = formatCssUnit(this._state.minHeight); + } + } + + /** Toggles the pointer events for the overlay pane element. */ + private _togglePointerEvents(enablePointer: boolean) { + this._pane.style.pointerEvents = enablePointer ? 'auto' : 'none'; + } + + /** Attaches a backdrop for this overlay. */ + private _attachBackdrop() { + this._backdropElement = document.createElement('div'); + this._backdropElement.classList.add('nz-overlay-backdrop'); + + if (this._state.backdropClass) { + this._backdropElement.classList.add(this._state.backdropClass); + } + + // Insert the backdrop before the pane in the DOM order, + // in order to handle stacked overlays properly. + this._pane.parentElement.insertBefore(this._backdropElement, this._pane); + + // Forward backdrop clicks such that the consumer of the overlay can perform + // whatever action desired when such a click occurs (usually closing the + // overlay). + this._backdropElement.addEventListener( + 'click', () => this._backdropClick.next(null)); + + // Add class to fade-in the backdrop after one frame. + requestAnimationFrame(() => { + if (this._backdropElement) { + this._backdropElement.classList.add('nz-overlay-backdrop-showing'); + } + }); + } + + /** + * Updates the stacking order of the element, moving it to the top if + * necessary. This is required in cases where one overlay was detached, while + * another one, that should be behind it, was destroyed. The next time both of + * them are opened, the stacking will be wrong, because the detached element's + * pane will still be in its original DOM position. + */ + private _updateStackingOrder() { + if (this._pane.nextSibling) { + this._pane.parentNode.appendChild(this._pane); + } + } + + /** Detaches the backdrop (if any) associated with the overlay. */ + detachBackdrop(): void { + const backdropToDetach = this._backdropElement; + + if (backdropToDetach) { + const finishDetach = () => { + // It may not be attached to anything in certain cases (e.g. unit + // tests). + if (backdropToDetach && backdropToDetach.parentNode) { + backdropToDetach.parentNode.removeChild(backdropToDetach); + } + + // It is possible that a new portal has been attached to this overlay + // since we started removing the backdrop. If that is the case, only + // clear the backdrop reference if it is still the same instance that we + // started to remove. + if (this._backdropElement === backdropToDetach) { + this._backdropElement = null; + } + }; + + backdropToDetach.classList.remove('nz-overlay-backdrop-showing'); + + if (this._state.backdropClass) { + backdropToDetach.classList.remove(this._state.backdropClass); + } + + backdropToDetach.addEventListener('transitionend', finishDetach); + + // If the backdrop doesn't have a transition, the `transitionend` event + // won't fire. In this case we make it unclickable and we try to remove it + // after a delay. + backdropToDetach.style.pointerEvents = 'none'; + + // Run this outside the Angular zone because there's nothing that Angular + // cares about. If it were to run inside the Angular zone, every test that + // used Overlay would have to be either async or fakeAsync. + this._ngZone.runOutsideAngular(() => { + setTimeout(finishDetach, 500); + }); + } + } +} + +function formatCssUnit(value: number|string) { + return typeof value === 'string' ? value as string : `${value}px`; +} diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/overlay-state.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/overlay-state.ts new file mode 100644 index 00000000..73d6b54d --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/overlay-state.ts @@ -0,0 +1,61 @@ +/** + * @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*/ +import {Direction} from '@angular/cdk'; + +import {PositionStrategy} from './position/position-strategy'; +import {ScrollStrategy} from './scroll/scroll-strategy'; + + +/** + * OverlayState is a bag of values for either the initial configuration or + * current state of an overlay. + */ +export class OverlayState { + /** Strategy with which to position the overlay. */ + positionStrategy: PositionStrategy; + + /** Strategy to be used when handling scroll events while the overlay is open. + */ + scrollStrategy: ScrollStrategy; + + /** Custom class to add to the overlay pane. */ + panelClass = ''; + + /** Whether the overlay has a backdrop. */ + hasBackdrop = false; + + /** Custom class to add to the backdrop */ + backdropClass = 'cdk-overlay-dark-backdrop'; + + /** The width of the overlay panel. If a number is provided, pixel units are + * assumed. */ + width?: number|string; + + /** The height of the overlay panel. If a number is provided, pixel units are + * assumed. */ + height?: number|string; + + /** The min-width of the overlay panel. If a number is provided, pixel units + * are assumed. */ + minWidth?: number|string; + + /** The min-height of the overlay panel. If a number is provided, pixel units + * are assumed. */ + minHeight?: number|string; + + /** The direction of the text in the overlay panel. */ + direction?: Direction = 'ltr'; + + // TODO(jelbourn): configuration still to add + // - focus trap + // - disable pointer events + // - z-index +} diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/overlay.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/overlay.ts new file mode 100644 index 00000000..5995201b --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/overlay.ts @@ -0,0 +1,109 @@ +/** + * @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*/ +import {DomPortalHost} from '@angular/cdk'; +import {ApplicationRef, ComponentFactoryResolver, Injectable, Injector, NgZone} from '@angular/core'; + +import {OverlayContainer} from './overlay-container'; +import {OverlayRef} from './overlay-ref'; +import {OverlayState} from './overlay-state'; +import {OverlayPositionBuilder} from './position/overlay-position-builder'; +import {ScrollStrategyOptions} from './scroll/index'; + + +/** Next overlay unique ID. */ +let nextUniqueId = 0; + +/** The default state for newly created overlays. */ +const defaultState = new OverlayState(); + + +/** + * Service to create Overlays. Overlays are dynamically added pieces of floating + * UI, meant to be used as a low-level building building block for other + * components. Dialogs, tooltips, menus, selects, etc. can all be built using + * overlays. The service should primarily be used by authors of re-usable + * components rather than developers building end-user applications. + * + * An overlay *is* a PortalHost, so any kind of Portal can be loaded into one. + */ +@Injectable() +export class Overlay { + constructor( + public scrollStrategies: ScrollStrategyOptions, + private _overlayContainer: OverlayContainer, + private _componentFactoryResolver: ComponentFactoryResolver, + private _positionBuilder: OverlayPositionBuilder, + private _appRef: ApplicationRef, private _injector: Injector, + private _ngZone: NgZone) {} + + /** + * Creates an overlay. + * @param state State to apply to the overlay. + * @returns Reference to the created overlay. + */ + create(state: OverlayState = defaultState, paneClassName?: string): + OverlayRef { + return this._createOverlayRef( + this._createPaneElement(paneClassName), state); + } + + /** + * Returns a position builder that can be used, via fluent API, + * to construct and configure a position strategy. + */ + position(): OverlayPositionBuilder { + return this._positionBuilder; + } + + /** + * Creates the DOM element for an overlay and appends it to the overlay + * container. + * @returns Newly-created pane element + */ + private _createPaneElement(className?: string): HTMLElement { + const pane = document.createElement('div'); + + pane.id = `nz-overlay-${nextUniqueId++}`; + pane.classList.add('nz-overlay-pane'); + if (className) { + const classList = className.split(' '); + classList.forEach(c => { + pane.classList.add(c); + }); + } + this._overlayContainer.getContainerElement().appendChild(pane); + + return pane; + } + + /** + * Create a DomPortalHost into which the overlay content can be loaded. + * @param pane The DOM element to turn into a portal host. + * @returns A portal host for the given DOM element. + */ + private _createPortalHost(pane: HTMLElement): DomPortalHost { + return new DomPortalHost( + pane, this._componentFactoryResolver, this._appRef, this._injector); + } + + /** + * Creates an OverlayRef for an overlay in the given DOM element. + * @param pane DOM element for the overlay + * @param state + */ + private _createOverlayRef(pane: HTMLElement, state: OverlayState): + OverlayRef { + const scrollStrategy = state.scrollStrategy || this.scrollStrategies.noop(); + const portalHost = this._createPortalHost(pane); + return new OverlayRef( + portalHost, pane, state, scrollStrategy, this._ngZone); + } +} diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/connected-position-strategy.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/connected-position-strategy.ts new file mode 100644 index 00000000..d144c81f --- /dev/null +++ b/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<ConnectedOverlayPositionChange> = + new Subject<ConnectedOverlayPositionChange>(); + + /** Emits an event when the connection point changes. */ + get onPositionChange(): Observable<ConnectedOverlayPositionChange> { + 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; +} diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/connected-position.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/connected-position.ts new file mode 100644 index 00000000..dad3f04e --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/connected-position.ts @@ -0,0 +1,87 @@ +/** + * @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*/ +/** Horizontal dimension of a connection point on the perimeter of the origin or + * overlay element. */ +import {Optional} from '@angular/core'; +export type HorizontalConnectionPos = 'start' | 'center' | 'end'; + +/** Vertical dimension of a connection point on the perimeter of the origin or + * overlay element. */ +export type VerticalConnectionPos = 'top' | 'center' | 'bottom'; + + +/** A connection point on the origin element. */ +export interface OriginConnectionPosition { + originX: HorizontalConnectionPos; + originY: VerticalConnectionPos; +} + +/** A connection point on the overlay element. */ +export interface OverlayConnectionPosition { + overlayX: HorizontalConnectionPos; + overlayY: VerticalConnectionPos; +} + +/** The points of the origin element and the overlay element to connect. */ +export class ConnectionPositionPair { + originX: HorizontalConnectionPos; + originY: VerticalConnectionPos; + overlayX: HorizontalConnectionPos; + overlayY: VerticalConnectionPos; + + constructor( + origin: OriginConnectionPosition, overlay: OverlayConnectionPosition) { + this.originX = origin.originX; + this.originY = origin.originY; + this.overlayX = overlay.overlayX; + this.overlayY = overlay.overlayY; + } +} + +/** + * Set of properties regarding the position of the origin and overlay relative + * to the viewport with respect to the containing Scrollable elements. + * + * The overlay and origin are clipped if any part of their bounding client + * rectangle exceeds the bounds of any one of the strategy's Scrollable's + * bounding client rectangle. + * + * The overlay and origin are outside view if there is no overlap between their + * bounding client rectangle and any one of the strategy's Scrollable's bounding + * client rectangle. + * + * ----------- ----------- + * | outside | | clipped | + * | view | -------------------------- + * | | | | | | + * ---------- | ----------- | + * -------------------------- | | + * | | | Scrollable | + * | | | | + * | | -------------------------- + * | Scrollable | + * | | + * -------------------------- + */ +export class ScrollableViewProperties { + isOriginClipped: boolean; + isOriginOutsideView: boolean; + isOverlayClipped: boolean; + isOverlayOutsideView: boolean; +} + +/** The change event emitted by the strategy when a fallback position is used. + */ +export class ConnectedOverlayPositionChange { + constructor( + public connectionPair: ConnectionPositionPair, + @Optional() public scrollableViewProperties: ScrollableViewProperties) {} +} diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/fake-viewport-ruler.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/fake-viewport-ruler.ts new file mode 100644 index 00000000..698aed1d --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/fake-viewport-ruler.ts @@ -0,0 +1,25 @@ +/** + * @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 + */ + +/** @docs-private */ +export class FakeViewportRuler { + getViewportRect() { + return { + left: 0, + top: 0, + width: 1014, + height: 686, + bottom: 686, + right: 1014 + }; + } + + getViewportScrollPosition() { + return {top: 0, left: 0}; + } +} diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/free-position-strategy.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/free-position-strategy.ts new file mode 100644 index 00000000..bd33e404 --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/free-position-strategy.ts @@ -0,0 +1,83 @@ +/* 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*/ +import {PositionStrategy} from './position-strategy'; + +/** + * Free position strategy for overlay without origin + * @author lingyi.zcs + */ +export class FreePositionStrategy implements PositionStrategy { + private _wrapper: HTMLElement; + // private _cssPosition: string = ''; + // private _top: string = ''; + // private _left: string = ''; + // private _width: string = ''; + // private _height: string = ''; + + // cssPosition(value: string) { + // this._cssPosition = value; + // return this; + // } + + // top(value: number | string): this { + // this._top = this._toCssValue(value); + // return this; + // } + + // left(value: number | string): this { + // this._left = this._toCssValue(value); + // return this; + // } + + // width(value: number | string): this { + // this._width = this._toCssValue(value); + // return this; + // } + + // height(value: number | string): this { + // this._height = this._toCssValue(value); + // return this; + // } + + /** + * Apply the position to the element. (NOTE: normally will triggered by + * scrolling) + * @docs-private + * + * @param element Element to which to apply the CSS. + * @returns Resolved when the styles have been applied. + */ + apply(element: HTMLElement): void { + if (!this._wrapper) { + this._wrapper = document.createElement('div'); + this._wrapper.classList.add('cdk-free-overlay-wrapper'); + element.parentNode.insertBefore(this._wrapper, element); + this._wrapper.appendChild(element); + + // // Initialized style once + // const style = element.style; + // style.position = this._cssPosition; + // style.top = this._top; + // style.left = this._left; + // style.width = this._width; + // style.height = this._height; + } + + // TODO: do somethings while triggered (eg. by scrolling) + } + + /** + * Removes the wrapper element from the DOM. + */ + dispose(): void { + if (this._wrapper && this._wrapper.parentNode) { + this._wrapper.parentNode.removeChild(this._wrapper); + this._wrapper = null; + } + } + + // private _toCssValue(value: number | string) { + // return typeof value === 'number' ? value + 'px' : value; + // } +} diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/global-position-strategy.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/global-position-strategy.ts new file mode 100644 index 00000000..8e3204a9 --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/global-position-strategy.ts @@ -0,0 +1,178 @@ +/** + * @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*/ +import {PositionStrategy} from './position-strategy'; + + +/** + * A strategy for positioning overlays. Using this strategy, an overlay is given + * an explicit position relative to the browser's viewport. We use flexbox, + * instead of transforms, in order to avoid issues with subpixel rendering which + * can cause the element to become blurry. + */ +export class GlobalPositionStrategy implements PositionStrategy { + private _cssPosition = 'static'; + private _topOffset = ''; + private _bottomOffset = ''; + private _leftOffset = ''; + private _rightOffset = ''; + private _alignItems = ''; + private _justifyContent = ''; + private _width = ''; + private _height = ''; + + /* A lazily-created wrapper for the overlay element that is used as a flex + * container. */ + private _wrapper: HTMLElement|null = null; + + /** + * Sets the top position of the overlay. Clears any previously set vertical + * position. + * @param value New top offset. + */ + top(value = ''): this { + this._bottomOffset = ''; + this._topOffset = value; + this._alignItems = 'flex-start'; + return this; + } + + /** + * Sets the left position of the overlay. Clears any previously set horizontal + * position. + * @param value New left offset. + */ + left(value = ''): this { + this._rightOffset = ''; + this._leftOffset = value; + this._justifyContent = 'flex-start'; + return this; + } + + /** + * Sets the bottom position of the overlay. Clears any previously set vertical + * position. + * @param value New bottom offset. + */ + bottom(value = ''): this { + this._topOffset = ''; + this._bottomOffset = value; + this._alignItems = 'flex-end'; + return this; + } + + /** + * Sets the right position of the overlay. Clears any previously set + * horizontal position. + * @param value New right offset. + */ + right(value = ''): this { + this._leftOffset = ''; + this._rightOffset = value; + this._justifyContent = 'flex-end'; + return this; + } + + /** + * Sets the overlay width and clears any previously set width. + * @param value New width for the overlay + */ + width(value = ''): this { + this._width = value; + + // When the width is 100%, we should reset the `left` and the offset, + // in order to ensure that the element is flush against the viewport edge. + if (value === '100%') { + this.left('0px'); + } + + return this; + } + + /** + * Sets the overlay height and clears any previously set height. + * @param value New height for the overlay + */ + height(value = ''): this { + this._height = value; + + // When the height is 100%, we should reset the `top` and the offset, + // in order to ensure that the element is flush against the viewport edge. + if (value === '100%') { + this.top('0px'); + } + + return this; + } + + /** + * Centers the overlay horizontally with an optional offset. + * Clears any previously set horizontal position. + * + * @param offset Overlay offset from the horizontal center. + */ + centerHorizontally(offset = ''): this { + this.left(offset); + this._justifyContent = 'center'; + return this; + } + + /** + * Centers the overlay vertically with an optional offset. + * Clears any previously set vertical position. + * + * @param offset Overlay offset from the vertical center. + */ + centerVertically(offset = ''): this { + this.top(offset); + this._alignItems = 'center'; + return this; + } + + /** + * Apply the position to the element. + * @docs-private + * + * @param element Element to which to apply the CSS. + * @returns Resolved when the styles have been applied. + */ + apply(element: HTMLElement): void { + if (!this._wrapper && element.parentNode) { + this._wrapper = document.createElement('div'); + this._wrapper.classList.add('cdk-global-overlay-wrapper'); + element.parentNode.insertBefore(this._wrapper, element); + this._wrapper.appendChild(element); + } + + const styles = element.style; + const parentStyles = (element.parentNode as HTMLElement).style; + + styles.position = this._cssPosition; + styles.marginTop = this._topOffset; + styles.marginLeft = this._leftOffset; + styles.marginBottom = this._bottomOffset; + styles.marginRight = this._rightOffset; + styles.width = this._width; + styles.height = this._height; + + parentStyles.justifyContent = this._justifyContent; + parentStyles.alignItems = this._alignItems; + } + + /** + * Removes the wrapper element from the DOM. + */ + dispose(): void { + if (this._wrapper && this._wrapper.parentNode) { + this._wrapper.parentNode.removeChild(this._wrapper); + this._wrapper = null; + } + } +} diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/overlay-position-builder.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/overlay-position-builder.ts new file mode 100644 index 00000000..0f6735eb --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/overlay-position-builder.ts @@ -0,0 +1,51 @@ +/** + * @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*/ +import {ElementRef, Injectable} from '@angular/core'; + +import {OriginConnectionPosition, OverlayConnectionPosition} from './connected-position'; +import {ConnectedPositionStrategy} from './connected-position-strategy'; +import {FreePositionStrategy} from './free-position-strategy'; +import {GlobalPositionStrategy} from './global-position-strategy'; +import {ViewportRuler} from './viewport-ruler'; + + +/** Builder for overlay position strategy. */ +@Injectable() +export class OverlayPositionBuilder { + constructor(private _viewportRuler: ViewportRuler) {} + + /** + * Creates a free position strategy + */ + free(): FreePositionStrategy { + return new FreePositionStrategy(); + } + + /** + * Creates a global position strategy. + */ + global(): GlobalPositionStrategy { + return new GlobalPositionStrategy(); + } + + /** + * Creates a relative position strategy. + * @param elementRef + * @param originPos + * @param overlayPos + */ + connectedTo( + elementRef: ElementRef, originPos: OriginConnectionPosition, + overlayPos: OverlayConnectionPosition): ConnectedPositionStrategy { + return new ConnectedPositionStrategy( + elementRef, originPos, overlayPos, this._viewportRuler); + } +} diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/position-strategy.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/position-strategy.ts new file mode 100644 index 00000000..21bd0fa0 --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/position-strategy.ts @@ -0,0 +1,17 @@ +/** + * @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 + */ + +/** Strategy for setting the position on an overlay. */ +export interface PositionStrategy { + /** Updates the position of the overlay element. */ + apply(element: Element): void; + + /** Cleans up any DOM modifications made by the position strategy, if + * necessary. */ + dispose(): void; +} diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/viewport-ruler.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/viewport-ruler.ts new file mode 100644 index 00000000..298cd642 --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/viewport-ruler.ts @@ -0,0 +1,110 @@ +/** + * @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*/ +import {Injectable, Optional, SkipSelf} from '@angular/core'; +import {ScrollDispatcher} from '../scroll/scroll-dispatcher'; + + +/** + * Simple utility for getting the bounds of the browser viewport. + * @docs-private + */ +@Injectable() +export class ViewportRuler { + /** Cached document client rectangle. */ + private _documentRect?: ClientRect; + + constructor(scrollDispatcher: ScrollDispatcher) { + // Subscribe to scroll and resize events and update the document rectangle + // on changes. + scrollDispatcher.scrolled(0, () => this._cacheViewportGeometry()); + } + + /** Gets a ClientRect for the viewport's bounds. */ + getViewportRect(documentRect = this._documentRect): ClientRect { + // Cache the document bounding rect so that we don't recompute it for + // multiple calls. + if (!documentRect) { + this._cacheViewportGeometry(); + documentRect = this._documentRect; + } + + // Use the document element's bounding rect rather than the window scroll + // properties (e.g. pageYOffset, scrollY) due to in issue in Chrome and IE + // where window scroll properties and client coordinates + // (boundingClientRect, clientX/Y, etc.) are in different conceptual + // viewports. Under most circumstances these viewports are equivalent, but + // they can disagree when the page is pinch-zoomed (on devices that support + // touch). See + // https://bugs.chromium.org/p/chromium/issues/detail?id=489206#c4 We use + // the documentElement instead of the body because, by default (without a + // css reset) browsers typically give the document body an 8px margin, which + // is not included in getBoundingClientRect(). + const scrollPosition = this.getViewportScrollPosition(documentRect); + const height = window.innerHeight; + const width = window.innerWidth; + + return { + top: scrollPosition.top, + left: scrollPosition.left, + bottom: scrollPosition.top + height, + right: scrollPosition.left + width, + height, + width, + }; + } + + + /** + * Gets the (top, left) scroll position of the viewport. + * @param documentRect + */ + getViewportScrollPosition(documentRect = this._documentRect) { + // Cache the document bounding rect so that we don't recompute it for + // multiple calls. + if (!documentRect) { + this._cacheViewportGeometry(); + documentRect = this._documentRect; + } + + // The top-left-corner of the viewport is determined by the scroll position + // of the document body, normally just (scrollLeft, scrollTop). However, + // Chrome and Firefox disagree about whether `document.body` or + // `document.documentElement` is the scrolled element, so reading + // `scrollTop` and `scrollLeft` is inconsistent. However, using the bounding + // rect of `document.documentElement` works consistently, where the `top` + // and `left` values will equal negative the scroll position. + const top = -documentRect.top || document.body.scrollTop || + window.scrollY || document.documentElement.scrollTop || 0; + + const left = -documentRect.left || document.body.scrollLeft || + window.scrollX || document.documentElement.scrollLeft || 0; + + return {top, left}; + } + + /** Caches the latest client rectangle of the document element. */ + _cacheViewportGeometry() { + this._documentRect = document.documentElement.getBoundingClientRect(); + } +} + +export function VIEWPORT_RULER_PROVIDER_FACTORY( + parentRuler: ViewportRuler, scrollDispatcher: ScrollDispatcher) { + return parentRuler || new ViewportRuler(scrollDispatcher); +} + +export const VIEWPORT_RULER_PROVIDER = { + // If there is already a ViewportRuler available, use that. Otherwise, provide + // a new one. + provide: ViewportRuler, + deps: [[new Optional(), new SkipSelf(), ViewportRuler], ScrollDispatcher], + useFactory: VIEWPORT_RULER_PROVIDER_FACTORY +}; diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/block-scroll-strategy.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/block-scroll-strategy.ts new file mode 100644 index 00000000..d1c1d401 --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/block-scroll-strategy.ts @@ -0,0 +1,77 @@ +/** + * @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*/ +import {ViewportRuler} from '../position/viewport-ruler'; + +import {ScrollStrategy} from './scroll-strategy'; + +/** + * Strategy that will prevent the user from scrolling while the overlay is + * visible. + */ +export class BlockScrollStrategy implements ScrollStrategy { + private _previousHTMLStyles = {top: '', left: ''}; + private _previousScrollPosition: {top: number, left: number}; + private _isEnabled = false; + + constructor(private _viewportRuler: ViewportRuler) {} + + attach() { + // + } + + enable() { + if (this._canBeEnabled()) { + const root = document.documentElement; + + this._previousScrollPosition = + this._viewportRuler.getViewportScrollPosition(); + + // Cache the previous inline styles in case the user had set them. + this._previousHTMLStyles.left = root.style.left || ''; + this._previousHTMLStyles.top = root.style.top || ''; + + // Note: we're using the `html` node, instead of the `body`, because the + // `body` may have the user agent margin, whereas the `html` is guaranteed + // not to have one. + root.style.left = `${- this._previousScrollPosition.left}px`; + root.style.top = `${- this._previousScrollPosition.top}px`; + root.classList.add('cdk-global-scrollblock'); + this._isEnabled = true; + } + } + + disable() { + if (this._isEnabled) { + this._isEnabled = false; + document.documentElement.style.left = this._previousHTMLStyles.left; + document.documentElement.style.top = this._previousHTMLStyles.top; + document.documentElement.classList.remove('cdk-global-scrollblock'); + window.scroll( + this._previousScrollPosition.left, this._previousScrollPosition.top); + } + } + + private _canBeEnabled(): boolean { + // Since the scroll strategies can't be singletons, we have to use a global + // CSS class + // (`cdk-global-scrollblock`) to make sure that we don't try to disable + // global scrolling multiple times. + if (document.documentElement.classList.contains('cdk-global-scrollblock') || + this._isEnabled) { + return false; + } + + const body = document.body; + const viewport = this._viewportRuler.getViewportRect(); + return body.scrollHeight > viewport.height || + body.scrollWidth > viewport.width; + } +} diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/close-scroll-strategy.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/close-scroll-strategy.ts new file mode 100644 index 00000000..51189dc1 --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/close-scroll-strategy.ts @@ -0,0 +1,54 @@ +/** + * @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*/ +import {Subscription} from 'rxjs/Subscription'; + +import {OverlayRef} from '../overlay-ref'; + +import {ScrollDispatcher} from './scroll-dispatcher'; +import {getMdScrollStrategyAlreadyAttachedError, ScrollStrategy} from './scroll-strategy'; + + +/** + * Strategy that will close the overlay as soon as the user starts scrolling. + */ +export class CloseScrollStrategy implements ScrollStrategy { + private _scrollSubscription: Subscription|null = null; + private _overlayRef: OverlayRef; + + constructor(private _scrollDispatcher: ScrollDispatcher) {} + + attach(overlayRef: OverlayRef) { + if (this._overlayRef) { + throw getMdScrollStrategyAlreadyAttachedError(); + } + + this._overlayRef = overlayRef; + } + + enable() { + if (!this._scrollSubscription) { + this._scrollSubscription = this._scrollDispatcher.scrolled(0, () => { + if (this._overlayRef.hasAttached()) { + this._overlayRef.detach(); + } + + this.disable(); + }); + } + } + + disable() { + if (this._scrollSubscription) { + this._scrollSubscription.unsubscribe(); + this._scrollSubscription = null; + } + } +} diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/index.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/index.ts new file mode 100644 index 00000000..e386770b --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/index.ts @@ -0,0 +1,35 @@ +/** + * @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*/ +import {PlatformModule} from '@angular/cdk'; +import {NgModule} from '@angular/core'; + +import {SCROLL_DISPATCHER_PROVIDER} from './scroll-dispatcher'; +import {ScrollStrategyOptions} from './scroll-strategy-options'; +import {Scrollable} from './scrollable'; + +export {BlockScrollStrategy} from './block-scroll-strategy'; +export {CloseScrollStrategy} from './close-scroll-strategy'; +export {NoopScrollStrategy} from './noop-scroll-strategy'; +export {RepositionScrollStrategy} from './reposition-scroll-strategy'; +export {ScrollDispatcher} from './scroll-dispatcher'; +// Export pre-defined scroll strategies and interface to build custom ones. +export {ScrollStrategy} from './scroll-strategy'; +export {ScrollStrategyOptions} from './scroll-strategy-options'; +export {Scrollable} from './scrollable'; + +@NgModule({ + imports: [PlatformModule], + exports: [Scrollable], + declarations: [Scrollable], + providers: [SCROLL_DISPATCHER_PROVIDER, ScrollStrategyOptions], +}) +export class ScrollDispatchModule { +} diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/noop-scroll-strategy.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/noop-scroll-strategy.ts new file mode 100644 index 00000000..9b92ab49 --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/noop-scroll-strategy.ts @@ -0,0 +1,24 @@ +/** + * @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 + */ + +import {ScrollStrategy} from './scroll-strategy'; + +/** + * Scroll strategy that doesn't do anything. + */ +export class NoopScrollStrategy implements ScrollStrategy { + enable() { + // + } + disable() { + // + } + attach() { + // + } +} diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/reposition-scroll-strategy.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/reposition-scroll-strategy.ts new file mode 100644 index 00000000..b15d5dea --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/reposition-scroll-strategy.ts @@ -0,0 +1,59 @@ +/** + * @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*/ +import {Subscription} from 'rxjs/Subscription'; + +import {OverlayRef} from '../overlay-ref'; + +import {ScrollDispatcher} from './scroll-dispatcher'; +import {getMdScrollStrategyAlreadyAttachedError, ScrollStrategy} from './scroll-strategy'; + +/** + * Config options for the RepositionScrollStrategy. + */ +export interface RepositionScrollStrategyConfig { scrollThrottle?: number; } + +/** + * Strategy that will update the element position as the user is scrolling. + */ +export class RepositionScrollStrategy implements ScrollStrategy { + private _scrollSubscription: Subscription|null = null; + private _overlayRef: OverlayRef; + + constructor( + private _scrollDispatcher: ScrollDispatcher, + private _config?: RepositionScrollStrategyConfig) {} + + attach(overlayRef: OverlayRef) { + if (this._overlayRef) { + throw getMdScrollStrategyAlreadyAttachedError(); + } + + this._overlayRef = overlayRef; + } + + enable() { + if (!this._scrollSubscription) { + const throttle = this._config ? this._config.scrollThrottle : 0; + + this._scrollSubscription = + this._scrollDispatcher.scrolled(throttle, () => { + this._overlayRef.updatePosition(); + }); + } + } + + disable() { + if (this._scrollSubscription) { + this._scrollSubscription.unsubscribe(); + this._scrollSubscription = null; + } + } +} diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/scroll-dispatcher.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/scroll-dispatcher.ts new file mode 100644 index 00000000..2c145af5 --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/scroll-dispatcher.ts @@ -0,0 +1,174 @@ +/** + * @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*/ +import {Platform} from '@angular/cdk'; +import {auditTime} from 'rxjs/operator/auditTime'; +import {ElementRef, Injectable, NgZone, Optional, SkipSelf} from '@angular/core'; +import {fromEvent} from 'rxjs/observable/fromEvent'; +import {merge} from 'rxjs/observable/merge'; +import {Subject} from 'rxjs/Subject'; +import {Subscription} from 'rxjs/Subscription'; + +import {Scrollable} from './scrollable'; + + +/** Time in ms to throttle the scrolling events by default. */ +export const DEFAULT_SCROLL_TIME = 20; + +/** + * Service contained all registered Scrollable references and emits an event + * when any one of the Scrollable references emit a scrolled event. + */ +@Injectable() +export class ScrollDispatcher { + /** Subject for notifying that a registered scrollable reference element has + * been scrolled. */ + _scrolled: Subject<void> = new Subject<void>(); + + /** Keeps track of the global `scroll` and `resize` subscriptions. */ + _globalSubscription: Subscription|null = null; + + /** Keeps track of the amount of subscriptions to `scrolled`. Used for + * cleaning up afterwards. */ + private _scrolledCount = 0; + + /** + * Map of all the scrollable references that are registered with the service + * and their scroll event subscriptions. + */ + scrollableReferences: Map<Scrollable, Subscription> = new Map(); + + constructor(private _ngZone: NgZone, private _platform: Platform) {} + + /** + * Registers a Scrollable with the service and listens for its scrolled + * events. When the scrollable is scrolled, the service emits the event in its + * scrolled observable. + * @param scrollable Scrollable instance to be registered. + */ + register(scrollable: Scrollable): void { + const scrollSubscription = + scrollable.elementScrolled().subscribe(() => this._notify()); + + this.scrollableReferences.set(scrollable, scrollSubscription); + } + + /** + * Deregisters a Scrollable reference and unsubscribes from its scroll event + * observable. + * @param scrollable Scrollable instance to be deregistered. + */ + deregister(scrollable: Scrollable): void { + const scrollableReference = this.scrollableReferences.get(scrollable); + + if (scrollableReference) { + scrollableReference.unsubscribe(); + this.scrollableReferences.delete(scrollable); + } + } + + /** + * Subscribes to an observable that emits an event whenever any of the + * registered Scrollable references (or window, document, or body) fire a + * scrolled event. Can provide a time in ms to override the default "throttle" + * time. + */ + scrolled(auditTimeInMs: number = DEFAULT_SCROLL_TIME, callback: () => any): + Subscription { + // Scroll events can only happen on the browser, so do nothing if we're not + // on the browser. + if (!this._platform.isBrowser) { + return Subscription.EMPTY; + } + + // In the case of a 0ms delay, use an observable without auditTime + // since it does add a perceptible delay in processing overhead. + const observable = auditTimeInMs > 0 ? + auditTime.call(this._scrolled.asObservable(), auditTimeInMs) : + this._scrolled.asObservable(); + + this._scrolledCount++; + + if (!this._globalSubscription) { + this._globalSubscription = this._ngZone.runOutsideAngular(() => { + return merge( + fromEvent(window.document, 'scroll'), + fromEvent(window, 'resize')) + .subscribe(() => this._notify()); + }); + } + + // Note that we need to do the subscribing from here, in order to be able to + // remove the global event listeners once there are no more subscriptions. + const subscription = observable.subscribe(callback); + + subscription.add(() => { + this._scrolledCount--; + + if (this._globalSubscription && !this.scrollableReferences.size && + !this._scrolledCount) { + this._globalSubscription.unsubscribe(); + this._globalSubscription = null; + } + }); + + return subscription; + } + + /** Returns all registered Scrollables that contain the provided element. */ + getScrollContainers(elementRef: ElementRef): Scrollable[] { + const scrollingContainers: Scrollable[] = []; + + this.scrollableReferences.forEach( + (_subscription: Subscription, scrollable: Scrollable) => { + if (this.scrollableContainsElement(scrollable, elementRef)) { + scrollingContainers.push(scrollable); + } + }); + + return scrollingContainers; + } + + /** Returns true if the element is contained within the provided Scrollable. + */ + scrollableContainsElement(scrollable: Scrollable, elementRef: ElementRef): + boolean { + let element = elementRef.nativeElement; + const scrollableElement = scrollable.getElementRef().nativeElement; + + // Traverse through the element parents until we reach null, checking if any + // of the elements are the scrollable's element. + do { + if (element === scrollableElement) { + return true; + } + } while (element = element.parentElement); + + return false; + } + + /** Sends a notification that a scroll event has been fired. */ + _notify() { + this._scrolled.next(); + } +} + +export function SCROLL_DISPATCHER_PROVIDER_FACTORY( + parentDispatcher: ScrollDispatcher, ngZone: NgZone, platform: Platform) { + return parentDispatcher || new ScrollDispatcher(ngZone, platform); +} + +export const SCROLL_DISPATCHER_PROVIDER = { + // If there is already a ScrollDispatcher available, use that. Otherwise, + // provide a new one. + provide: ScrollDispatcher, + deps: [[new Optional(), new SkipSelf(), ScrollDispatcher], NgZone, Platform], + useFactory: SCROLL_DISPATCHER_PROVIDER_FACTORY +}; diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/scroll-strategy-options.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/scroll-strategy-options.ts new file mode 100644 index 00000000..f6270388 --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/scroll-strategy-options.ts @@ -0,0 +1,52 @@ +/** + * @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-unused-expression*/ +import {Injectable} from '@angular/core'; + +import {ViewportRuler} from '../position/viewport-ruler'; + +import {BlockScrollStrategy} from './block-scroll-strategy'; +import {CloseScrollStrategy} from './close-scroll-strategy'; +import {NoopScrollStrategy} from './noop-scroll-strategy'; +import {RepositionScrollStrategy, RepositionScrollStrategyConfig} from './reposition-scroll-strategy'; +import {ScrollDispatcher} from './scroll-dispatcher'; +// import {ScrollStrategy} from './scroll-strategy'; + + +/** + * Options for how an overlay will handle scrolling. + * + * Users can provide a custom value for `ScrollStrategyOptions` to replace the + * default behaviors. This class primarily acts as a factory for ScrollStrategy + * instances. + */ +@Injectable() +export class ScrollStrategyOptions { + constructor( + private _scrollDispatcher: ScrollDispatcher, + private _viewportRuler: ViewportRuler) {} + + /** Do nothing on scroll. */ + noop = () => new NoopScrollStrategy(); + + /** Close the overlay as soon as the user scrolls. */ + close = () => new CloseScrollStrategy(this._scrollDispatcher); + + /** Block scrolling. */ + block = () => new BlockScrollStrategy(this._viewportRuler); + + /** + * Update the overlay's position on scroll. + * @param config Configuration to be used inside the scroll strategy. + * Allows debouncing the reposition calls. + */ + reposition = (config?: RepositionScrollStrategyConfig) => + new RepositionScrollStrategy(this._scrollDispatcher, config) +} diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/scroll-strategy.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/scroll-strategy.ts new file mode 100644 index 00000000..d59651a7 --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/scroll-strategy.ts @@ -0,0 +1,29 @@ +/** + * @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*/ +import {OverlayRef} from '../overlay-ref'; + +/** + * Describes a strategy that will be used by an overlay + * to handle scroll events while it is open. + */ +export abstract class ScrollStrategy { + enable: () => void; + disable: () => void; + attach: (overlayRef: OverlayRef) => void; +} + +/** + * Returns an error to be thrown when attempting to attach an already-attached + * scroll strategy. + */ +export function getMdScrollStrategyAlreadyAttachedError(): Error { + return Error(`Scroll strategy has already been attached.`); +} diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/scrollable.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/scrollable.ts new file mode 100644 index 00000000..fe7b041c --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/scrollable.ts @@ -0,0 +1,63 @@ +/** + * @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*/ +import {Directive, ElementRef, NgZone, OnDestroy, OnInit, Renderer2} from '@angular/core'; +import {Observable} from 'rxjs/Observable'; +import {Subject} from 'rxjs/Subject'; + +import {ScrollDispatcher} from './scroll-dispatcher'; + + +/** + * Sends an event when the directive's element is scrolled. Registers itself + * with the ScrollDispatcher service to include itself as part of its collection + * of scrolling events that it can be listened to through the service. + */ +@Directive({selector: '[cdk-scrollable], [cdkScrollable]'}) +export class Scrollable implements OnInit, OnDestroy { + private _elementScrolled: Subject<Event> = new Subject(); + private _scrollListener: Function|null; + + constructor( + private _elementRef: ElementRef, private _scroll: ScrollDispatcher, + private _ngZone: NgZone, private _renderer: Renderer2) {} + + ngOnInit() { + this._scrollListener = this._ngZone.runOutsideAngular(() => { + return this._renderer.listen( + this.getElementRef().nativeElement, 'scroll', (event: Event) => { + this._elementScrolled.next(event); + }); + }); + + this._scroll.register(this); + } + + ngOnDestroy() { + this._scroll.deregister(this); + + if (this._scrollListener) { + this._scrollListener(); + this._scrollListener = null; + } + } + + /** + * Returns observable that emits when a scroll event is fired on the host + * element. + */ + elementScrolled(): Observable<any> { + return this._elementScrolled.asObservable(); + } + + getElementRef(): ElementRef { + return this._elementRef; + } +} diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlaypanel/index.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlaypanel/index.ts new file mode 100644 index 00000000..4ad2b4a3 --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlaypanel/index.ts @@ -0,0 +1 @@ +export * from './overlaypanel';
\ No newline at end of file diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/overlaypanel/overlaypanel.ts b/sdc-workflow-designer-ui/src/app/paletx/core/overlaypanel/overlaypanel.ts new file mode 100644 index 00000000..ee529c5f --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/overlaypanel/overlaypanel.ts @@ -0,0 +1,163 @@ +import {CommonModule} from '@angular/common'; +import {AfterViewInit, Component, ElementRef, EventEmitter, Input, NgModule, OnDestroy, OnInit, Output, Renderer} from '@angular/core'; + +import {DomHandler} from '../domhandler'; + +@Component({ + selector: 'plx-overlay-panel', + template: ` + <div [ngClass]="'ui-overlaypanel ui-widget ui-widget-content ui-corner-all ui-shadow'" [ngStyle]="style" [class]="styleClass" + [style.display]="visible ? 'block' : 'none'" (click)="onPanelClick()"> + <div class="ui-overlaypanel-content"> + <ng-content></ng-content> + </div> + <a href="#" *ngIf="showCloseIcon" class="ui-overlaypanel-close ui-state-default" (click)="onCloseClick($event)"> + <span class="fa fa-fw fa-close"></span> + </a> + </div> + `, + providers: [DomHandler] +}) +export class PlxOverlayPanelComponent implements OnInit, AfterViewInit, + OnDestroy { + @Input() dismissable: boolean = true; + + @Input() showCloseIcon: boolean; + + @Input() style: any; + + @Input() styleClass: string; + + @Input() appendTo: any; + + @Output() onBeforeShow: EventEmitter<any> = new EventEmitter(); + + @Output() onAfterShow: EventEmitter<any> = new EventEmitter(); + + @Output() onBeforeHide: EventEmitter<any> = new EventEmitter(); + + @Output() onAfterHide: EventEmitter<any> = new EventEmitter(); + + container: any; + + visible: boolean = false; + + documentClickListener: any; + + selfClick: boolean; + + targetEvent: boolean; + + target: any; + + constructor( + public el: ElementRef, public domHandler: DomHandler, + public renderer: Renderer) {} + + ngOnInit() { + if (this.dismissable) { + this.documentClickListener = + this.renderer.listenGlobal('body', 'click', () => { + if (!this.selfClick && !this.targetEvent) { + this.hide(); + } + this.selfClick = false; + this.targetEvent = false; + }); + } + } + + ngAfterViewInit() { + this.container = this.el.nativeElement.children[0]; + if (this.appendTo) { + if (this.appendTo === 'body') { + document.body.appendChild(this.container); + } else { + this.domHandler.appendChild(this.container, this.appendTo); + } + } + } + + toggle(event: any, target?: any) { + let currentTarget = (target || event.currentTarget || event.target); + + if (!this.target || this.target === currentTarget) { + if (this.visible) { + this.hide(); + } else { + this.show(event, target); + } + } else { + this.show(event, target); + } + + if (this.dismissable) { + this.targetEvent = true; + } + + this.target = currentTarget; + } + + show(event: any, target?: any) { + if (this.dismissable) { + this.targetEvent = true; + } + + this.onBeforeShow.emit(null); + let elementTarget = target || event.currentTarget || event.target; + this.container.style.zIndex = ++DomHandler.zindex; + + if (this.visible) { + this.domHandler.absolutePosition(this.container, elementTarget); + } else { + this.visible = true; + this.domHandler.absolutePosition(this.container, elementTarget); + this.domHandler.fadeIn(this.container, 250); + } + this.onAfterShow.emit(null); + } + + hide() { + if (this.visible) { + this.onBeforeHide.emit(null); + this.visible = false; + this.onAfterHide.emit(null); + } + } + + onPanelClick() { + if (this.dismissable) { + this.selfClick = true; + } + } + + onCloseClick(event: any) { + this.hide(); + + if (this.dismissable) { + this.selfClick = true; + } + + event.preventDefault(); + } + + ngOnDestroy() { + if (this.documentClickListener) { + this.documentClickListener(); + } + + if (this.appendTo) { + this.el.nativeElement.appendChild(this.container); + } + + this.target = null; + } +} + +@NgModule({ + imports: [CommonModule], + exports: [PlxOverlayPanelComponent], + declarations: [PlxOverlayPanelComponent] +}) +export class PlxOverlayPanelModule { +}
\ No newline at end of file diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/pxbutton/button-state.ts b/sdc-workflow-designer-ui/src/app/paletx/core/pxbutton/button-state.ts new file mode 100644 index 00000000..2f1f73b2 --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/pxbutton/button-state.ts @@ -0,0 +1,6 @@ +export enum PlxButtonState { + IDLE, + DOING, + SUCCESS, + FAILURE +}
\ No newline at end of file diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/pxbutton/button.directive.ts b/sdc-workflow-designer-ui/src/app/paletx/core/pxbutton/button.directive.ts new file mode 100644 index 00000000..842b9fb4 --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/pxbutton/button.directive.ts @@ -0,0 +1,178 @@ +import {AfterViewInit, Directive, ElementRef, Input, OnDestroy} from '@angular/core'; + +import {DomHandler} from '../domhandler'; + +import {PlxButtonState} from './button-state'; + +@Directive({selector: '[pxButton]', providers: [DomHandler]}) +export class PlxButtonDirective implements AfterViewInit, OnDestroy { + @Input() iconPos: string = 'left'; + + @Input() cornerStyleClass: string = 'ui-corner-all'; + + _label: string; + + _loadinglabel: string; + + _icon: string; + + _state: number; + + initialized: boolean; + + constructor( + public el: ElementRef, public domHandler: DomHandler) {} + + ngAfterViewInit() { + this.domHandler.addMultipleClasses( + this.el.nativeElement, this.getStyleClass()); + if (this.icon) { + let iconElement = document.createElement('span'); + let iconPosClass = (this.iconPos === 'right') ? 'ui-button-icon-right' : + 'ui-button-icon-left'; + iconElement.className = + iconPosClass + ' ui-c iconfont plx-icon-' + this.icon; + this.el.nativeElement.appendChild(iconElement); + } + + let iconAnimationElement = document.createElement('span'); + iconAnimationElement.className = + 'ui-button-icon-left ui-c iconfont plx-icon-circle-o-notch plx-spin'; + iconAnimationElement.style.display = 'none'; + this.el.nativeElement.appendChild(iconAnimationElement); + + let labelElement = document.createElement('span'); + labelElement.className = 'ui-button-text ui-c'; + labelElement.appendChild(document.createTextNode(this.label || '')); + this.el.nativeElement.appendChild(labelElement); + + if (this.state) { + let spanElement = + this.domHandler.findSingle(this.el.nativeElement, '.ui-button-text'); + if (this.state === PlxButtonState.DOING) { + if (spanElement) { + spanElement.innerText = this.loadinglabel || 'loading'; + } + this.el.nativeElement.disabled = true; + this.setIconELement(true); + } else { + spanElement.innerText = this.label || ''; + this.el.nativeElement.disabled = false; + this.setIconELement(false); + } + } + + this.initialized = true; + } + + getStyleClass(): string { + let styleClass = + 'ui-button ui-widget ui-state-default ' + this.cornerStyleClass; + if (this.icon) { + if (this.label !== null && this.label !== undefined) { + if (this.iconPos === 'left') { + styleClass = styleClass + ' ui-button-text-icon-left'; + } else { + styleClass = styleClass + ' ui-button-text-icon-right'; + } + } else { + styleClass = styleClass + ' ui-button-icon-only'; + } + } else { + styleClass = styleClass + ' ui-button-text-only'; + } + + return styleClass; + } + + setIconELement(isShowAnimation: boolean) { + let iconLeftElement = this.domHandler.findSingle( + this.el.nativeElement, '.ui-button-icon-left.iconfont'); + if (iconLeftElement) { + iconLeftElement.style.display = isShowAnimation ? 'none' : 'inline-block'; + } + let iconRightElement = this.domHandler.findSingle( + this.el.nativeElement, '.ui-button-icon-left.iconfont'); + if (iconRightElement) { + iconRightElement.style.display = + isShowAnimation ? 'none' : 'inline-block'; + } + let iconAnimationElement = this.domHandler.findSingle( + this.el.nativeElement, '.iconfont.plx-icon-circle-o-notch.plx-spin'); + if (iconAnimationElement) { + iconAnimationElement.style.display = + isShowAnimation ? 'inline-block' : 'none'; + } + } + + @Input() + get label(): string { + return this._label; + } + + set label(val: string) { + this._label = val; + + if (this.initialized) { + this.domHandler.findSingle(this.el.nativeElement, '.ui-button-text') + .textContent = this._label; + } + } + + @Input() + get loadinglabel(): string { + return this._loadinglabel; + } + + set loadinglabel(val: string) { + this._loadinglabel = val; + } + + @Input() + get icon(): string { + return this._icon; + } + + set icon(val: string) { + this._icon = val; + + if (this.initialized) { + let iconPosClass = (this.iconPos === 'right') ? 'ui-button-icon-right' : + 'ui-button-icon-left'; + this.domHandler.findSingle(this.el.nativeElement, '.iconfont').className = + iconPosClass + ' ui-c iconfont plx-icon-' + this.icon; + } + } + + @Input() + get state(): number { + return this._state; + } + + set state(val: number) { + this._state = val; + if (this.initialized) { + let spanElement = + this.domHandler.findSingle(this.el.nativeElement, '.ui-button-text'); + if (this.state === PlxButtonState.DOING) { + if (spanElement) { + spanElement.innerText = this.loadinglabel || 'loading'; + } + this.el.nativeElement.disabled = true; + this.setIconELement(true); + } else { + spanElement.innerText = this.label || ''; + this.el.nativeElement.disabled = false; + this.setIconELement(false); + } + } + } + + ngOnDestroy() { + while (this.el.nativeElement.hasChildNodes()) { + this.el.nativeElement.removeChild(this.el.nativeElement.lastChild); + } + + this.initialized = false; + } +} diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/pxbutton/button.module.ts b/sdc-workflow-designer-ui/src/app/paletx/core/pxbutton/button.module.ts new file mode 100644 index 00000000..01973295 --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/pxbutton/button.module.ts @@ -0,0 +1,14 @@ +/** + * Created by 10190264 on 2016/12/15. + */ +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {PlxButtonDirective} from './button.directive'; + +@NgModule({ + imports: [CommonModule], + exports: [PlxButtonDirective], + declarations: [PlxButtonDirective] +}) +export class PlxButtonModule { +}
\ No newline at end of file diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/pxbutton/index.ts b/sdc-workflow-designer-ui/src/app/paletx/core/pxbutton/index.ts new file mode 100644 index 00000000..f435b247 --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/pxbutton/index.ts @@ -0,0 +1,2 @@ +export * from './button.module'; +export * from './button-state';
\ No newline at end of file diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/select.service.ts b/sdc-workflow-designer-ui/src/app/paletx/core/select.service.ts new file mode 100644 index 00000000..ba6f579e --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/select.service.ts @@ -0,0 +1,57 @@ +/* tslint:disable:array-type member-access variable-name */ +import {Injectable} from '@angular/core'; + +@Injectable() +export class SelectService { + selection: string[] = []; + + selected(indexName: string): boolean { + if (this.selection === undefined || this.selection === []) { + return null; + } + + for (let item of this.selection) { + if (item === indexName) { + return true; + } + } + return false; + } + + handleSingleSelect(optionIndex: string) { + this.selection = []; + this.selection.push(optionIndex); + return this.selection; + } + + handleMutipleSelect(optionIndex: string) { + if (this.selected(optionIndex)) { + this.selection = this.handleSecondSelect(optionIndex); + } else { + this.selection.push(optionIndex); + } + return this.selection; + } + + handleSecondSelect(optionIndex: string) { + let selectedOption = []; + for (let option of this.selection) { + if (option !== optionIndex) { + selectedOption.push(option); + } + } + return selectedOption; + } + + select(optionIndex: string, isMutiple: boolean): string[] { + if (!isMutiple) { + return this.handleSingleSelect(optionIndex); + } else { + return this.handleMutipleSelect(optionIndex); + } + } + + deselect() { + this.selection = []; + } +} diff --git a/sdc-workflow-designer-ui/src/app/paletx/core/uuid.ts b/sdc-workflow-designer-ui/src/app/paletx/core/uuid.ts new file mode 100644 index 00000000..58756b63 --- /dev/null +++ b/sdc-workflow-designer-ui/src/app/paletx/core/uuid.ts @@ -0,0 +1,36 @@ +/* tslint:disable:array-type member-access variable-name */ +export class UUID { + constructor() { + // + } + + static UUID(): string { + if (typeof(window.crypto) !== 'undefined' && + typeof(window.crypto.getRandomValues) !== 'undefined') { + // If we have a cryptographically secure PRNG, use that + let buf: Uint16Array = new Uint16Array(8); + window.crypto.getRandomValues(buf); + return ( + this.pad4(buf[0]) + this.pad4(buf[1]) + '-' + this.pad4(buf[2]) + + '-' + this.pad4(buf[3]) + '-' + this.pad4(buf[4]) + '-' + + this.pad4(buf[5]) + this.pad4(buf[6]) + this.pad4(buf[7])); + } else { + // Otherwise, just use Math.random + return this.random4() + this.random4() + '-' + this.random4() + '-' + + this.random4() + '-' + this.random4() + '-' + this.random4() + + this.random4() + this.random4(); + } + } + + private static pad4(num: number): string { + let ret: string = num.toString(16); + while (ret.length < 4) { + ret = '0' + ret; + } + return ret; + } + + private static random4(): string { + return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); + } +} |