ng-hub-ui-modal 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,920 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Injectable, inject, ElementRef, NgZone, Component, ViewEncapsulation, Input, EventEmitter, ViewChild, Output, ApplicationRef, Injector, EnvironmentInjector, createComponent, TemplateRef } from '@angular/core';
3
+ import { DOCUMENT, NgIf } from '@angular/common';
4
+ import { hubRunTransition, reflow, isDefined, isPromise, isString, getFocusableBoundaryElements, ScrollBar, hubFocusTrap, ContentRef } from 'ng-hub-ui-utils';
5
+ import { Subject, of, zip, fromEvent } from 'rxjs';
6
+ import { take, takeUntil, filter, tap, switchMap } from 'rxjs/operators';
7
+
8
+ /**
9
+ * A configuration service for the [`HubModal`](#/components/modal/api#HubModal) service.
10
+ *
11
+ * You can inject this service, typically in your root component, and customize the values of its properties in
12
+ * order to provide default values for all modals used in the application.
13
+ *
14
+ * @since 3.1.0
15
+ */
16
+ class HubModalConfig {
17
+ constructor() {
18
+ this.backdrop = true;
19
+ this.fullscreen = false;
20
+ this.keyboard = true;
21
+ this.dismissSelector = '[data-dismiss="modal"]';
22
+ }
23
+ get animation() {
24
+ return this._animation ?? true /* this._hubConfig.animation */;
25
+ }
26
+ set animation(animation) {
27
+ this._animation = animation;
28
+ }
29
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubModalConfig, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
30
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubModalConfig, providedIn: 'root' }); }
31
+ }
32
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubModalConfig, decorators: [{
33
+ type: Injectable,
34
+ args: [{ providedIn: 'root' }]
35
+ }] });
36
+
37
+ class HubModalBackdrop {
38
+ constructor() {
39
+ this._nativeElement = inject(ElementRef).nativeElement;
40
+ this._zone = inject(NgZone);
41
+ }
42
+ ngOnInit() {
43
+ this._zone.onStable
44
+ .asObservable()
45
+ .pipe(take(1))
46
+ .subscribe(() => {
47
+ hubRunTransition(this._zone, this._nativeElement, (element, animation) => {
48
+ if (animation) {
49
+ reflow(element);
50
+ }
51
+ element.classList.add('show');
52
+ }, { animation: this.animation, runningTransition: 'continue' });
53
+ });
54
+ }
55
+ hide() {
56
+ return hubRunTransition(this._zone, this._nativeElement, ({ classList }) => classList.remove('show'), {
57
+ animation: this.animation,
58
+ runningTransition: 'stop'
59
+ });
60
+ }
61
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubModalBackdrop, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
62
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.5", type: HubModalBackdrop, isStandalone: true, selector: "hub-modal-backdrop", inputs: { animation: "animation", backdropClass: "backdropClass" }, host: { properties: { "class": "\"modal-backdrop\" + (backdropClass ? \" \" + backdropClass : \"\")", "class.show": "!animation", "class.fade": "animation" }, styleAttribute: "z-index: 1055" }, ngImport: i0, template: '', isInline: true, encapsulation: i0.ViewEncapsulation.None }); }
63
+ }
64
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubModalBackdrop, decorators: [{
65
+ type: Component,
66
+ args: [{
67
+ selector: 'hub-modal-backdrop',
68
+ standalone: true,
69
+ encapsulation: ViewEncapsulation.None,
70
+ template: '',
71
+ host: {
72
+ '[class]': '"modal-backdrop" + (backdropClass ? " " + backdropClass : "")',
73
+ '[class.show]': '!animation',
74
+ '[class.fade]': 'animation',
75
+ style: 'z-index: 1055'
76
+ }
77
+ }]
78
+ }], propDecorators: { animation: [{
79
+ type: Input
80
+ }], backdropClass: [{
81
+ type: Input
82
+ }] } });
83
+
84
+ /**
85
+ * A reference to the currently opened (active) modal.
86
+ *
87
+ * Instances of this class can be injected into your component passed as modal content.
88
+ * So you can `.update()`, `.close()` or `.dismiss()` the modal window from your component.
89
+ */
90
+ class HubActiveModal {
91
+ /**
92
+ * Updates options of an opened modal.
93
+ *
94
+ * @since 14.2.0
95
+ */
96
+ update(options) { }
97
+ /**
98
+ * Closes the modal with an optional `result` value.
99
+ *
100
+ * The `HubModalRef.result` promise will be resolved with the provided value.
101
+ */
102
+ close(result) { }
103
+ /**
104
+ * Dismisses the modal with an optional `reason` value.
105
+ *
106
+ * The `HubModalRef.result` promise will be rejected with the provided value.
107
+ */
108
+ dismiss(reason) { }
109
+ }
110
+ const WINDOW_ATTRIBUTES = [
111
+ 'animation',
112
+ 'ariaLabelledBy',
113
+ 'ariaDescribedBy',
114
+ 'backdrop',
115
+ 'centered',
116
+ 'fullscreen',
117
+ 'keyboard',
118
+ 'scrollable',
119
+ 'size',
120
+ 'windowClass',
121
+ 'modalDialogClass'
122
+ ];
123
+ const BACKDROP_ATTRIBUTES = ['animation', 'backdropClass'];
124
+ /**
125
+ * A reference to the newly opened modal returned by the `HubModal.open()` method.
126
+ */
127
+ class HubModalRef {
128
+ _applyWindowOptions(windowInstance, options) {
129
+ WINDOW_ATTRIBUTES.forEach((optionName) => {
130
+ if (isDefined(options[optionName])) {
131
+ windowInstance[optionName] = options[optionName];
132
+ }
133
+ });
134
+ }
135
+ _applyBackdropOptions(backdropInstance, options) {
136
+ BACKDROP_ATTRIBUTES.forEach((optionName) => {
137
+ if (isDefined(options[optionName])) {
138
+ backdropInstance[optionName] = options[optionName];
139
+ }
140
+ });
141
+ }
142
+ /**
143
+ * Updates options of an opened modal.
144
+ *
145
+ * @since 14.2.0
146
+ */
147
+ update(options) {
148
+ this._applyWindowOptions(this._windowCmptRef.instance, options);
149
+ if (this._backdropCmptRef && this._backdropCmptRef.instance) {
150
+ this._applyBackdropOptions(this._backdropCmptRef.instance, options);
151
+ }
152
+ }
153
+ /**
154
+ * The instance of a component used for the modal content.
155
+ *
156
+ * When a `TemplateRef` is used as the content or when the modal is closed, will return `undefined`.
157
+ */
158
+ get componentInstance() {
159
+ if (this._contentRef && this._contentRef.componentRef) {
160
+ return this._contentRef.componentRef.instance;
161
+ }
162
+ }
163
+ /**
164
+ * The observable that emits when the modal is closed via the `.close()` method.
165
+ *
166
+ * It will emit the result passed to the `.close()` method.
167
+ *
168
+ * @since 8.0.0
169
+ */
170
+ get closed() {
171
+ return this._closed.asObservable().pipe(takeUntil(this._hidden));
172
+ }
173
+ /**
174
+ * The observable that emits when the modal is dismissed via the `.dismiss()` method.
175
+ *
176
+ * It will emit the reason passed to the `.dismissed()` method by the user, or one of the internal
177
+ * reasons like backdrop click or ESC key press.
178
+ *
179
+ * @since 8.0.0
180
+ */
181
+ get dismissed() {
182
+ return this._dismissed.asObservable().pipe(takeUntil(this._hidden));
183
+ }
184
+ /**
185
+ * The observable that emits when both modal window and backdrop are closed and animations were finished.
186
+ * At this point modal and backdrop elements will be removed from the DOM tree.
187
+ *
188
+ * This observable will be completed after emitting.
189
+ *
190
+ * @since 8.0.0
191
+ */
192
+ get hidden() {
193
+ return this._hidden.asObservable();
194
+ }
195
+ /**
196
+ * The observable that emits when modal is fully visible and animation was finished.
197
+ * Modal DOM element is always available synchronously after calling 'modal.open()' service.
198
+ *
199
+ * This observable will be completed after emitting.
200
+ * It will not emit, if modal is closed before open animation is finished.
201
+ *
202
+ * @since 8.0.0
203
+ */
204
+ get shown() {
205
+ return this._windowCmptRef.instance.shown.asObservable();
206
+ }
207
+ constructor(_windowCmptRef, _contentRef, _backdropCmptRef, _beforeDismiss) {
208
+ this._windowCmptRef = _windowCmptRef;
209
+ this._contentRef = _contentRef;
210
+ this._backdropCmptRef = _backdropCmptRef;
211
+ this._beforeDismiss = _beforeDismiss;
212
+ this._closed = new Subject();
213
+ this._dismissed = new Subject();
214
+ this._hidden = new Subject();
215
+ _windowCmptRef.instance.dismissEvent.subscribe((reason) => {
216
+ this.dismiss(reason);
217
+ });
218
+ this.result = new Promise((resolve, reject) => {
219
+ this._resolve = resolve;
220
+ this._reject = reject;
221
+ });
222
+ this.result.then(null, () => { });
223
+ }
224
+ /**
225
+ * Closes the modal with an optional `result` value.
226
+ *
227
+ * The `HubMobalRef.result` promise will be resolved with the provided value.
228
+ */
229
+ close(result) {
230
+ if (this._windowCmptRef) {
231
+ this._closed.next(result);
232
+ this._resolve(result);
233
+ this._removeModalElements();
234
+ }
235
+ }
236
+ _dismiss(reason) {
237
+ this._dismissed.next(reason);
238
+ this._reject(reason);
239
+ this._removeModalElements();
240
+ }
241
+ /**
242
+ * Dismisses the modal with an optional `reason` value.
243
+ *
244
+ * The `HubModalRef.result` promise will be rejected with the provided value.
245
+ */
246
+ dismiss(reason) {
247
+ if (this._windowCmptRef) {
248
+ if (!this._beforeDismiss) {
249
+ this._dismiss(reason);
250
+ }
251
+ else {
252
+ const dismiss = this._beforeDismiss();
253
+ if (isPromise(dismiss)) {
254
+ dismiss.then((result) => {
255
+ if (result !== false) {
256
+ this._dismiss(reason);
257
+ }
258
+ }, () => { });
259
+ }
260
+ else if (dismiss !== false) {
261
+ this._dismiss(reason);
262
+ }
263
+ }
264
+ }
265
+ }
266
+ _removeModalElements() {
267
+ const windowTransition$ = this._windowCmptRef.instance.hide();
268
+ const backdropTransition$ = this._backdropCmptRef
269
+ ? this._backdropCmptRef.instance.hide()
270
+ : of(undefined);
271
+ // hiding window
272
+ windowTransition$.subscribe(() => {
273
+ const { nativeElement } = this._windowCmptRef.location;
274
+ nativeElement.parentNode.removeChild(nativeElement);
275
+ this._windowCmptRef.destroy();
276
+ this._contentRef?.viewRef?.destroy();
277
+ this._windowCmptRef = null;
278
+ this._contentRef = null;
279
+ });
280
+ // hiding backdrop
281
+ backdropTransition$.subscribe(() => {
282
+ if (this._backdropCmptRef) {
283
+ const { nativeElement } = this._backdropCmptRef.location;
284
+ nativeElement.parentNode.removeChild(nativeElement);
285
+ this._backdropCmptRef.destroy();
286
+ this._backdropCmptRef = null;
287
+ }
288
+ });
289
+ // all done
290
+ zip(windowTransition$, backdropTransition$).subscribe(() => {
291
+ this._hidden.next();
292
+ this._hidden.complete();
293
+ });
294
+ }
295
+ }
296
+
297
+ var ModalDismissReasons;
298
+ (function (ModalDismissReasons) {
299
+ ModalDismissReasons[ModalDismissReasons["BACKDROP_CLICK"] = 0] = "BACKDROP_CLICK";
300
+ ModalDismissReasons[ModalDismissReasons["ESC"] = 1] = "ESC";
301
+ })(ModalDismissReasons || (ModalDismissReasons = {}));
302
+
303
+ class HubModalWindow {
304
+ constructor() {
305
+ this._document = inject(DOCUMENT);
306
+ this._elRef = inject((ElementRef));
307
+ this._zone = inject(NgZone);
308
+ this._closed$ = new Subject();
309
+ this._elWithFocus = null; // element that is focused prior to modal opening
310
+ this.backdrop = true;
311
+ this.keyboard = true;
312
+ this.dismissEvent = new EventEmitter();
313
+ this.shown = new Subject();
314
+ this.hidden = new Subject();
315
+ }
316
+ get fullscreenClass() {
317
+ return this.fullscreen === true
318
+ ? ' modal-fullscreen'
319
+ : isString(this.fullscreen)
320
+ ? ` modal-fullscreen-${this.fullscreen}-down`
321
+ : '';
322
+ }
323
+ dismiss(reason) {
324
+ this.dismissEvent.emit(reason);
325
+ }
326
+ ngOnInit() {
327
+ this._elWithFocus = this._document.activeElement;
328
+ this._zone.onStable
329
+ .asObservable()
330
+ .pipe(take(1))
331
+ .subscribe(() => {
332
+ this._show();
333
+ });
334
+ }
335
+ ngOnDestroy() {
336
+ this._disableEventHandling();
337
+ }
338
+ hide() {
339
+ const { nativeElement } = this._elRef;
340
+ const context = {
341
+ animation: this.animation,
342
+ runningTransition: 'stop'
343
+ };
344
+ const windowTransition$ = hubRunTransition(this._zone, nativeElement, () => nativeElement.classList.remove('show'), context);
345
+ const dialogTransition$ = hubRunTransition(this._zone, this._dialogEl.nativeElement, () => { }, context);
346
+ const transitions$ = zip(windowTransition$, dialogTransition$);
347
+ transitions$.subscribe(() => {
348
+ this.hidden.next();
349
+ this.hidden.complete();
350
+ });
351
+ this._disableEventHandling();
352
+ this._restoreFocus();
353
+ return transitions$;
354
+ }
355
+ _show() {
356
+ const context = {
357
+ animation: this.animation,
358
+ runningTransition: 'continue'
359
+ };
360
+ const windowTransition$ = hubRunTransition(this._zone, this._elRef.nativeElement, (element, animation) => {
361
+ if (animation) {
362
+ reflow(element);
363
+ }
364
+ element.classList.add('show');
365
+ }, context);
366
+ const dialogTransition$ = hubRunTransition(this._zone, this._dialogEl.nativeElement, () => { }, context);
367
+ zip(windowTransition$, dialogTransition$).subscribe(() => {
368
+ this.shown.next();
369
+ this.shown.complete();
370
+ });
371
+ this._enableEventHandling();
372
+ this._setFocus();
373
+ }
374
+ _enableEventHandling() {
375
+ const { nativeElement } = this._elRef;
376
+ this._zone.runOutsideAngular(() => {
377
+ fromEvent(nativeElement, 'keydown')
378
+ .pipe(takeUntil(this._closed$), filter((e) => e.key === 'Escape'))
379
+ .subscribe((event) => {
380
+ if (this.keyboard) {
381
+ requestAnimationFrame(() => {
382
+ if (!event.defaultPrevented) {
383
+ this._zone.run(() => this.dismiss(ModalDismissReasons.ESC));
384
+ }
385
+ });
386
+ }
387
+ else if (this.backdrop === 'static') {
388
+ this._bumpBackdrop();
389
+ }
390
+ });
391
+ // We're listening to 'mousedown' and 'mouseup' to prevent modal from closing when pressing the mouse
392
+ // inside the modal dialog and releasing it outside
393
+ let preventClose = false;
394
+ fromEvent(this._dialogEl.nativeElement, 'mousedown')
395
+ .pipe(takeUntil(this._closed$), tap(() => (preventClose = false)), switchMap(() => fromEvent(nativeElement, 'mouseup').pipe(takeUntil(this._closed$), take(1))), filter(({ target }) => nativeElement === target))
396
+ .subscribe(() => {
397
+ preventClose = true;
398
+ });
399
+ // We're listening to 'click' to dismiss modal on modal window click, except when:
400
+ // 1. clicking on modal dialog itself
401
+ // 2. closing was prevented by mousedown/up handlers
402
+ // 3. clicking on scrollbar when the viewport is too small and modal doesn't fit (click is not triggered at all)
403
+ fromEvent(nativeElement, 'click')
404
+ .pipe(takeUntil(this._closed$))
405
+ .subscribe(({ target }) => {
406
+ if (nativeElement === target) {
407
+ if (this.backdrop === 'static') {
408
+ this._bumpBackdrop();
409
+ }
410
+ else if (this.backdrop === true && !preventClose) {
411
+ this._zone.run(() => this.dismiss(ModalDismissReasons.BACKDROP_CLICK));
412
+ }
413
+ }
414
+ preventClose = false;
415
+ });
416
+ });
417
+ }
418
+ _disableEventHandling() {
419
+ this._closed$.next();
420
+ }
421
+ _setFocus() {
422
+ const { nativeElement } = this._elRef;
423
+ if (!nativeElement.contains(document.activeElement)) {
424
+ const autoFocusable = nativeElement.querySelector(`[hubAutofocus]`);
425
+ const firstFocusable = getFocusableBoundaryElements(nativeElement)[0];
426
+ const elementToFocus = autoFocusable || firstFocusable || nativeElement;
427
+ elementToFocus.focus();
428
+ }
429
+ }
430
+ _restoreFocus() {
431
+ const body = this._document.body;
432
+ const elWithFocus = this._elWithFocus;
433
+ let elementToFocus;
434
+ if (elWithFocus && elWithFocus['focus'] && body.contains(elWithFocus)) {
435
+ elementToFocus = elWithFocus;
436
+ }
437
+ else {
438
+ elementToFocus = body;
439
+ }
440
+ this._zone.runOutsideAngular(() => {
441
+ setTimeout(() => elementToFocus.focus());
442
+ this._elWithFocus = null;
443
+ });
444
+ }
445
+ _bumpBackdrop() {
446
+ if (this.backdrop === 'static') {
447
+ hubRunTransition(this._zone, this._elRef.nativeElement, ({ classList }) => {
448
+ classList.add('modal-static');
449
+ return () => classList.remove('modal-static');
450
+ }, { animation: this.animation, runningTransition: 'continue' });
451
+ }
452
+ }
453
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubModalWindow, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
454
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.5", type: HubModalWindow, isStandalone: true, selector: "hub-modal-window", inputs: { animation: "animation", ariaLabelledBy: "ariaLabelledBy", ariaDescribedBy: "ariaDescribedBy", backdrop: "backdrop", centered: "centered", fullscreen: "fullscreen", keyboard: "keyboard", scrollable: "scrollable", size: "size", windowClass: "windowClass", modalDialogClass: "modalDialogClass" }, outputs: { dismissEvent: "dismiss" }, host: { attributes: { "role": "dialog", "tabindex": "-1" }, properties: { "class": "\"modal d-block\" + (windowClass ? \" \" + windowClass : \"\")", "class.fade": "animation", "attr.aria-modal": "true", "attr.aria-labelledby": "ariaLabelledBy", "attr.aria-describedby": "ariaDescribedBy" } }, viewQueries: [{ propertyName: "_dialogEl", first: true, predicate: ["dialog"], descendants: true, static: true }], ngImport: i0, template: `
455
+ <div
456
+ #dialog
457
+ [class]="
458
+ 'modal-dialog' +
459
+ (size ? ' modal-' + size : '') +
460
+ (centered ? ' modal-dialog-centered' : '') +
461
+ fullscreenClass +
462
+ (scrollable ? ' modal-dialog-scrollable' : '') +
463
+ (modalDialogClass ? ' ' + modalDialogClass : '')
464
+ "
465
+ role="document"
466
+ >
467
+ <div class="modal-content">
468
+ <ng-container *ngIf="singleContent; else multipleContent">
469
+ <ng-content></ng-content>
470
+ </ng-container>
471
+ <ng-template #multipleContent>
472
+ <div class="modal-header">
473
+ <ng-content />
474
+ <button
475
+ type="button"
476
+ class="btn-close"
477
+ data-bs-dismiss="modal"
478
+ aria-label="Close"
479
+ (click)="dismiss(null)"
480
+ ></button>
481
+ </div>
482
+ <div class="modal-body">
483
+ <ng-content />
484
+ </div>
485
+ <div class="modal-footer">
486
+ <ng-content />
487
+ </div>
488
+ </ng-template>
489
+ </div>
490
+ </div>
491
+ `, isInline: true, styles: ["hub-modal-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 }); }
492
+ }
493
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubModalWindow, decorators: [{
494
+ type: Component,
495
+ args: [{ selector: 'hub-modal-window', standalone: true, imports: [NgIf], host: {
496
+ '[class]': '"modal d-block" + (windowClass ? " " + windowClass : "")',
497
+ '[class.fade]': 'animation',
498
+ role: 'dialog',
499
+ tabindex: '-1',
500
+ '[attr.aria-modal]': 'true',
501
+ '[attr.aria-labelledby]': 'ariaLabelledBy',
502
+ '[attr.aria-describedby]': 'ariaDescribedBy'
503
+ }, template: `
504
+ <div
505
+ #dialog
506
+ [class]="
507
+ 'modal-dialog' +
508
+ (size ? ' modal-' + size : '') +
509
+ (centered ? ' modal-dialog-centered' : '') +
510
+ fullscreenClass +
511
+ (scrollable ? ' modal-dialog-scrollable' : '') +
512
+ (modalDialogClass ? ' ' + modalDialogClass : '')
513
+ "
514
+ role="document"
515
+ >
516
+ <div class="modal-content">
517
+ <ng-container *ngIf="singleContent; else multipleContent">
518
+ <ng-content></ng-content>
519
+ </ng-container>
520
+ <ng-template #multipleContent>
521
+ <div class="modal-header">
522
+ <ng-content />
523
+ <button
524
+ type="button"
525
+ class="btn-close"
526
+ data-bs-dismiss="modal"
527
+ aria-label="Close"
528
+ (click)="dismiss(null)"
529
+ ></button>
530
+ </div>
531
+ <div class="modal-body">
532
+ <ng-content />
533
+ </div>
534
+ <div class="modal-footer">
535
+ <ng-content />
536
+ </div>
537
+ </ng-template>
538
+ </div>
539
+ </div>
540
+ `, encapsulation: ViewEncapsulation.None, styles: ["hub-modal-window .component-host-scrollable{display:flex;flex-direction:column;overflow:hidden}\n"] }]
541
+ }], propDecorators: { _dialogEl: [{
542
+ type: ViewChild,
543
+ args: ['dialog', { static: true }]
544
+ }], animation: [{
545
+ type: Input
546
+ }], ariaLabelledBy: [{
547
+ type: Input
548
+ }], ariaDescribedBy: [{
549
+ type: Input
550
+ }], backdrop: [{
551
+ type: Input
552
+ }], centered: [{
553
+ type: Input
554
+ }], fullscreen: [{
555
+ type: Input
556
+ }], keyboard: [{
557
+ type: Input
558
+ }], scrollable: [{
559
+ type: Input
560
+ }], size: [{
561
+ type: Input
562
+ }], windowClass: [{
563
+ type: Input
564
+ }], modalDialogClass: [{
565
+ type: Input
566
+ }], dismissEvent: [{
567
+ type: Output,
568
+ args: ['dismiss']
569
+ }] } });
570
+
571
+ class HubModalStack {
572
+ constructor() {
573
+ this._applicationRef = inject(ApplicationRef);
574
+ this._injector = inject(Injector);
575
+ this._environmentInjector = inject(EnvironmentInjector);
576
+ this._document = inject(DOCUMENT);
577
+ this._scrollBar = inject(ScrollBar);
578
+ this._activeWindowCmptHasChanged = new Subject();
579
+ this._ariaHiddenValues = new Map();
580
+ this._scrollBarRestoreFn = null;
581
+ this._modalRefs = [];
582
+ this._windowCmpts = [];
583
+ this._activeInstances = new EventEmitter();
584
+ const ngZone = inject(NgZone);
585
+ // Trap focus on active WindowCmpt
586
+ this._activeWindowCmptHasChanged.subscribe(() => {
587
+ if (this._windowCmpts.length) {
588
+ const activeWindowCmpt = this._windowCmpts[this._windowCmpts.length - 1];
589
+ hubFocusTrap(ngZone, activeWindowCmpt.location.nativeElement, this._activeWindowCmptHasChanged);
590
+ this._revertAriaHidden();
591
+ this._setAriaHidden(activeWindowCmpt.location.nativeElement);
592
+ }
593
+ });
594
+ }
595
+ _restoreScrollBar() {
596
+ const scrollBarRestoreFn = this._scrollBarRestoreFn;
597
+ if (scrollBarRestoreFn) {
598
+ this._scrollBarRestoreFn = null;
599
+ scrollBarRestoreFn();
600
+ }
601
+ }
602
+ _hideScrollBar() {
603
+ if (!this._scrollBarRestoreFn) {
604
+ this._scrollBarRestoreFn = this._scrollBar.hide();
605
+ }
606
+ }
607
+ open(contentInjector, content, options) {
608
+ const containerEl = options.container instanceof HTMLElement
609
+ ? options.container
610
+ : isDefined(options.container)
611
+ ? this._document.querySelector(options.container)
612
+ : this._document.body;
613
+ if (!containerEl) {
614
+ throw new Error(`The specified modal container "${options.container || 'body'}" was not found in the DOM.`);
615
+ }
616
+ this._hideScrollBar();
617
+ const activeModal = new HubActiveModal();
618
+ contentInjector = options.injector || contentInjector;
619
+ const environmentInjector = contentInjector.get(EnvironmentInjector, null) ||
620
+ this._environmentInjector;
621
+ const contentRef = this._getContentRef(contentInjector, environmentInjector, content, activeModal, options);
622
+ const backdropCmptRef = options.backdrop !== false
623
+ ? this._attachBackdrop(containerEl)
624
+ : undefined;
625
+ const windowCmptRef = this._attachWindowComponent(containerEl, contentRef.nodes, options);
626
+ const hubModalRef = new HubModalRef(windowCmptRef, contentRef, backdropCmptRef, options.beforeDismiss);
627
+ this._registerModalRef(hubModalRef);
628
+ this._registerWindowCmpt(windowCmptRef);
629
+ // We have to cleanup DOM after the last modal when BOTH 'hidden' was emitted and 'result' promise was resolved:
630
+ // - with animations OFF, 'hidden' emits synchronously, then 'result' is resolved asynchronously
631
+ // - with animations ON, 'result' is resolved asynchronously, then 'hidden' emits asynchronously
632
+ hubModalRef.hidden.pipe(take(1)).subscribe(() => Promise.resolve(true).then(() => {
633
+ if (!this._modalRefs.length) {
634
+ this._document.body.classList.remove('modal-open');
635
+ this._restoreScrollBar();
636
+ this._revertAriaHidden();
637
+ }
638
+ }));
639
+ activeModal.close = (result) => {
640
+ hubModalRef.close(result);
641
+ };
642
+ activeModal.dismiss = (reason) => {
643
+ hubModalRef.dismiss(reason);
644
+ };
645
+ activeModal.update = (options) => {
646
+ hubModalRef.update(options);
647
+ };
648
+ hubModalRef.update(options);
649
+ if (this._modalRefs.length === 1) {
650
+ this._document.body.classList.add('modal-open');
651
+ }
652
+ if (backdropCmptRef && backdropCmptRef.instance) {
653
+ backdropCmptRef.changeDetectorRef.detectChanges();
654
+ }
655
+ windowCmptRef.changeDetectorRef.detectChanges();
656
+ return hubModalRef;
657
+ }
658
+ get activeInstances() {
659
+ return this._activeInstances;
660
+ }
661
+ dismissAll(reason) {
662
+ this._modalRefs.forEach((hubModalRef) => hubModalRef.dismiss(reason));
663
+ }
664
+ hasOpenModals() {
665
+ return this._modalRefs.length > 0;
666
+ }
667
+ _attachBackdrop(containerEl) {
668
+ let backdropCmptRef = createComponent(HubModalBackdrop, {
669
+ environmentInjector: this._applicationRef.injector,
670
+ elementInjector: this._injector
671
+ });
672
+ this._applicationRef.attachView(backdropCmptRef.hostView);
673
+ containerEl.appendChild(backdropCmptRef.location.nativeElement);
674
+ return backdropCmptRef;
675
+ }
676
+ _attachWindowComponent(containerEl, [headerNodes, bodyNodes, footerNodes], options) {
677
+ const singleContent = !options.headerSelector && !options.footerSelector;
678
+ let windowCmptRef = createComponent(HubModalWindow, {
679
+ environmentInjector: this._applicationRef.injector,
680
+ elementInjector: this._injector,
681
+ projectableNodes: singleContent
682
+ ? [bodyNodes]
683
+ : [[], headerNodes, bodyNodes, footerNodes]
684
+ });
685
+ Object.assign(windowCmptRef.instance, { singleContent });
686
+ this._applicationRef.attachView(windowCmptRef.hostView);
687
+ containerEl.appendChild(windowCmptRef.location.nativeElement);
688
+ return windowCmptRef;
689
+ }
690
+ _getContentRef(contentInjector, environmentInjector, content, activeModal, options) {
691
+ if (!content) {
692
+ return new ContentRef([]);
693
+ }
694
+ else if (content instanceof TemplateRef) {
695
+ return this._createFromTemplateRef(content, activeModal, options);
696
+ }
697
+ else if (isString(content)) {
698
+ return this._createFromString(content);
699
+ }
700
+ else {
701
+ return this._createFromComponent(contentInjector, environmentInjector, content, activeModal, options);
702
+ }
703
+ }
704
+ _createFromTemplateRef(templateRef, activeModal, options) {
705
+ const context = {
706
+ $implicit: activeModal,
707
+ close(result) {
708
+ activeModal.close(result);
709
+ },
710
+ dismiss(reason) {
711
+ activeModal.dismiss(reason);
712
+ }
713
+ };
714
+ const viewRef = templateRef.createEmbeddedView(context);
715
+ this._applicationRef.attachView(viewRef);
716
+ const containerNode = document.createElement('ng-container');
717
+ containerNode.append(...viewRef.rootNodes);
718
+ this._addDismissEventListener(containerNode, context, options);
719
+ return new ContentRef([
720
+ options.headerSelector
721
+ ? extractAndRemoveNodesBySelector(containerNode, options.headerSelector)
722
+ : [],
723
+ containerNode.childNodes,
724
+ options.footerSelector
725
+ ? extractAndRemoveNodesBySelector(containerNode, options.footerSelector)
726
+ : []
727
+ ], viewRef);
728
+ }
729
+ _createFromString(content) {
730
+ const component = this._document.createTextNode(`${content}`);
731
+ return new ContentRef([[component]]);
732
+ }
733
+ _createFromComponent(contentInjector, environmentInjector, componentType, context, options) {
734
+ const elementInjector = Injector.create({
735
+ providers: [{ provide: HubActiveModal, useValue: context }],
736
+ parent: contentInjector
737
+ });
738
+ const componentRef = createComponent(componentType, {
739
+ environmentInjector,
740
+ elementInjector
741
+ });
742
+ const componentNativeEl = componentRef.location.nativeElement;
743
+ if (options.scrollable) {
744
+ componentNativeEl.classList.add('component-host-scrollable');
745
+ }
746
+ this._applicationRef.attachView(componentRef.hostView);
747
+ this._addDismissEventListener(componentNativeEl, context, options);
748
+ // FIXME: we should here get rid of the component nativeElement
749
+ // and use `[Array.from(componentNativeEl.childNodes)]` instead and remove the above CSS class.
750
+ return new ContentRef([
751
+ options.headerSelector
752
+ ? extractAndRemoveNodesBySelector(componentNativeEl, options.headerSelector)
753
+ : [],
754
+ componentNativeEl.childNodes,
755
+ options.footerSelector
756
+ ? extractAndRemoveNodesBySelector(componentNativeEl, options.footerSelector)
757
+ : []
758
+ ], componentRef.hostView, componentRef);
759
+ }
760
+ _setAriaHidden(element) {
761
+ const parent = element.parentElement;
762
+ if (parent && element !== this._document.body) {
763
+ Array.from(parent.children).forEach((sibling) => {
764
+ if (sibling !== element && sibling.nodeName !== 'SCRIPT') {
765
+ this._ariaHiddenValues.set(sibling, sibling.getAttribute('aria-hidden'));
766
+ sibling.setAttribute('aria-hidden', 'true');
767
+ }
768
+ });
769
+ this._setAriaHidden(parent);
770
+ }
771
+ }
772
+ _revertAriaHidden() {
773
+ this._ariaHiddenValues.forEach((value, element) => {
774
+ if (value) {
775
+ element.setAttribute('aria-hidden', value);
776
+ }
777
+ else {
778
+ element.removeAttribute('aria-hidden');
779
+ }
780
+ });
781
+ this._ariaHiddenValues.clear();
782
+ }
783
+ _registerModalRef(hubModalRef) {
784
+ const unregisterModalRef = () => {
785
+ const index = this._modalRefs.indexOf(hubModalRef);
786
+ if (index > -1) {
787
+ this._modalRefs.splice(index, 1);
788
+ this._activeInstances.emit(this._modalRefs);
789
+ }
790
+ };
791
+ this._modalRefs.push(hubModalRef);
792
+ this._activeInstances.emit(this._modalRefs);
793
+ hubModalRef.result.then(unregisterModalRef, unregisterModalRef);
794
+ }
795
+ _registerWindowCmpt(hubWindowCmpt) {
796
+ this._windowCmpts.push(hubWindowCmpt);
797
+ this._activeWindowCmptHasChanged.next();
798
+ hubWindowCmpt.onDestroy(() => {
799
+ const index = this._windowCmpts.indexOf(hubWindowCmpt);
800
+ if (index > -1) {
801
+ this._windowCmpts.splice(index, 1);
802
+ this._activeWindowCmptHasChanged.next();
803
+ }
804
+ });
805
+ }
806
+ /**
807
+ * Attaches click event listeners to elements within a container based on a specified dismiss selector to dismiss a modal.
808
+ *
809
+ * @param {HTMLElement} container - The `container` parameter is an HTMLElement that represents the DOM element which contains the
810
+ * modal content.
811
+ * @param {HubActiveModal} context - The `context` parameter in the `_addDismissEventListener` function refers to the active modal
812
+ * instance that is being displayed. It is used to call the `dismiss` method on the modal instance when a dismissible element is
813
+ * clicked.
814
+ * @param {HubModalOptions} options - The `options` parameter is an object that contains configuration options for the modal. It
815
+ * may include properties such as `dismissSelector`, which is used to specify a CSS selector for elements that, when clicked, will
816
+ * dismiss the modal by calling the `dismiss` method on the `context` object.
817
+ */
818
+ _addDismissEventListener(container, context, options) {
819
+ if (options.dismissSelector) {
820
+ const dismissaable = container.querySelectorAll(options.dismissSelector);
821
+ for (const item of Array.from(dismissaable)) {
822
+ item.addEventListener('click', () => context.dismiss());
823
+ }
824
+ }
825
+ }
826
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubModalStack, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
827
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubModalStack, providedIn: 'root' }); }
828
+ }
829
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubModalStack, decorators: [{
830
+ type: Injectable,
831
+ args: [{ providedIn: 'root' }]
832
+ }], ctorParameters: () => [] });
833
+ /**
834
+ * Extracts child nodes matching a selector from a container element, removes those nodes from the DOM, and returns them as an array.
835
+ *
836
+ * @param {HTMLElement} container - The `container` parameter in the `extractAndRemoveNodesBySelector` function is an HTMLElement
837
+ * that represents the parent element within which we want to search for nodes matching a specific selector and remove them.
838
+ * @param {string} selector - The `selector` parameter in the `extractAndRemoveNodesBySelector` function is a string that
839
+ * represents a CSS selector. This selector is used to query and select specific elements within the `container` HTMLElement.
840
+ *
841
+ * @returns An array of nodes that were extracted from the container element based on the provided selector, and then removes those
842
+ * nodes from the DOM.
843
+ */
844
+ function extractAndRemoveNodesBySelector(container, selector) {
845
+ let containerNodes = container.querySelectorAll(selector);
846
+ const nodes = Array.from(containerNodes).reduce((acc, c) => {
847
+ return [...acc, ...Array.from(c.childNodes)];
848
+ }, []);
849
+ // Selecciona los nodos dentro del contenedor que coincidan con el selector
850
+ const nodesToRemove = container.querySelectorAll(selector);
851
+ // Convertir NodeList a array y eliminar cada nodo del DOM
852
+ Array.from(nodesToRemove).forEach((node) => node.remove());
853
+ return nodes;
854
+ }
855
+
856
+ /**
857
+ * A service for opening modal windows.
858
+ *
859
+ * Creating a modal is straightforward: create a component or a template and pass it as an argument to
860
+ * the `.open()` method.
861
+ */
862
+ class HubModal {
863
+ constructor() {
864
+ this._injector = inject(Injector);
865
+ this._modalStack = inject(HubModalStack);
866
+ this._config = inject(HubModalConfig);
867
+ }
868
+ /**
869
+ * Opens a new modal window with the specified content and supplied options.
870
+ *
871
+ * Content can be provided as a `TemplateRef` or a component type. If you pass a component type as content,
872
+ * then instances of those components can be injected with an instance of the `HubActiveModal` class. You can then
873
+ * use `HubActiveModal` methods to close / dismiss modals from "inside" of your component.
874
+ *
875
+ * Also see the [`HubModalOptions`](#/components/modal/api#HubModalOptions) for the list of supported options.
876
+ */
877
+ open(content, options = {}) {
878
+ const combinedOptions = { ...this._config, animation: this._config.animation, ...options };
879
+ return this._modalStack.open(this._injector, content, combinedOptions);
880
+ }
881
+ /**
882
+ * Returns an observable that holds the active modal instances.
883
+ */
884
+ get activeInstances() {
885
+ return this._modalStack.activeInstances;
886
+ }
887
+ /**
888
+ * Dismisses all currently displayed modal windows with the supplied reason.
889
+ *
890
+ * @since 3.1.0
891
+ */
892
+ dismissAll(reason) {
893
+ this._modalStack.dismissAll(reason);
894
+ }
895
+ /**
896
+ * Indicates if there are currently any open modal windows in the application.
897
+ *
898
+ * @since 3.3.0
899
+ */
900
+ hasOpenModals() {
901
+ return this._modalStack.hasOpenModals();
902
+ }
903
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubModal, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
904
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubModal, providedIn: 'root' }); }
905
+ }
906
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.5", ngImport: i0, type: HubModal, decorators: [{
907
+ type: Injectable,
908
+ args: [{ providedIn: 'root' }]
909
+ }] });
910
+
911
+ /*
912
+ * Public API Surface of modal
913
+ */
914
+
915
+ /**
916
+ * Generated bundle index. Do not edit.
917
+ */
918
+
919
+ export { HubModal };
920
+ //# sourceMappingURL=ng-hub-ui-modal.mjs.map