Catalog alignment
[sdc.git] / catalog-ui / src / app / ng2 / components / ui / popover / popover-content.component.ts
1 /*-
2  * ============LICENSE_START=======================================================
3  * SDC
4  * ================================================================================
5  * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
6  * ================================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ============LICENSE_END=========================================================
19  */
20
21 import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, OnDestroy, Output, Renderer, ViewChild } from '@angular/core';
22 import { ButtonsModelMap } from 'app/models';
23 import { PopoverComponent } from './popover.component';
24
25 @Component({
26     selector: 'popover-content',
27     templateUrl: './popover-content.component.html',
28     styleUrls: ['popover-content.component.less']
29 })
30 export class PopoverContentComponent implements AfterViewInit, OnDestroy {
31
32     @Input() public title: string;
33     @Input() public buttons: ButtonsModelMap;
34
35     @Input()
36     content: string;
37
38     @Input()
39     placement: 'top'|'bottom'|'left'|'right'|'auto'|'auto top'|'auto bottom'|'auto left'|'auto right' = 'bottom';
40
41     @Input()
42     animation: boolean = true;
43
44     @Input()
45     closeOnClickOutside: boolean = false;
46
47     @Input()
48     closeOnMouseOutside: boolean = false;
49
50     @Input()
51     hideArrow: boolean = false;
52
53     @ViewChild('popoverDiv')
54     popoverDiv: ElementRef;
55
56     buttonsNames: string[];
57
58     popover: PopoverComponent;
59     onCloseFromOutside = new EventEmitter();
60     top: number = 250;
61     left: number = 300;
62     isIn: boolean = false;
63     displayType: string = 'none';
64     effectivePlacement: string;
65
66     listenClickFunc: any;
67     listenMouseFunc: any;
68
69     constructor(protected element: ElementRef,
70                 protected cdr: ChangeDetectorRef,
71                 protected renderer: Renderer) {
72     }
73
74     onDocumentMouseDown = (event: any) => {
75         const element = this.element.nativeElement;
76         if (!element || !this.popover) { return; }
77         if (element.contains(event.target) || this.popover.getElement().contains(event.target)) { return; }
78         this.hide();
79         this.onCloseFromOutside.emit(undefined);
80     }
81
82     ngAfterViewInit(): void {
83         if ( this.buttons ) {
84             this.buttonsNames = Object.keys(this.buttons);
85         }
86         if (this.closeOnClickOutside) {
87             this.listenClickFunc = this.renderer.listenGlobal('document', 'mousedown', (event: any) => this.onDocumentMouseDown(event));
88         }
89         if (this.closeOnMouseOutside) {
90             this.listenMouseFunc = this.renderer.listenGlobal('document', 'mouseover', (event: any) => this.onDocumentMouseDown(event));
91         }
92
93         this.cdr.detectChanges();
94     }
95
96     ngOnDestroy() {
97         if (this.closeOnClickOutside) {
98             this.listenClickFunc();
99         }
100         if (this.closeOnMouseOutside) {
101             this.listenMouseFunc();
102         }
103     }
104
105     // -------------------------------------------------------------------------
106     // Public Methods
107     // -------------------------------------------------------------------------
108
109     show(): void {
110         if (!this.popover || !this.popover.getElement()) {
111             return;
112         }
113
114         const p = this.positionElements(this.popover.getElement(), this.popoverDiv.nativeElement, this.placement);
115         this.displayType = 'block';
116         this.top = p.top;
117         this.left = p.left;
118         this.isIn = true;
119     }
120
121     hide(): void {
122         this.top = -10000;
123         this.left = -10000;
124         this.isIn = true;
125         this.popover.hide();
126     }
127
128     hideFromPopover() {
129         this.displayType = 'none';
130         this.isIn = true;
131     }
132
133     // -------------------------------------------------------------------------
134     // Protected Methods
135     // -------------------------------------------------------------------------
136
137     protected positionElements(hostEl: HTMLElement, targetEl: HTMLElement, positionStr: string, appendToBody: boolean = false): { top: number, left: number } {
138         const positionStrParts = positionStr.split('-');
139         let pos0 = positionStrParts[0];
140         const pos1 = positionStrParts[1] || 'center';
141         const hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl);
142         const targetElWidth = targetEl.offsetWidth;
143         const targetElHeight = targetEl.offsetHeight;
144
145         this.effectivePlacement = pos0 = this.getEffectivePlacement(pos0, hostEl, targetEl);
146
147         const shiftWidth: any = {
148             center(): number {
149                 return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2;
150             },
151             left(): number {
152                 return hostElPos.left;
153             },
154             right(): number {
155                 return hostElPos.left + hostElPos.width - targetElWidth;
156             }
157         };
158
159         const shiftHeight: any = {
160             center(): number {
161                 return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2;
162             },
163             top(): number {
164                 return hostElPos.top;
165             },
166             bottom(): number {
167                 return hostElPos.top + hostElPos.height - targetElHeight;
168             }
169         };
170
171         let targetElPos: { top: number, left: number };
172         switch (pos0) {
173             case 'right':
174                 targetElPos = {
175                     top: shiftHeight[pos1](),
176                     left: hostElPos.left + hostElPos.width
177                 };
178                 break;
179
180             case 'left':
181                 targetElPos = {
182                     top: shiftHeight[pos1](),
183                     left: hostElPos.left - targetElWidth
184                 };
185                 break;
186
187             case 'bottom':
188                 targetElPos = {
189                     top: hostElPos.top + hostElPos.height,
190                     left: shiftWidth[pos1]()
191                 };
192                 break;
193
194             default:
195                 targetElPos = {
196                     top: hostElPos.top - targetElHeight,
197                     left: shiftWidth[pos1]()
198                 };
199                 break;
200         }
201
202         return targetElPos;
203     }
204
205     protected position(nativeEl: HTMLElement): { width: number, height: number, top: number, left: number } {
206         let offsetParentBCR = { top: 0, left: 0 };
207         const elBCR = this.offset(nativeEl);
208         const offsetParentEl = this.parentOffsetEl(nativeEl);
209         if (offsetParentEl !== window.document) {
210             offsetParentBCR = this.offset(offsetParentEl);
211             offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop;
212             offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft;
213         }
214
215         const boundingClientRect = nativeEl.getBoundingClientRect();
216         return {
217             width: boundingClientRect.width || nativeEl.offsetWidth,
218             height: boundingClientRect.height || nativeEl.offsetHeight,
219             top: elBCR.top - offsetParentBCR.top,
220             left: elBCR.left - offsetParentBCR.left
221         };
222     }
223
224     protected offset(nativeEl: any): { width: number, height: number, top: number, left: number } {
225         const boundingClientRect = nativeEl.getBoundingClientRect();
226         return {
227             width: boundingClientRect.width || nativeEl.offsetWidth,
228             height: boundingClientRect.height || nativeEl.offsetHeight,
229             top: boundingClientRect.top + (window.pageYOffset || window.document.documentElement.scrollTop),
230             left: boundingClientRect.left + (window.pageXOffset || window.document.documentElement.scrollLeft)
231         };
232     }
233
234     protected getStyle(nativeEl: HTMLElement, cssProp: string): string {
235         if ((nativeEl as any).currentStyle) { // IE
236             return (nativeEl as any).currentStyle[cssProp];
237         }
238
239         if (window.getComputedStyle) {
240             return (window.getComputedStyle as any)(nativeEl)[cssProp];
241         }
242
243         // finally try and get inline style
244         return (nativeEl.style as any)[cssProp];
245     }
246
247     protected isStaticPositioned(nativeEl: HTMLElement): boolean {
248         return (this.getStyle(nativeEl, 'position') || 'static' ) === 'static';
249     }
250
251     protected parentOffsetEl(nativeEl: HTMLElement): any {
252         let offsetParent: any = nativeEl.offsetParent || window.document;
253         while (offsetParent && offsetParent !== window.document && this.isStaticPositioned(offsetParent)) {
254             offsetParent = offsetParent.offsetParent;
255         }
256         return offsetParent || window.document;
257     }
258
259     protected getEffectivePlacement(placement: string, hostElement: HTMLElement, targetElement: HTMLElement): string {
260         const placementParts = placement.split(' ');
261         if (placementParts[0] !== 'auto') {
262             return placement;
263         }
264
265         const hostElBoundingRect = hostElement.getBoundingClientRect();
266
267         const desiredPlacement = placementParts[1] || 'bottom';
268
269         if (desiredPlacement === 'top' && hostElBoundingRect.top - targetElement.offsetHeight < 0) {
270             return 'bottom';
271         }
272         if (desiredPlacement === 'bottom' && hostElBoundingRect.bottom + targetElement.offsetHeight > window.innerHeight) {
273             return 'top';
274         }
275         if (desiredPlacement === 'left' && hostElBoundingRect.left - targetElement.offsetWidth < 0) {
276             return 'right';
277         }
278         if (desiredPlacement === 'right' && hostElBoundingRect.right + targetElement.offsetWidth > window.innerWidth) {
279             return 'left';
280         }
281
282         return desiredPlacement;
283     }
284 }