File

src/dialog/dialog.component.ts

Description

Implements a Dialog that can be positioned anywhere on the page. Used to implement a popover or tooltip.

Implements

OnInit AfterViewInit OnDestroy

Example

Metadata

selector ibm-dialog

Index

Properties
Methods
Inputs
Outputs
HostListeners

Constructor

constructor(elementRef: ElementRef)

Creates an instance of Dialog.

Parameters :
Name Type Optional Description
elementRef ElementRef

Inputs

dialogConfig

Receives DialogConfig interface object with properties of Dialog explictly defined.

Type: DialogConfig

Outputs

close

Emits event that handles the closing of a Dialog object.

$event type: EventEmitter<any>

HostListeners

document:click
Arguments : '$event'
document:click(event: )
keydown
Arguments : '$event'
keydown(event: KeyboardEvent)

Methods

Public doClose
doClose()

Closes Dialog object by emitting the close event upwards to parents.

Returns : void
ngAfterViewInit
ngAfterViewInit()

After the DOM is ready, focus is set and dialog is placed in respect to the parent element.

Returns : void
ngOnDestroy
ngOnDestroy()

At destruction of component, Dialog unsubscribes from handling window resizing changes.

Returns : void
ngOnInit
ngOnInit()

Initilize the Dialog, set the placement and gap, and add a Subscription to resize events.

Returns : void
onDialogInit
onDialogInit()

Empty method to be overridden by consuming classes to run any additional initialization code.

Returns : void
placeDialog
placeDialog()

Uses the position service to position the Dialog in screen space

Returns : void

Properties

Protected addGap
addGap:

Handles offsetting the Dialog item based on the defined position to not obscure the content beneath.

Public data
data:

Stores the data received from dialogConfig.

dialog
dialog: ElementRef
Type : ElementRef
Decorators : ViewChild

Maintains a reference to the view DOM element of the Dialog.

Public placement
placement: string
Type : string

The placement of the Dialog is recieved from the Position service.

Protected resizeObservable
resizeObservable: Observable<any>
Type : Observable<any>

One static event observable to handle window resizing.

Protected resizeSubscription
resizeSubscription: Subscription
Type : Subscription

Subscription used to update placement in the event of a window resize.

Protected scrollSubscription
scrollSubscription: Subscription
Type : Subscription

Subscription to all the scrollable parents scroll event

import {
	Component,
	Input,
	Output,
	EventEmitter,
	ElementRef,
	ViewChild,
	OnInit,
	AfterViewInit,
	OnDestroy,
	HostListener
} from "@angular/core";
import {
	Observable,
	Subscription,
	fromEvent,
	merge
} from "rxjs";
import { throttleTime } from "rxjs/operators";
// the AbsolutePosition is required to import the declaration correctly
import position, { AbsolutePosition } from "./../utils/position";
import { cycleTabs } from "./../common/tab.service";
import { DialogConfig } from "./dialog-config.interface";


/**
 * Implements a `Dialog` that can be positioned anywhere on the page.
 * Used to implement a popover or tooltip.
 *
 * @export
 * @class Dialog
 * @implements {OnInit}
 * @implements {AfterViewInit}
 * @implements {OnDestroy}
 */
@Component({
	selector: "ibm-dialog",
	template: ""
})
export class Dialog implements OnInit, AfterViewInit, OnDestroy {
	/**
	 * One static event observable to handle window resizing.
	 * @protected
	 * @static
	 * @type {Observable<any>}
	 * @memberof Dialog
	 */
	protected static resizeObservable: Observable<any> = fromEvent(window, "resize").pipe(throttleTime(100));
	/**
	 * Emits event that handles the closing of a `Dialog` object.
	 * @type {EventEmitter<any>}
	 * @memberof Dialog
	 */
	@Output() close: EventEmitter<any> = new EventEmitter();
	/**
	 * Receives `DialogConfig` interface object with properties of `Dialog`
	 * explictly defined.
	 * @type {DialogConfig}
	 * @memberof Dialog
	 */
	@Input() dialogConfig: DialogConfig;
	/**
	 * Maintains a reference to the view DOM element of the `Dialog`.
	 * @type {ElementRef}
	 * @memberof Dialog
	 */
	@ViewChild("dialog") dialog: ElementRef;

	/**
	 * Stores the data received from `dialogConfig`.
	 * @memberof Dialog
	 */
	public data = {};

	/**
	 * The placement of the `Dialog` is recieved from the `Position` service.
	 * @type {Placement}
	 * @memberof Dialog
	 */
	public placement: string;

	/**
	 * `Subscription` used to update placement in the event of a window resize.
	 * @protected
	 * @type {Subscription}
	 * @memberof Dialog
	 */
	protected resizeSubscription: Subscription;
	/**
	 * Subscription to all the scrollable parents `scroll` event
	 */
	// add a new subscription temprarily so that contexts (such as tests)
	// that don't run ngAfterViewInit have something to unsubscribe in ngOnDestroy
	protected scrollSubscription: Subscription = new Subscription();
	/**
	 * Handles offsetting the `Dialog` item based on the defined position
	 * to not obscure the content beneath.
	 * @protected
	 * @memberof Dialog
	 */
	protected addGap = {
		"left": pos => position.addOffset(pos, 0, -this.dialogConfig.gap),
		"right": pos => position.addOffset(pos, 0, this.dialogConfig.gap),
		"top": pos => position.addOffset(pos, -this.dialogConfig.gap),
		"bottom": pos => position.addOffset(pos, this.dialogConfig.gap),
		"left-bottom": pos => position.addOffset(pos, 0, -this.dialogConfig.gap),
		"right-bottom": pos => position.addOffset(pos, 0, this.dialogConfig.gap)
	};

	/**
	 * Creates an instance of `Dialog`.
	 * @param {ElementRef} elementRef
	 * @memberof Dialog
	 */
	constructor(protected elementRef: ElementRef) {}

	/**
	 * Initilize the `Dialog`, set the placement and gap, and add a `Subscription` to resize events.
	 * @memberof Dialog
	 */
	ngOnInit() {
		this.placement = this.dialogConfig.placement.split(",")[0];
		this.data = this.dialogConfig.data;

		this.resizeSubscription = Dialog.resizeObservable.subscribe(() => {
			this.placeDialog();
		});

		// run any additional initlization code that consuming classes may have
		this.onDialogInit();
	}

	/**
	 * After the DOM is ready, focus is set and dialog is placed
	 * in respect to the parent element.
	 * @memberof Dialog
	 */
	ngAfterViewInit() {
		const dialogElement = this.dialog.nativeElement;
		// split the wrapper class list and apply separately to avoid IE from
		// 1. throwing an error due to assigning a readonly property (classList)
		// 2. throwing a SyntaxError due to passing an empty string to `add`
		if (this.dialogConfig.wrapperClass) {
			for (const extraClass of this.dialogConfig.wrapperClass.split(" ")) {
				dialogElement.classList.add(extraClass);
			}
		}
		this.placeDialog();
		dialogElement.focus();
		const parentEl: HTMLElement = this.dialogConfig.parentRef.nativeElement;
		let node = parentEl;
		let observables = [];

		// if the element has an overflow set as part of
		// its computed style it can scroll
		const isScrollableElement = (element: HTMLElement) => {
			const computedStyle = getComputedStyle(element);
			return (
				computedStyle.overflow === "auto" ||
				computedStyle.overflow === "scroll" ||
				computedStyle["overflow-y"] === "auto" ||
				computedStyle["overflow-y"] === "scroll" ||
				computedStyle["overflow-x"] === "auto" ||
				computedStyle["overflow-x"] === "scroll"
			);
		};

		const isVisibleInContainer = (element, container) => {
			const elementRect = element.getBoundingClientRect();
			const containerRect = container.getBoundingClientRect();
			return elementRect.bottom <= containerRect.bottom && elementRect.top >= containerRect.top;
		};

		const placeDialogInContainer = () => {
			// only do the work to find the scroll containers if we're appended to body
			// or skip this work if we're inline
			if (!this.dialogConfig.appendInline) {
				// walk the parents and subscribe to all the scroll events we can
				while (node.parentElement && node !== document.body) {
					if (isScrollableElement(node)) {
						observables.push(fromEvent(node, "scroll"));
					}
					node = node.parentElement;
				}
				// subscribe to the observable, and update the position and visibility
				const scrollObservable = merge(...observables);
				this.scrollSubscription = scrollObservable.subscribe((event: any) => {
					this.placeDialog();
					if (!isVisibleInContainer(this.dialogConfig.parentRef.nativeElement, event.target)) {
						this.doClose();
					}
				});
			}
		};

		// settimeout to let the DOM settle before attempting to place the dialog
		setTimeout(placeDialogInContainer);
	}

	/**
	 * Empty method to be overridden by consuming classes to run any additional initialization code.
	 * @memberof Dialog
	 */
	onDialogInit() {}

	/**
	 * Uses the position service to position the `Dialog` in screen space
	 * @memberof Dialog
	 */
	placeDialog(): void {
		// helper to find the position based on the current/given environment
		const findPosition = (reference, target, placement) => {
			let pos;
			if (this.dialogConfig.appendInline) {
				pos = this.addGap[placement](position.findRelative(reference, target, placement));
			} else {
				pos = this.addGap[placement](position.findAbsolute(reference, target, placement));
				pos = position.addOffset(pos, window.scrollY, window.scrollX);
			}
			return pos;
		};

		let parentEl = this.dialogConfig.parentRef.nativeElement;
		let el = this.dialog.nativeElement;
		let dialogPlacement = this.placement;

		// split always retuns an array, so we can just use the auto position logic
		// for single positions too
		const placements = this.dialogConfig.placement.split(",");
		const weightedPlacements = placements.map(placement => {
			const pos = findPosition(parentEl, el, placement);
			let box = position.getPlacementBox(el, pos);
			let hiddenHeight = box.bottom - window.innerHeight - window.scrollY;
			let hiddenWidth = box.right - window.innerWidth - window.scrollX;
			// if the hiddenHeight or hiddenWidth is negative, reset to offsetHeight or offsetWidth
			hiddenHeight = hiddenHeight < 0 ? el.offsetHeight : hiddenHeight;
			hiddenWidth = hiddenWidth < 0 ? el.offsetWidth : hiddenWidth;
			const area = el.offsetHeight * el.offsetWidth;
			const hiddenArea = hiddenHeight * hiddenWidth;
			let visibleArea = area - hiddenArea;
			// if the visibleArea is 0 set it back to area (to calculate the percentage in a useful way)
			visibleArea = visibleArea === 0 ? area : visibleArea;
			const visiblePercent = visibleArea / area;
			return {
				placement,
				weight: visiblePercent
			};
		});

		// sort the placments from best to worst
		weightedPlacements.sort((a, b) => b.weight - a.weight);
		// pick the best!
		dialogPlacement = weightedPlacements[0].placement;

		// calculate the final position
		const pos = findPosition(parentEl, el, dialogPlacement);

		// update the element
		position.setElement(el, pos);
		setTimeout(() => { this.placement = dialogPlacement; });
	}

	/**
	 * Sets up a KeyboardEvent to close `Dialog` with Escape key.
	 * @param {KeyboardEvent} event
	 * @memberof Dialog
	 */
	@HostListener("keydown", ["$event"])
	escapeClose(event: KeyboardEvent) {
		switch (event.key) {
			case "Esc": // IE specific value
			case "Escape": {
				event.stopImmediatePropagation();
				this.doClose();
				break;
			}
			case "Tab": {
				cycleTabs(event, this.elementRef.nativeElement);
				break;
			}
		}
	}

	/**
	 * Sets up a event Listener to close `Dialog` if click event occurs outside
	 * `Dialog` object.
	 * @param {any} event
	 * @memberof Dialog
	 */
	@HostListener("document:click", ["$event"])
	clickClose(event) {
		if (!this.elementRef.nativeElement.contains(event.target)
			&& !this.dialogConfig.parentRef.nativeElement.contains(event.target) ) {
			this.doClose();
		}
	}

	/**
	 * Closes `Dialog` object by emitting the close event upwards to parents.
	 * @memberof Dialog
	 */
	public doClose() {
		this.close.emit();
	}

	/**
	 * At destruction of component, `Dialog` unsubscribes from handling window resizing changes.
	 * @memberof Dialog
	 */
	ngOnDestroy() {
		this.resizeSubscription.unsubscribe();
		this.scrollSubscription.unsubscribe();
	}
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""