ng-hub-ui-portal
Version:
A flexible Angular portal library for dynamic content rendering with advanced positioning and interaction control
884 lines (873 loc) • 37.5 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, inject, ElementRef, NgZone, EventEmitter, Component, ViewEncapsulation, ViewChild, Input, Output, ApplicationRef, Injector, EnvironmentInjector, createComponent, TemplateRef, NgModule } from '@angular/core';
import { DOCUMENT, NgIf } from '@angular/common';
import { isDefined, isPromise, hubRunTransition, reflow, getFocusableBoundaryElements, ScrollBar, hubFocusTrap, ContentRef, isString } from 'ng-hub-ui-utils';
import { Subject, zip } from 'rxjs';
import { takeUntil, take } from 'rxjs/operators';
/**
* A configuration service for the [`HubPortal`](#/components/portal/api#HubPortal) service.
*
* You can inject this service, typically in your root component, and customize the values of its properties in
* order to provide default values for all portals used in the application.
*
* @since 3.1.0
*/
class HubPortalConfig {
constructor() {
this.keyboard = true;
this.dismissSelector = '[data-dismiss="portal"]';
this.closeSelector = '[data-close="portal"]';
}
get animation() {
return this._animation ?? true /* this._hubConfig.animation */;
}
set animation(animation) {
this._animation = animation;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubPortalConfig, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubPortalConfig, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubPortalConfig, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
/**
* A reference to the currently opened (active) portal.
*
* Instances of this class can be injected into your component passed as portal content.
* So you can `.update()`, `.close()` or `.dismiss()` the portal window from your component.
*/
class HubActivePortal {
/**
* Updates options of an opened portal.
*
* @since 14.2.0
*/
update(options) { }
/**
* Closes the portal with an optional `result` value.
*
* The `HubPortalRef.result` promise will be resolved with the provided value.
*/
close(result) { }
/**
* Dismisses the portal with an optional `reason` value.
*
* The `HubPortalRef.result` promise will be rejected with the provided value.
*/
dismiss(reason) { }
}
const WINDOW_ATTRIBUTES = [
'animation',
'ariaLabelledBy',
'ariaDescribedBy',
'scrollable',
'windowClass',
'portalDialogClass',
'portalContentClass'
];
const BACKDROP_ATTRIBUTES = ['animation', 'backdropClass'];
/**
* A reference to the newly opened portal returned by the `HubPortal.open()` method.
*/
class HubPortalRef {
_applyWindowOptions(windowInstance, options) {
WINDOW_ATTRIBUTES.forEach((optionName) => {
if (isDefined(options[optionName])) {
windowInstance[optionName] = options[optionName];
}
});
}
/**
* Updates options of an opened portal.
*
* @since 14.2.0
*/
update(options) {
this._applyWindowOptions(this._windowCmptRef.instance, options);
}
/**
* The instance of a component used for the portal content.
*
* When a `TemplateRef` is used as the content or when the portal is closed, will return `undefined`.
*/
get componentInstance() {
if (this._contentRef && this._contentRef.componentRef) {
return this._contentRef.componentRef.instance;
}
}
/**
* The observable that emits when the portal is closed via the `.close()` method.
*
* It will emit the result passed to the `.close()` method.
*/
get closed() {
return this._closed.asObservable().pipe(takeUntil(this._hidden));
}
/**
* The observable that emits when the portal is dismissed via the `.dismiss()` method.
*
* It will emit the reason passed to the `.dismissed()` method by the user.
*/
get dismissed() {
return this._dismissed.asObservable().pipe(takeUntil(this._hidden));
}
/**
* The observable that emits when portal window is closed and animations were finished.
* At this point portal element will be removed from the DOM tree.
*
* This observable will be completed after emitting.
*/
get hidden() {
return this._hidden.asObservable();
}
/**
* The observable that emits when portal is fully visible and animation was finished.
* Portal DOM element is always available synchronously after calling 'portal.open()' service.
*
* This observable will be completed after emitting.
* It will not emit, if portal is closed before open animation is finished.
*/
get shown() {
return this._windowCmptRef.instance.shown.asObservable();
}
constructor(_windowCmptRef, _contentRef, _beforeDismiss) {
this._windowCmptRef = _windowCmptRef;
this._contentRef = _contentRef;
this._beforeDismiss = _beforeDismiss;
this._closed = new Subject();
this._dismissed = new Subject();
this._hidden = new Subject();
_windowCmptRef.instance.dismissEvent.subscribe((reason) => {
this.dismiss(reason);
});
this.result = new Promise((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
});
this.result.then(null, () => { });
}
/**
* Closes the portal with an optional `result` value.
*
* The `HubMobalRef.result` promise will be resolved with the provided value.
*/
close(result) {
if (this._windowCmptRef) {
this._closed.next(result);
this._resolve(result);
this._removePortalElements();
}
}
_dismiss(reason) {
this._dismissed.next(reason);
this._reject(reason);
this._removePortalElements();
}
/**
* Dismisses the portal with an optional `reason` value.
*
* The `HubPortalRef.result` promise will be rejected with the provided value.
*/
dismiss(reason) {
if (this._windowCmptRef) {
if (!this._beforeDismiss) {
this._dismiss(reason);
}
else {
const dismiss = this._beforeDismiss();
if (isPromise(dismiss)) {
dismiss.then((result) => {
if (result !== false) {
this._dismiss(reason);
}
}, () => { });
}
else if (dismiss !== false) {
this._dismiss(reason);
}
}
}
}
_removePortalElements() {
const windowTransition$ = this._windowCmptRef.instance.hide();
// hiding window
windowTransition$.subscribe(() => {
const { nativeElement } = this._windowCmptRef.location;
nativeElement.parentNode.removeChild(nativeElement);
this._windowCmptRef.destroy();
this._contentRef?.viewRef?.destroy();
this._windowCmptRef = null;
this._contentRef = null;
});
// all done
zip(windowTransition$).subscribe(() => {
this._hidden.next();
this._hidden.complete();
});
}
}
class HubPortalWindow {
constructor() {
this._document = inject(DOCUMENT);
this._elRef = inject((ElementRef));
this._zone = inject(NgZone);
this._closed$ = new Subject();
this._elWithFocus = null; // element that is focused prior to portal opening
this.dismissEvent = new EventEmitter();
this.shown = new Subject();
this.hidden = new Subject();
}
dismiss(reason) {
this.dismissEvent.emit(reason);
}
ngOnInit() {
this._elWithFocus = this._document.activeElement;
this._zone.onStable
.asObservable()
.pipe(take(1))
.subscribe(() => {
this._show();
});
}
ngOnDestroy() {
this._disableEventHandling();
}
hide() {
const { nativeElement } = this._elRef;
const context = {
animation: this.animation,
runningTransition: 'stop'
};
const windowTransition$ = hubRunTransition(this._zone, nativeElement, () => nativeElement.classList.remove('show'), context);
const dialogTransition$ = hubRunTransition(this._zone, this._dialogEl.nativeElement, () => { }, context);
const transitions$ = zip(windowTransition$, dialogTransition$);
transitions$.subscribe(() => {
this.hidden.next();
this.hidden.complete();
});
this._disableEventHandling();
this._restoreFocus();
return transitions$;
}
_show() {
const context = {
animation: this.animation,
runningTransition: 'continue'
};
const windowTransition$ = hubRunTransition(this._zone, this._elRef.nativeElement, (element, animation) => {
if (animation) {
reflow(element);
}
element.classList.add('show');
}, context);
const dialogTransition$ = hubRunTransition(this._zone, this._dialogEl.nativeElement, () => { }, context);
zip(windowTransition$, dialogTransition$).subscribe(() => {
this.shown.next();
this.shown.complete();
});
this._setFocus();
}
_disableEventHandling() {
this._closed$.next();
}
_setFocus() {
const { nativeElement } = this._elRef;
if (!nativeElement.contains(document.activeElement)) {
const autoFocusable = nativeElement.querySelector(`[hubAutofocus]`);
const firstFocusable = getFocusableBoundaryElements(nativeElement)[0];
const elementToFocus = autoFocusable || firstFocusable || nativeElement;
elementToFocus.focus();
}
}
_restoreFocus() {
const body = this._document.body;
const elWithFocus = this._elWithFocus;
let elementToFocus;
if (elWithFocus && elWithFocus['focus'] && body.contains(elWithFocus)) {
elementToFocus = elWithFocus;
}
else {
elementToFocus = body;
}
this._zone.runOutsideAngular(() => {
setTimeout(() => elementToFocus.focus());
this._elWithFocus = null;
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubPortalWindow, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.5", type: HubPortalWindow, isStandalone: true, selector: "hub-portal-window", inputs: { animation: "animation", ariaLabelledBy: "ariaLabelledBy", ariaDescribedBy: "ariaDescribedBy", scrollable: "scrollable", windowClass: "windowClass", portalDialogClass: "portalDialogClass", portalContentClass: "portalContentClass" }, outputs: { dismissEvent: "dismiss" }, host: { attributes: { "role": "dialog", "tabindex": "-1" }, properties: { "class": "\"portal d-block\" + (windowClass ? \" \" + windowClass : \"\")", "class.fade": "animation", "attr.aria-portal": "true", "attr.aria-labelledby": "ariaLabelledBy", "attr.aria-describedby": "ariaDescribedBy" } }, viewQueries: [{ propertyName: "_dialogEl", first: true, predicate: ["dialog"], descendants: true, static: true }], ngImport: i0, template: `
<div
#dialog
[class]="
'portal-dialog' +
(scrollable ? ' portal-dialog-scrollable' : '') +
(portalDialogClass ? ' ' + portalDialogClass : '')
"
role="document"
>
<div
[class]="
'portal-content' +
(portalContentClass ? ' ' + portalContentClass : '')
"
>
<ng-container *ngIf="singleContent; else multipleContent">
<ng-content></ng-content>
</ng-container>
<ng-template #multipleContent>
<div class="portal-header">
<ng-content />
<button
type="button"
class="btn-close"
data-bs-dismiss="portal"
aria-label="Close"
(click)="dismiss(null)"
></button>
</div>
<div class="portal-body">
<ng-content />
</div>
<div class="portal-footer">
<ng-content />
</div>
</ng-template>
</div>
</div>
`, isInline: true, styles: ["hub-portal-window .component-host-scrollable{display:flex;flex-direction:column;overflow:hidden}\n"], dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], encapsulation: i0.ViewEncapsulation.None }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubPortalWindow, decorators: [{
type: Component,
args: [{ selector: 'hub-portal-window', standalone: true, imports: [NgIf], host: {
'[class]': '"portal d-block" + (windowClass ? " " + windowClass : "")',
'[class.fade]': 'animation',
role: 'dialog',
tabindex: '-1',
'[attr.aria-portal]': 'true',
'[attr.aria-labelledby]': 'ariaLabelledBy',
'[attr.aria-describedby]': 'ariaDescribedBy'
}, template: `
<div
#dialog
[class]="
'portal-dialog' +
(scrollable ? ' portal-dialog-scrollable' : '') +
(portalDialogClass ? ' ' + portalDialogClass : '')
"
role="document"
>
<div
[class]="
'portal-content' +
(portalContentClass ? ' ' + portalContentClass : '')
"
>
<ng-container *ngIf="singleContent; else multipleContent">
<ng-content></ng-content>
</ng-container>
<ng-template #multipleContent>
<div class="portal-header">
<ng-content />
<button
type="button"
class="btn-close"
data-bs-dismiss="portal"
aria-label="Close"
(click)="dismiss(null)"
></button>
</div>
<div class="portal-body">
<ng-content />
</div>
<div class="portal-footer">
<ng-content />
</div>
</ng-template>
</div>
</div>
`, encapsulation: ViewEncapsulation.None, styles: ["hub-portal-window .component-host-scrollable{display:flex;flex-direction:column;overflow:hidden}\n"] }]
}], propDecorators: { _dialogEl: [{
type: ViewChild,
args: ['dialog', { static: true }]
}], animation: [{
type: Input
}], ariaLabelledBy: [{
type: Input
}], ariaDescribedBy: [{
type: Input
}], scrollable: [{
type: Input
}], windowClass: [{
type: Input
}], portalDialogClass: [{
type: Input
}], portalContentClass: [{
type: Input
}], dismissEvent: [{
type: Output,
args: ['dismiss']
}] } });
class HubPortalStack {
constructor() {
this._applicationRef = inject(ApplicationRef);
this._injector = inject(Injector);
this._environmentInjector = inject(EnvironmentInjector);
this._document = inject(DOCUMENT);
this._scrollBar = inject(ScrollBar);
this._activeWindowCmptHasChanged = new Subject();
this._ariaHiddenValues = new Map();
this._scrollBarRestoreFn = null;
this._portalRefs = [];
this._windowCmpts = [];
this._activeInstances = new EventEmitter();
const ngZone = inject(NgZone);
// Trap focus on active WindowCmpt
this._activeWindowCmptHasChanged.subscribe(() => {
if (this._windowCmpts.length) {
const activeWindowCmpt = this._windowCmpts[this._windowCmpts.length - 1];
hubFocusTrap(ngZone, activeWindowCmpt.location.nativeElement, this._activeWindowCmptHasChanged);
this._revertAriaHidden();
this._setAriaHidden(activeWindowCmpt.location.nativeElement);
}
});
}
_restoreScrollBar() {
const scrollBarRestoreFn = this._scrollBarRestoreFn;
if (scrollBarRestoreFn) {
this._scrollBarRestoreFn = null;
scrollBarRestoreFn();
}
}
_hideScrollBar() {
if (!this._scrollBarRestoreFn) {
this._scrollBarRestoreFn = this._scrollBar.hide();
}
}
open(contentInjector, content, options) {
const containerEl = options.container instanceof HTMLElement
? options.container
: isDefined(options.container)
? this._document.querySelector(options.container)
: this._document.body;
if (!containerEl) {
throw new Error(`The specified portal container "${options.container || 'body'}" was not found in the DOM.`);
}
this._hideScrollBar();
const activePortal = new HubActivePortal();
contentInjector = options.injector || contentInjector;
const environmentInjector = contentInjector.get(EnvironmentInjector, null) ||
this._environmentInjector;
const contentRef = this._getContentRef(contentInjector, environmentInjector, content, activePortal, options);
const windowCmptRef = this._createWindowComponent(contentRef.nodes, options);
this._attachWindowComponent(containerEl, windowCmptRef);
const hubPortalRef = new HubPortalRef(windowCmptRef, contentRef, options.beforeDismiss);
this._registerPortalRef(hubPortalRef);
this._registerWindowCmpt(windowCmptRef);
// We have to cleanup DOM after the last portal when BOTH 'hidden' was emitted and 'result' promise was resolved:
// - with animations OFF, 'hidden' emits synchronously, then 'result' is resolved asynchronously
// - with animations ON, 'result' is resolved asynchronously, then 'hidden' emits asynchronously
hubPortalRef.hidden.pipe(take(1)).subscribe(() => Promise.resolve(true).then(() => {
if (!this._portalRefs.length) {
this._document.body.classList.remove('portal-open');
this._restoreScrollBar();
this._revertAriaHidden();
}
}));
activePortal.close = (result) => {
hubPortalRef.close(result);
};
activePortal.dismiss = (reason) => {
hubPortalRef.dismiss(reason);
};
activePortal.update = (options) => {
hubPortalRef.update(options);
};
hubPortalRef.update(options);
if (this._portalRefs.length === 1) {
this._document.body.classList.add('portal-open');
}
windowCmptRef.changeDetectorRef.detectChanges();
return hubPortalRef;
}
/**
* Toggles a portal by dismissing all existing portals and waiting for them to be hidden
* before showing the new one.
*
* @param contentInjector - The injector to use for dependency injection
* @param content - The content to display (component, template, or string)
* @param options - Portal configuration options
* @returns A reference to the newly created portal
*/
toggle(contentInjector, content, options) {
// Get current portals before creating the new one
const existingPortals = [...this._portalRefs];
// Create the new portal but suppress normal registration
const containerEl = options.container instanceof HTMLElement
? options.container
: isDefined(options.container)
? this._document.querySelector(options.container)
: this._document.body;
if (!containerEl) {
throw new Error(`The specified portal container "${options.container || 'body'}" was not found in the DOM.`);
}
this._hideScrollBar();
const activePortal = new HubActivePortal();
contentInjector = options.injector || contentInjector;
const environmentInjector = contentInjector.get(EnvironmentInjector, null) ||
this._environmentInjector;
const contentRef = this._getContentRef(contentInjector, environmentInjector, content, activePortal, options);
const windowCmptRef = this._createWindowComponent(contentRef.nodes, options);
const newPortalRef = new HubPortalRef(windowCmptRef, contentRef, options.beforeDismiss);
// Setup active portal methods
activePortal.close = (result) => {
newPortalRef.close(result);
};
activePortal.dismiss = (reason) => {
newPortalRef.dismiss(reason);
};
activePortal.update = (options) => {
newPortalRef.update(options);
};
// Dismiss all existing portals and wait for them to be hidden
const hidePromises = existingPortals.map((portal) => new Promise((resolve) => {
portal.hidden.pipe(take(1)).subscribe(() => {
resolve();
});
portal.dismiss('toggle');
}));
// After all portals are hidden, register and show the new one
Promise.all(hidePromises).then(() => {
this._attachWindowComponent(containerEl, windowCmptRef);
this._registerPortalRef(newPortalRef);
this._registerWindowCmpt(windowCmptRef);
if (this._portalRefs.length === 1) {
this._document.body.classList.add('portal-open');
}
newPortalRef.update(options);
windowCmptRef.changeDetectorRef.detectChanges();
});
// Setup hidden cleanup like in open()
newPortalRef.hidden.pipe(take(1)).subscribe(() => Promise.resolve(true).then(() => {
if (!this._portalRefs.length) {
this._document.body.classList.remove('portal-open');
this._restoreScrollBar();
this._revertAriaHidden();
}
}));
return newPortalRef;
}
get activeInstances() {
return this._activeInstances;
}
dismissAll(reason) {
this._portalRefs.forEach((hubPortalRef) => hubPortalRef.dismiss(reason));
}
hasOpenPortals() {
return this._portalRefs.length > 0;
}
_createWindowComponent([headerNodes, bodyNodes, footerNodes], options) {
const singleContent = !options.headerSelector && !options.footerSelector;
let windowCmptRef = createComponent(HubPortalWindow, {
environmentInjector: this._applicationRef.injector,
elementInjector: this._injector,
projectableNodes: singleContent
? [bodyNodes]
: [[], headerNodes, bodyNodes, footerNodes]
});
Object.assign(windowCmptRef.instance, { singleContent });
return windowCmptRef;
}
_attachWindowComponent(containerEl, windowCmptRef) {
this._applicationRef.attachView(windowCmptRef.hostView);
containerEl.appendChild(windowCmptRef.location.nativeElement);
return windowCmptRef;
}
_getContentRef(contentInjector, environmentInjector, content, activePortal, options) {
if (!content) {
return new ContentRef([]);
}
else if (content instanceof TemplateRef) {
return this._createFromTemplateRef(content, activePortal, options);
}
else if (isString(content)) {
return this._createFromString(content);
}
else {
return this._createFromComponent(contentInjector, environmentInjector, content, activePortal, options);
}
}
_createFromTemplateRef(templateRef, activePortal, options) {
const context = {
$implicit: activePortal,
close(result) {
activePortal.close(result);
},
dismiss(reason) {
activePortal.dismiss(reason);
}
};
const viewRef = templateRef.createEmbeddedView(context);
this._applicationRef.attachView(viewRef);
const containerNode = document.createElement('ng-container');
containerNode.append(...viewRef.rootNodes);
this._addDismissEventListener(containerNode, context, options);
this._addCloseEventListener(containerNode, context, options);
return new ContentRef([
options.headerSelector
? extractAndRemoveNodesBySelector(containerNode, options.headerSelector)
: [],
containerNode.childNodes,
options.footerSelector
? extractAndRemoveNodesBySelector(containerNode, options.footerSelector)
: []
], viewRef);
}
_createFromString(content) {
const component = this._document.createTextNode(`${content}`);
return new ContentRef([[component]]);
}
_createFromComponent(contentInjector, environmentInjector, componentType, context, options) {
const elementInjector = Injector.create({
providers: [{ provide: HubActivePortal, useValue: context }],
parent: contentInjector
});
const componentRef = createComponent(componentType, {
environmentInjector,
elementInjector
});
const componentNativeEl = componentRef.location.nativeElement;
if (options.scrollable) {
componentNativeEl.classList.add('component-host-scrollable');
}
this._applicationRef.attachView(componentRef.hostView);
this._addDismissEventListener(componentNativeEl, context, options);
this._addCloseEventListener(componentNativeEl, context, options);
// FIXME: we should here get rid of the component nativeElement
// and use `[Array.from(componentNativeEl.childNodes)]` instead and remove the above CSS class.
return new ContentRef([
options.headerSelector
? extractAndRemoveNodesBySelector(componentNativeEl, options.headerSelector)
: [],
componentNativeEl.childNodes,
options.footerSelector
? extractAndRemoveNodesBySelector(componentNativeEl, options.footerSelector)
: []
], componentRef.hostView, componentRef);
}
_setAriaHidden(element) {
const parent = element.parentElement;
if (parent && element !== this._document.body) {
Array.from(parent.children).forEach((sibling) => {
if (sibling !== element && sibling.nodeName !== 'SCRIPT') {
this._ariaHiddenValues.set(sibling, sibling.getAttribute('aria-hidden'));
sibling.setAttribute('aria-hidden', 'true');
}
});
this._setAriaHidden(parent);
}
}
_revertAriaHidden() {
this._ariaHiddenValues.forEach((value, element) => {
if (value) {
element.setAttribute('aria-hidden', value);
}
else {
element.removeAttribute('aria-hidden');
}
});
this._ariaHiddenValues.clear();
}
_registerPortalRef(hubPortalRef) {
const unregisterPortalRef = () => {
const index = this._portalRefs.indexOf(hubPortalRef);
if (index > -1) {
this._portalRefs.splice(index, 1);
this._activeInstances.emit(this._portalRefs);
}
};
this._portalRefs.push(hubPortalRef);
this._activeInstances.emit(this._portalRefs);
hubPortalRef.result.then(unregisterPortalRef, unregisterPortalRef);
}
_registerWindowCmpt(hubWindowCmpt) {
this._windowCmpts.push(hubWindowCmpt);
this._activeWindowCmptHasChanged.next();
hubWindowCmpt.onDestroy(() => {
const index = this._windowCmpts.indexOf(hubWindowCmpt);
if (index > -1) {
this._windowCmpts.splice(index, 1);
this._activeWindowCmptHasChanged.next();
}
});
}
/**
* Attaches click event listeners to elements within a container based on a specified dismiss selector to dismiss a portal.
*
* @param {HTMLElement} container - The `container` parameter is an HTMLElement that represents the DOM element which contains the
* portal content.
* @param {HubActivePortal} context - The `context` parameter in the `_addDismissEventListener` function refers to the active portal
* instance that is being displayed. It is used to call the `dismiss` method on the portal instance when a dismissible element is
* clicked.
* @param {HubPortalOptions} options - The `options` parameter is an object that contains configuration options for the portal. It
* may include properties such as `dismissSelector`, which is used to specify a CSS selector for elements that, when clicked, will
* dismiss the portal by calling the `dismiss` method on the `context` object.
*/
_addDismissEventListener(container, context, options) {
if (options.dismissSelector) {
const dismissaable = container.querySelectorAll(options.dismissSelector);
for (const item of Array.from(dismissaable)) {
item.addEventListener('click', () => context.dismiss());
}
}
}
/**
* Attaches click event listeners to elements matching a specified selector to close a portal window.
*
* @param {HTMLElement} container - The `container` parameter is an HTMLElement that represents the DOM element which contains the
* portal content.
* @param {HubActivePortal} context - The `context` parameter in the `_addCloseEventListener` function is of type `HubActivePortal`.
* It is used to reference the active portal instance within the function and call the `close()` method on it when a close event is
* triggered.
* @param {HubPortalOptions} options - The `options` parameter is an object that contains configuration options for the portal. It
* may include properties such as `closeSelector`, which is used to specify the selector for elements that can trigger the portal
* to close when clicked.
*/
_addCloseEventListener(container, context, options) {
if (options.closeSelector) {
const dismissaable = container.querySelectorAll(options.closeSelector);
for (const item of Array.from(dismissaable)) {
item.addEventListener('click', () => context.close());
}
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubPortalStack, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubPortalStack, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubPortalStack, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [] });
/**
* Extracts child nodes matching a selector from a container element, removes those nodes from the DOM, and returns them as an array.
*
* @param {HTMLElement} container - The `container` parameter in the `extractAndRemoveNodesBySelector` function is an HTMLElement
* that represents the parent element within which we want to search for nodes matching a specific selector and remove them.
* @param {string} selector - The `selector` parameter in the `extractAndRemoveNodesBySelector` function is a string that
* represents a CSS selector. This selector is used to query and select specific elements within the `container` HTMLElement.
*
* @returns An array of nodes that were extracted from the container element based on the provided selector, and then removes those
* nodes from the DOM.
*/
function extractAndRemoveNodesBySelector(container, selector) {
let containerNodes = container.querySelectorAll(selector);
const nodes = Array.from(containerNodes).reduce((acc, c) => {
return [...acc, ...Array.from(c.childNodes)];
}, []);
// Selecciona los nodos dentro del contenedor que coincidan con el selector
const nodesToRemove = container.querySelectorAll(selector);
// Convertir NodeList a array y eliminar cada nodo del DOM
Array.from(nodesToRemove).forEach((node) => node.remove());
return nodes;
}
/**
* A service for opening portal windows.
*
* Creating a portal is straightforward: create a component or a template and pass it as an argument to
* the `.open()` method.
*/
class HubPortal {
constructor() {
this._injector = inject(Injector);
this._portalStack = inject(HubPortalStack);
this._config = inject(HubPortalConfig);
}
/**
* Opens a new portal window with the specified content and supplied options.
*
* Content can be provided as a `TemplateRef` or a component type. If you pass a component type as content,
* then instances of those components can be injected with an instance of the `HubActivePortal` class. You can then
* use `HubActivePortal` methods to close / dismiss portals from "inside" of your component.
*
* Also see the [`HubPortalOptions`](#/components/portal/api#HubPortalOptions) for the list of supported options.
*/
open(content, options = {}) {
const combinedOptions = {
...this._config,
animation: this._config.animation,
...options
};
return this._portalStack.open(this._injector, content, combinedOptions);
}
/**
* The function `toggle` opens a portal with specified content and options while dismissing any existing portals.
*
* @param {any} content - The `content` parameter in the `toggle` function is the content that you want to display within the
* portal. This can be any type of content such as a component, template, or any other HTML element that you want to show in the
* portal.
* @param {HubPortalOptions} options - The `options` parameter in the `toggle` function is an object that allows you to customize
* the behavior of the portal.
*
* @returns The `toggle` function is returning a `HubPortalRef` object.
*/
toggle(content, options = {}) {
const combinedOptions = {
...this._config,
animation: this._config.animation,
...options
};
// this.dismissAll();
// return this._portalStack.open(this._injector, content, combinedOptions);
return this._portalStack.toggle(this._injector, content, combinedOptions);
}
/**
* Returns an observable that holds the active portal instances.
*/
get activeInstances() {
return this._portalStack.activeInstances;
}
/**
* Dismisses all currently displayed portal windows with the supplied reason.
*
* @since 3.1.0
*/
dismissAll(reason) {
this._portalStack.dismissAll(reason);
}
/**
* Indicates if there are currently any open portal windows in the application.
*
* @since 3.3.0
*/
hasOpenPortals() {
return this._portalStack.hasOpenPortals();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubPortal, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubPortal, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubPortal, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
var PortalDismissReasons;
(function (PortalDismissReasons) {
PortalDismissReasons[PortalDismissReasons["BACKDROP_CLICK"] = 0] = "BACKDROP_CLICK";
PortalDismissReasons[PortalDismissReasons["ESC"] = 1] = "ESC";
})(PortalDismissReasons || (PortalDismissReasons = {}));
class HubPortalModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubPortalModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "17.3.5", ngImport: i0, type: HubPortalModule }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubPortalModule, providers: [HubPortal] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubPortalModule, decorators: [{
type: NgModule,
args: [{ providers: [HubPortal] }]
}] });
/*
* Public API Surface of modal
*/
/**
* Generated bundle index. Do not edit.
*/
export { HubActivePortal, HubPortal, HubPortalConfig, HubPortalModule, HubPortalRef, HubPortalStack, PortalDismissReasons };
//# sourceMappingURL=ng-hub-ui-portal.mjs.map