UNPKG

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
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