aboutsummaryrefslogtreecommitdiffstats
path: root/sdc-workflow-designer-ui
diff options
context:
space:
mode:
Diffstat (limited to 'sdc-workflow-designer-ui')
-rw-r--r--sdc-workflow-designer-ui/package-lock.json51
-rw-r--r--sdc-workflow-designer-ui/package.json6
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/boolean-field-value.ts19
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/domhandler.ts432
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/number-wrapper-parse.ts10
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/fullscreen-overlay-container.ts62
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/generic-component-type.ts9
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/index.ts49
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/overlay-container.ts83
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/overlay-directives.ts329
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/overlay-position-map.ts124
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/overlay-ref.ts271
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/overlay-state.ts61
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/overlay.ts109
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/connected-position-strategy.ts478
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/connected-position.ts87
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/fake-viewport-ruler.ts25
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/free-position-strategy.ts83
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/global-position-strategy.ts178
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/overlay-position-builder.ts51
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/position-strategy.ts17
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/position/viewport-ruler.ts110
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/block-scroll-strategy.ts77
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/close-scroll-strategy.ts54
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/index.ts35
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/noop-scroll-strategy.ts24
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/reposition-scroll-strategy.ts59
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/scroll-dispatcher.ts174
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/scroll-strategy-options.ts52
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/scroll-strategy.ts29
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlay/scroll/scrollable.ts63
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlaypanel/index.ts1
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/overlaypanel/overlaypanel.ts163
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/pxbutton/button-state.ts6
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/pxbutton/button.directive.ts178
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/pxbutton/button.module.ts14
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/pxbutton/index.ts2
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/select.service.ts57
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/core/uuid.ts36
39 files changed, 3659 insertions, 9 deletions
diff --git a/sdc-workflow-designer-ui/package-lock.json b/sdc-workflow-designer-ui/package-lock.json
index bd501c05..a7015452 100644
--- a/sdc-workflow-designer-ui/package-lock.json
+++ b/sdc-workflow-designer-ui/package-lock.json
@@ -23,6 +23,14 @@
"tslib": "1.7.1"
}
},
+ "@angular/cdk": {
+ "version": "2.0.0-beta.8",
+ "resolved": "http://registry.npm.taobao.org/@angular/cdk/download/@angular/cdk-2.0.0-beta.8.tgz",
+ "integrity": "sha1-cZYchR376xngheiYv15EYUCPi1c=",
+ "requires": {
+ "tslib": "1.7.1"
+ }
+ },
"@angular/cli": {
"version": "1.3.1",
"resolved": "http://registry.npm.taobao.org/@angular/cli/download/@angular/cli-1.3.1.tgz",
@@ -87,7 +95,6 @@
"typescript": "2.3.4",
"url-loader": "0.5.9",
"walk-sync": "0.3.2",
- "webpack": "3.4.1",
"webpack-dev-middleware": "1.12.0",
"webpack-dev-server": "2.5.1",
"webpack-merge": "4.1.0",
@@ -159,6 +166,14 @@
"integrity": "sha1-ttiC6kDRjVE/w6A1p5h1Ap/jjwE=",
"dev": true
},
+ "@angular/material": {
+ "version": "5.2.4",
+ "resolved": "http://registry.npm.taobao.org/@angular/material/download/@angular/material-5.2.4.tgz",
+ "integrity": "sha1-noI3mDJCg9I+qDkVb6xby3NEPVU=",
+ "requires": {
+ "tslib": "1.7.1"
+ }
+ },
"@angular/platform-browser": {
"version": "4.3.5",
"resolved": "http://registry.npm.taobao.org/@angular/platform-browser/download/@angular/platform-browser-4.3.5.tgz",
@@ -209,6 +224,16 @@
"source-map": "0.5.6"
}
},
+ "@ngx-translate/core": {
+ "version": "7.2.2",
+ "resolved": "http://registry.npm.taobao.org/@ngx-translate/core/download/@ngx-translate/core-7.2.2.tgz",
+ "integrity": "sha1-5wBp1e+Og36jNuaJB3N4b177XeU="
+ },
+ "@ngx-translate/http-loader": {
+ "version": "1.1.0",
+ "resolved": "http://registry.npm.taobao.org/@ngx-translate/http-loader/download/@ngx-translate/http-loader-1.1.0.tgz",
+ "integrity": "sha1-opMmDOpvYXhKb/nFWklz6pd43OI="
+ },
"@types/jasmine": {
"version": "2.5.53",
"resolved": "http://registry.npm.taobao.org/@types/jasmine/download/@types/jasmine-2.5.53.tgz",
@@ -1870,6 +1895,11 @@
}
}
},
+ "date-fns": {
+ "version": "1.29.0",
+ "resolved": "http://registry.npm.taobao.org/date-fns/download/date-fns-1.29.0.tgz",
+ "integrity": "sha1-EuYJzcuTUScxHQTTMzTilgoqVOY="
+ },
"date-now": {
"version": "0.1.4",
"resolved": "http://registry.npm.taobao.org/date-now/download/date-now-0.1.4.tgz",
@@ -4179,6 +4209,11 @@
"resolved": "http://10.75.8.148/repository/npm-pub/jquery/-/jquery-3.2.1.tgz",
"integrity": "sha1-XE2d5lKvbNCncBVKYxu6ErAVx4c="
},
+ "jquery-ui": {
+ "version": "1.12.1",
+ "resolved": "http://registry.npm.taobao.org/jquery-ui/download/jquery-ui-1.12.1.tgz",
+ "integrity": "sha1-vLQEXI3QU5wTS8FIjN0+dop6nlE="
+ },
"js-base64": {
"version": "2.1.9",
"resolved": "http://registry.npm.taobao.org/js-base64/download/js-base64-2.1.9.tgz",
@@ -8456,9 +8491,9 @@
}
},
"webpack": {
- "version": "3.4.1",
- "resolved": "http://registry.npm.taobao.org/webpack/download/webpack-3.4.1.tgz",
- "integrity": "sha1-TD9PP7MYFVpNsMtqNv8FxWl0GPQ=",
+ "version": "3.5.6",
+ "resolved": "http://registry.npm.taobao.org/webpack/download/webpack-3.5.6.tgz",
+ "integrity": "sha1-pJL7bB7X9XOBb5DgDI+7WiDMXDY=",
"dev": true,
"requires": {
"acorn": "5.1.1",
@@ -8477,7 +8512,7 @@
"mkdirp": "0.5.1",
"node-libs-browser": "2.0.0",
"source-map": "0.5.6",
- "supports-color": "4.2.1",
+ "supports-color": "4.5.0",
"tapable": "0.2.8",
"uglifyjs-webpack-plugin": "0.4.6",
"watchpack": "1.4.0",
@@ -8566,9 +8601,9 @@
"dev": true
},
"supports-color": {
- "version": "4.2.1",
- "resolved": "http://registry.npm.taobao.org/supports-color/download/supports-color-4.2.1.tgz",
- "integrity": "sha1-ZaS7JjHpDgJCDbpVVMN1pHVLuDY=",
+ "version": "4.5.0",
+ "resolved": "http://registry.npm.taobao.org/supports-color/download/supports-color-4.5.0.tgz",
+ "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=",
"dev": true,
"requires": {
"has-flag": "2.0.0"
diff --git a/sdc-workflow-designer-ui/package.json b/sdc-workflow-designer-ui/package.json
index 561639ea..3cd612ec 100644
--- a/sdc-workflow-designer-ui/package.json
+++ b/sdc-workflow-designer-ui/package.json
@@ -13,11 +13,13 @@
"private": true,
"dependencies": {
"@angular/animations": "^4.2.4",
+ "@angular/cdk": "2.0.0-beta.8",
"@angular/common": "^4.2.4",
"@angular/compiler": "^4.2.4",
"@angular/core": "^4.2.4",
"@angular/forms": "^4.2.4",
"@angular/http": "^4.2.4",
+ "@angular/material": "^5.2.0",
"@angular/platform-browser": "^4.2.4",
"@angular/platform-browser-dynamic": "^4.2.4",
"@angular/router": "^4.2.4",
@@ -26,6 +28,7 @@
"angular-in-memory-web-api": "^0.3.2",
"bootstrap": "4.0.0-alpha.6",
"core-js": "^2.4.1",
+ "date-fns": "^1.29.0",
"font-awesome": "^4.7.0",
"jquery": "^3.2.1",
"jquery-ui": "^1.12.1",
@@ -55,6 +58,7 @@
"protractor": "~5.1.2",
"ts-node": "~3.2.0",
"tslint": "~5.3.2",
- "typescript": "~2.3.3"
+ "typescript": "~2.3.3",
+ "webpack": "3.5.6"
}
}
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);
+ }
+}