aboutsummaryrefslogtreecommitdiffstats
path: root/deprecated-workflow-designer/sdc-workflow-designer-ui/src/app/paletx/core/overlay/overlay-ref.ts
blob: 03c8c2b5723242e85859c34a5d5234c6cb6c89db (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
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`;
}