mn-angular-lib 1.0.135 → 1.0.137
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.
- package/fesm2022/mn-angular-lib.mjs +859 -428
- package/fesm2022/mn-angular-lib.mjs.map +1 -1
- package/package.json +8 -2
- package/src/lib/features/mn-rich-text-editor/mn-rich-text-editor.component.css +36 -0
- package/src/lib/styles/index.css +1 -0
- package/types/mn-angular-lib.d.ts +356 -108
- package/src/lib/features/mn-grid/mn-grid.component.css +0 -63
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, Injectable, inject, HostBinding, Input, Component,
|
|
2
|
+
import { InjectionToken, Injectable, inject, HostBinding, Input, Component, ApplicationRef, APP_INITIALIZER, Pipe, ChangeDetectionStrategy, signal, DestroyRef, Optional, SkipSelf, Attribute, Directive, ElementRef, EventEmitter, computed, Output, input, output, Injector, viewChildren, linkedSignal, afterNextRender, Renderer2, ChangeDetectorRef, HostListener, ViewChild, ViewContainerRef, forwardRef, afterEveryRender, TemplateRef, ViewChildren, viewChild, EnvironmentInjector, createComponent, isSignal, effect, untracked, ViewEncapsulation } from '@angular/core';
|
|
3
3
|
export { TemplateRef, Type } from '@angular/core';
|
|
4
4
|
import { BehaviorSubject, firstValueFrom, skip, Subject, debounceTime, of, takeUntil, map, catchError } from 'rxjs';
|
|
5
5
|
import * as i1 from '@angular/common';
|
|
6
6
|
import { CommonModule, NgClass, NgOptimizedImage, NgTemplateOutlet } from '@angular/common';
|
|
7
7
|
import { tv } from 'tailwind-variants';
|
|
8
|
+
import { HttpClient, HttpErrorResponse, HttpStatusCode, HttpParams } from '@angular/common/http';
|
|
8
9
|
import * as i1$1 from '@angular/forms';
|
|
9
10
|
import { NgControl, Validators, FormsModule, NG_VALUE_ACCESSOR, FormBuilder, ReactiveFormsModule } from '@angular/forms';
|
|
10
|
-
import { HttpClient, HttpErrorResponse, HttpStatusCode, HttpParams } from '@angular/common/http';
|
|
11
11
|
import JSON5 from 'json5';
|
|
12
12
|
import { LucideFile, LucideImagePlus, LucideTrash2, LucideUpload, LucideX, LucideCalendarDays, LucideChevronLeft, LucideChevronRight, LucideChevronDown, LucideArrowLeft, LucideArrowRight, LucideCheck, LucideInbox, LucideFilter, LucideDynamicIcon, LucideFunnel, LucideTriangleAlert, LucideCircleAlert } from '@lucide/angular';
|
|
13
13
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
@@ -442,7 +442,243 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
442
442
|
args: ['attr.tabindex']
|
|
443
443
|
}] } });
|
|
444
444
|
|
|
445
|
+
/**
|
|
446
|
+
* Type guard: checks whether a value is a translatable marker object.
|
|
447
|
+
*/
|
|
448
|
+
function isTranslatable(value) {
|
|
449
|
+
return (typeof value === 'object' &&
|
|
450
|
+
value !== null &&
|
|
451
|
+
typeof value['$translate'] === 'string');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
class MnLanguageService {
|
|
455
|
+
http = inject(HttpClient);
|
|
456
|
+
appRef = inject(ApplicationRef);
|
|
457
|
+
_translations = {};
|
|
458
|
+
_locale$ = new BehaviorSubject('en');
|
|
459
|
+
_urlPattern = null;
|
|
460
|
+
_debug = false;
|
|
461
|
+
/** Observable of the current active locale. */
|
|
462
|
+
locale$ = this._locale$.asObservable();
|
|
463
|
+
/** Current active locale. */
|
|
464
|
+
get locale() {
|
|
465
|
+
return this._locale$.value;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Enable or disable debug logging.
|
|
469
|
+
*/
|
|
470
|
+
setDebug(enabled) {
|
|
471
|
+
this._debug = enabled;
|
|
472
|
+
if (enabled) {
|
|
473
|
+
console.log(`[MnLanguage] Debug mode enabled`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Configure the URL pattern used to fetch translation files.
|
|
478
|
+
* Use `{locale}` as placeholder, e.g. `"assets/i18n/{locale}.json"`.
|
|
479
|
+
*/
|
|
480
|
+
configure(urlPattern) {
|
|
481
|
+
if (this._debug) {
|
|
482
|
+
console.log(`[MnLanguage] Configured urlPattern: ${urlPattern}`);
|
|
483
|
+
}
|
|
484
|
+
this._urlPattern = urlPattern;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Load translations for a locale from the configured URL pattern.
|
|
488
|
+
* If translations are already loaded for this locale, this is a no-op.
|
|
489
|
+
*/
|
|
490
|
+
async loadLocale(locale) {
|
|
491
|
+
if (this._translations[locale])
|
|
492
|
+
return;
|
|
493
|
+
if (!this._urlPattern) {
|
|
494
|
+
console.warn(`[MnLanguage] No URL pattern configured. Call configure() or use provideMnLanguage().`);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const url = this._urlPattern.replace('{locale}', locale);
|
|
498
|
+
if (this._debug) {
|
|
499
|
+
console.log(`[MnLanguage] Loading locale "${locale}" from ${url}`);
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
const map = await firstValueFrom(this.http.get(url));
|
|
503
|
+
this._translations[locale] = map ?? {};
|
|
504
|
+
if (this._debug) {
|
|
505
|
+
console.log(`[MnLanguage] Loaded locale "${locale}"`, this._translations[locale]);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
catch (err) {
|
|
509
|
+
console.warn(`[MnLanguage] Failed to load translations from ${url}`, err);
|
|
510
|
+
this._translations[locale] = {};
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Switch the active locale. Loads translations if not yet loaded.
|
|
515
|
+
*/
|
|
516
|
+
async setLocale(locale) {
|
|
517
|
+
if (this._debug) {
|
|
518
|
+
console.log(`[MnLanguage] Setting locale to "${locale}"`);
|
|
519
|
+
}
|
|
520
|
+
await this.loadLocale(locale);
|
|
521
|
+
this._locale$.next(locale);
|
|
522
|
+
this.appRef.tick();
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Register translations for a locale directly from code (no HTTP needed).
|
|
526
|
+
*/
|
|
527
|
+
registerTranslations(locale, translations) {
|
|
528
|
+
this._translations[locale] = {
|
|
529
|
+
...(this._translations[locale] ?? {}),
|
|
530
|
+
...translations,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Translate a key using the current locale, with optional parameter interpolation.
|
|
535
|
+
* Falls back to the key itself if no translation is found.
|
|
536
|
+
*
|
|
537
|
+
* Interpolation replaces `{{paramName}}` with the provided value.
|
|
538
|
+
*/
|
|
539
|
+
translate(key, params) {
|
|
540
|
+
const map = this._translations[this.locale] ?? {};
|
|
541
|
+
let value = this.getValueFromMap(map, key);
|
|
542
|
+
if (value === undefined) {
|
|
543
|
+
if (this._debug) {
|
|
544
|
+
console.warn(`[MnLanguage] Missing translation for key: "${key}" in locale: "${this.locale}"`);
|
|
545
|
+
}
|
|
546
|
+
return key;
|
|
547
|
+
}
|
|
548
|
+
if (params) {
|
|
549
|
+
for (const [paramKey, paramValue] of Object.entries(params)) {
|
|
550
|
+
value = value.replace(new RegExp(`\\{\\{${paramKey}\\}\\}`, 'g'), String(paramValue));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return value;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Helper to retrieve a value from a potentially nested translation map using a dot-notated key.
|
|
557
|
+
*/
|
|
558
|
+
getValueFromMap(map, key) {
|
|
559
|
+
if (map[key] !== undefined)
|
|
560
|
+
return map[key];
|
|
561
|
+
const parts = key.split('.');
|
|
562
|
+
let current = map;
|
|
563
|
+
for (const part of parts) {
|
|
564
|
+
if (current === null || typeof current !== 'object')
|
|
565
|
+
return undefined;
|
|
566
|
+
current = current[part];
|
|
567
|
+
}
|
|
568
|
+
return typeof current === 'string' ? current : undefined;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Translate a key **only if it is defined**, returning `undefined` otherwise.
|
|
572
|
+
*
|
|
573
|
+
* {@link translate} deliberately returns the key itself when it is missing, which
|
|
574
|
+
* makes it unusable for a library's own default labels: a consumer that never
|
|
575
|
+
* defined `mnCollection.rowsPerPage` would see that raw string in their UI. This
|
|
576
|
+
* lets a caller try a conventional key and fall back to a readable English default
|
|
577
|
+
* when the app has not translated it, so components ship translatable strings
|
|
578
|
+
* without forcing every consumer to define them.
|
|
579
|
+
*
|
|
580
|
+
* @param key The dot-notated translation key.
|
|
581
|
+
* @param params Optional `{{name}}` interpolation values.
|
|
582
|
+
* @returns The translation, or `undefined` when the key is not defined.
|
|
583
|
+
*/
|
|
584
|
+
translateIfPresent(key, params) {
|
|
585
|
+
const map = this._translations[this.locale] ?? {};
|
|
586
|
+
if (this.getValueFromMap(map, key) === undefined)
|
|
587
|
+
return undefined;
|
|
588
|
+
return this.translate(key, params);
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Shorthand alias for `translate`.
|
|
592
|
+
*/
|
|
593
|
+
t(key, params) {
|
|
594
|
+
return this.translate(key, params);
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Resolve the effective default locale from a domain-to-locale map.
|
|
598
|
+
* Matches `window.location.hostname` against the map keys.
|
|
599
|
+
* Returns the mapped locale, or the provided fallback if no match is found.
|
|
600
|
+
*/
|
|
601
|
+
resolveLocaleForDomain(domainLocaleMap, fallback) {
|
|
602
|
+
if (!domainLocaleMap || typeof window === 'undefined')
|
|
603
|
+
return fallback;
|
|
604
|
+
const hostname = window.location.hostname;
|
|
605
|
+
return domainLocaleMap[hostname] ?? fallback;
|
|
606
|
+
}
|
|
607
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnLanguageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
608
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnLanguageService, providedIn: 'root' });
|
|
609
|
+
}
|
|
610
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnLanguageService, decorators: [{
|
|
611
|
+
type: Injectable,
|
|
612
|
+
args: [{ providedIn: 'root' }]
|
|
613
|
+
}] });
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Provides an APP_INITIALIZER that configures the MnLanguageService and
|
|
617
|
+
* preloads the requested locales during application bootstrap.
|
|
618
|
+
*
|
|
619
|
+
* Usage in app.config.ts:
|
|
620
|
+
* ...provideMnLanguage({
|
|
621
|
+
* urlPattern: 'assets/i18n/{locale}.json',
|
|
622
|
+
* defaultLocale: 'en',
|
|
623
|
+
* preload: ['en', 'nl'],
|
|
624
|
+
* })
|
|
625
|
+
*/
|
|
626
|
+
function provideMnLanguage(config) {
|
|
627
|
+
return [
|
|
628
|
+
{
|
|
629
|
+
provide: APP_INITIALIZER,
|
|
630
|
+
multi: true,
|
|
631
|
+
useFactory: (svc) => async () => {
|
|
632
|
+
if (config.debug) {
|
|
633
|
+
svc.setDebug(true);
|
|
634
|
+
}
|
|
635
|
+
svc.configure(config.urlPattern);
|
|
636
|
+
const effectiveLocale = svc.resolveLocaleForDomain(config.domainLocaleMap, config.defaultLocale);
|
|
637
|
+
const localesToLoad = config.preload ?? [effectiveLocale];
|
|
638
|
+
await Promise.all(localesToLoad.map(l => svc.loadLocale(l)));
|
|
639
|
+
await svc.setLocale(effectiveLocale);
|
|
640
|
+
},
|
|
641
|
+
deps: [MnLanguageService],
|
|
642
|
+
},
|
|
643
|
+
];
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Pipe that translates a key via MnLanguageService.
|
|
648
|
+
*
|
|
649
|
+
* Usage in templates:
|
|
650
|
+
* {{ 'form.email.label' | mnTranslate }}
|
|
651
|
+
* {{ 'greeting' | mnTranslate:{ name: 'World' } }}
|
|
652
|
+
*
|
|
653
|
+
* Note: This pipe is impure so it re-evaluates when the locale changes.
|
|
654
|
+
*/
|
|
655
|
+
class MnTranslatePipe {
|
|
656
|
+
lang = inject(MnLanguageService);
|
|
657
|
+
transform(key, params) {
|
|
658
|
+
return this.lang.translate(key, params);
|
|
659
|
+
}
|
|
660
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
661
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, isStandalone: true, name: "mnTranslate", pure: false });
|
|
662
|
+
}
|
|
663
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, decorators: [{
|
|
664
|
+
type: Pipe,
|
|
665
|
+
args: [{
|
|
666
|
+
name: 'mnTranslate',
|
|
667
|
+
standalone: true,
|
|
668
|
+
pure: false,
|
|
669
|
+
}]
|
|
670
|
+
}] });
|
|
671
|
+
|
|
445
672
|
class MnAlertOutletComponent {
|
|
673
|
+
lang = inject(MnLanguageService);
|
|
674
|
+
/**
|
|
675
|
+
* Accessible name for this control. Resolved through the conventional
|
|
676
|
+
* `mnAlert.close` key so an app can translate it, falling back to English when the
|
|
677
|
+
* key is not defined rather than leaking the raw key into the UI.
|
|
678
|
+
*/
|
|
679
|
+
get closeLabel() {
|
|
680
|
+
return this.lang.translateIfPresent('mnAlert.close') ?? 'Close';
|
|
681
|
+
}
|
|
446
682
|
template;
|
|
447
683
|
store = inject(MnAlertStore);
|
|
448
684
|
alerts$ = this.store.alerts$;
|
|
@@ -462,11 +698,11 @@ class MnAlertOutletComponent {
|
|
|
462
698
|
};
|
|
463
699
|
}
|
|
464
700
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnAlertOutletComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
465
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnAlertOutletComponent, isStandalone: true, selector: "mn-alert-outlet", inputs: { template: "template" }, ngImport: i0, template: "@if (alerts$ | async; as alerts) {\n <div class=\"fixed top-4 left-0 right-0 z-[9999] flex flex-col gap-3 w-full px-2 sm:px-0 sm:left-auto sm:right-4 sm:max-w-sm\">\n @for (a of alerts; track trackById($index, a)) {\n @if (template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"contextFor(a)\">\n </ng-container>\n } @else {\n <div [class]=\"getAlertClasses(a)\" [class.extra]=\"a.cssClass\" class=\"relative\">\n <div class=\"flex-1 min-w-0 pr-8\">\n <h4 class=\"font-semibold text-sm\">{{ a.title }}</h4>\n @if (a.subTitle) {\n <p class=\"text-sm mt-1 opacity-90 leading-tight\">{{ a.subTitle }}</p>\n }\n </div>\n <button\n mnButton\n [data]=\"{ size: 'md', variant: 'text' }\"\n (click)=\"dismissAlert(a.id)\"\n class=\"absolute top-2 right-2 shrink-0 !text-current hover:!bg-current/10\"\n
|
|
701
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnAlertOutletComponent, isStandalone: true, selector: "mn-alert-outlet", inputs: { template: "template" }, ngImport: i0, template: "@if (alerts$ | async; as alerts) {\n <div class=\"fixed top-4 left-0 right-0 z-[9999] flex flex-col gap-3 w-full px-2 sm:px-0 sm:left-auto sm:right-4 sm:max-w-sm\">\n @for (a of alerts; track trackById($index, a)) {\n @if (template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"contextFor(a)\">\n </ng-container>\n } @else {\n <div [class]=\"getAlertClasses(a)\" [class.extra]=\"a.cssClass\" class=\"relative\">\n <div class=\"flex-1 min-w-0 pr-8\">\n <h4 class=\"font-semibold text-sm\">{{ a.title }}</h4>\n @if (a.subTitle) {\n <p class=\"text-sm mt-1 opacity-90 leading-tight\">{{ a.subTitle }}</p>\n }\n </div>\n <button\n [attr.aria-label]=\"closeLabel\"\n mnButton\n [data]=\"{ size: 'md', variant: 'text' }\"\n (click)=\"dismissAlert(a.id)\"\n class=\"absolute top-2 right-2 shrink-0 !text-current hover:!bg-current/10\"\n type=\"button\"\n >\n ×\n </button>\n </div>\n }\n }\n </div>\n}\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
466
702
|
}
|
|
467
703
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnAlertOutletComponent, decorators: [{
|
|
468
704
|
type: Component,
|
|
469
|
-
args: [{ selector: 'mn-alert-outlet', standalone: true, imports: [CommonModule, MnButton], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (alerts$ | async; as alerts) {\n <div class=\"fixed top-4 left-0 right-0 z-[9999] flex flex-col gap-3 w-full px-2 sm:px-0 sm:left-auto sm:right-4 sm:max-w-sm\">\n @for (a of alerts; track trackById($index, a)) {\n @if (template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"contextFor(a)\">\n </ng-container>\n } @else {\n <div [class]=\"getAlertClasses(a)\" [class.extra]=\"a.cssClass\" class=\"relative\">\n <div class=\"flex-1 min-w-0 pr-8\">\n <h4 class=\"font-semibold text-sm\">{{ a.title }}</h4>\n @if (a.subTitle) {\n <p class=\"text-sm mt-1 opacity-90 leading-tight\">{{ a.subTitle }}</p>\n }\n </div>\n <button\n mnButton\n [data]=\"{ size: 'md', variant: 'text' }\"\n (click)=\"dismissAlert(a.id)\"\n class=\"absolute top-2 right-2 shrink-0 !text-current hover:!bg-current/10\"\n
|
|
705
|
+
args: [{ selector: 'mn-alert-outlet', standalone: true, imports: [CommonModule, MnButton], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (alerts$ | async; as alerts) {\n <div class=\"fixed top-4 left-0 right-0 z-[9999] flex flex-col gap-3 w-full px-2 sm:px-0 sm:left-auto sm:right-4 sm:max-w-sm\">\n @for (a of alerts; track trackById($index, a)) {\n @if (template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"contextFor(a)\">\n </ng-container>\n } @else {\n <div [class]=\"getAlertClasses(a)\" [class.extra]=\"a.cssClass\" class=\"relative\">\n <div class=\"flex-1 min-w-0 pr-8\">\n <h4 class=\"font-semibold text-sm\">{{ a.title }}</h4>\n @if (a.subTitle) {\n <p class=\"text-sm mt-1 opacity-90 leading-tight\">{{ a.subTitle }}</p>\n }\n </div>\n <button\n [attr.aria-label]=\"closeLabel\"\n mnButton\n [data]=\"{ size: 'md', variant: 'text' }\"\n (click)=\"dismissAlert(a.id)\"\n class=\"absolute top-2 right-2 shrink-0 !text-current hover:!bg-current/10\"\n type=\"button\"\n >\n ×\n </button>\n </div>\n }\n }\n </div>\n}\n" }]
|
|
470
706
|
}], ctorParameters: () => [], propDecorators: { template: [{
|
|
471
707
|
type: Input
|
|
472
708
|
}] } });
|
|
@@ -791,218 +1027,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
791
1027
|
}], id: [{
|
|
792
1028
|
type: Input,
|
|
793
1029
|
args: [{ required: true }]
|
|
794
|
-
}] } });
|
|
795
|
-
|
|
796
|
-
/**
|
|
797
|
-
* Types for mn-lib configuration.
|
|
798
|
-
*/
|
|
799
|
-
|
|
800
|
-
/**
|
|
801
|
-
* Type guard: checks whether a value is a translatable marker object.
|
|
802
|
-
*/
|
|
803
|
-
function isTranslatable(value) {
|
|
804
|
-
return (typeof value === 'object' &&
|
|
805
|
-
value !== null &&
|
|
806
|
-
typeof value['$translate'] === 'string');
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
class MnLanguageService {
|
|
810
|
-
http = inject(HttpClient);
|
|
811
|
-
appRef = inject(ApplicationRef);
|
|
812
|
-
_translations = {};
|
|
813
|
-
_locale$ = new BehaviorSubject('en');
|
|
814
|
-
_urlPattern = null;
|
|
815
|
-
_debug = false;
|
|
816
|
-
/** Observable of the current active locale. */
|
|
817
|
-
locale$ = this._locale$.asObservable();
|
|
818
|
-
/** Current active locale. */
|
|
819
|
-
get locale() {
|
|
820
|
-
return this._locale$.value;
|
|
821
|
-
}
|
|
822
|
-
/**
|
|
823
|
-
* Enable or disable debug logging.
|
|
824
|
-
*/
|
|
825
|
-
setDebug(enabled) {
|
|
826
|
-
this._debug = enabled;
|
|
827
|
-
if (enabled) {
|
|
828
|
-
console.log(`[MnLanguage] Debug mode enabled`);
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
/**
|
|
832
|
-
* Configure the URL pattern used to fetch translation files.
|
|
833
|
-
* Use `{locale}` as placeholder, e.g. `"assets/i18n/{locale}.json"`.
|
|
834
|
-
*/
|
|
835
|
-
configure(urlPattern) {
|
|
836
|
-
if (this._debug) {
|
|
837
|
-
console.log(`[MnLanguage] Configured urlPattern: ${urlPattern}`);
|
|
838
|
-
}
|
|
839
|
-
this._urlPattern = urlPattern;
|
|
840
|
-
}
|
|
841
|
-
/**
|
|
842
|
-
* Load translations for a locale from the configured URL pattern.
|
|
843
|
-
* If translations are already loaded for this locale, this is a no-op.
|
|
844
|
-
*/
|
|
845
|
-
async loadLocale(locale) {
|
|
846
|
-
if (this._translations[locale])
|
|
847
|
-
return;
|
|
848
|
-
if (!this._urlPattern) {
|
|
849
|
-
console.warn(`[MnLanguage] No URL pattern configured. Call configure() or use provideMnLanguage().`);
|
|
850
|
-
return;
|
|
851
|
-
}
|
|
852
|
-
const url = this._urlPattern.replace('{locale}', locale);
|
|
853
|
-
if (this._debug) {
|
|
854
|
-
console.log(`[MnLanguage] Loading locale "${locale}" from ${url}`);
|
|
855
|
-
}
|
|
856
|
-
try {
|
|
857
|
-
const map = await firstValueFrom(this.http.get(url));
|
|
858
|
-
this._translations[locale] = map ?? {};
|
|
859
|
-
if (this._debug) {
|
|
860
|
-
console.log(`[MnLanguage] Loaded locale "${locale}"`, this._translations[locale]);
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
catch (err) {
|
|
864
|
-
console.warn(`[MnLanguage] Failed to load translations from ${url}`, err);
|
|
865
|
-
this._translations[locale] = {};
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
/**
|
|
869
|
-
* Switch the active locale. Loads translations if not yet loaded.
|
|
870
|
-
*/
|
|
871
|
-
async setLocale(locale) {
|
|
872
|
-
if (this._debug) {
|
|
873
|
-
console.log(`[MnLanguage] Setting locale to "${locale}"`);
|
|
874
|
-
}
|
|
875
|
-
await this.loadLocale(locale);
|
|
876
|
-
this._locale$.next(locale);
|
|
877
|
-
this.appRef.tick();
|
|
878
|
-
}
|
|
879
|
-
/**
|
|
880
|
-
* Register translations for a locale directly from code (no HTTP needed).
|
|
881
|
-
*/
|
|
882
|
-
registerTranslations(locale, translations) {
|
|
883
|
-
this._translations[locale] = {
|
|
884
|
-
...(this._translations[locale] ?? {}),
|
|
885
|
-
...translations,
|
|
886
|
-
};
|
|
887
|
-
}
|
|
888
|
-
/**
|
|
889
|
-
* Translate a key using the current locale, with optional parameter interpolation.
|
|
890
|
-
* Falls back to the key itself if no translation is found.
|
|
891
|
-
*
|
|
892
|
-
* Interpolation replaces `{{paramName}}` with the provided value.
|
|
893
|
-
*/
|
|
894
|
-
translate(key, params) {
|
|
895
|
-
const map = this._translations[this.locale] ?? {};
|
|
896
|
-
let value = this.getValueFromMap(map, key);
|
|
897
|
-
if (value === undefined) {
|
|
898
|
-
if (this._debug) {
|
|
899
|
-
console.warn(`[MnLanguage] Missing translation for key: "${key}" in locale: "${this.locale}"`);
|
|
900
|
-
}
|
|
901
|
-
return key;
|
|
902
|
-
}
|
|
903
|
-
if (params) {
|
|
904
|
-
for (const [paramKey, paramValue] of Object.entries(params)) {
|
|
905
|
-
value = value.replace(new RegExp(`\\{\\{${paramKey}\\}\\}`, 'g'), String(paramValue));
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
return value;
|
|
909
|
-
}
|
|
910
|
-
/**
|
|
911
|
-
* Helper to retrieve a value from a potentially nested translation map using a dot-notated key.
|
|
912
|
-
*/
|
|
913
|
-
getValueFromMap(map, key) {
|
|
914
|
-
if (map[key] !== undefined)
|
|
915
|
-
return map[key];
|
|
916
|
-
const parts = key.split('.');
|
|
917
|
-
let current = map;
|
|
918
|
-
for (const part of parts) {
|
|
919
|
-
if (current === null || typeof current !== 'object')
|
|
920
|
-
return undefined;
|
|
921
|
-
current = current[part];
|
|
922
|
-
}
|
|
923
|
-
return typeof current === 'string' ? current : undefined;
|
|
924
|
-
}
|
|
925
|
-
/**
|
|
926
|
-
* Shorthand alias for `translate`.
|
|
927
|
-
*/
|
|
928
|
-
t(key, params) {
|
|
929
|
-
return this.translate(key, params);
|
|
930
|
-
}
|
|
931
|
-
/**
|
|
932
|
-
* Resolve the effective default locale from a domain-to-locale map.
|
|
933
|
-
* Matches `window.location.hostname` against the map keys.
|
|
934
|
-
* Returns the mapped locale, or the provided fallback if no match is found.
|
|
935
|
-
*/
|
|
936
|
-
resolveLocaleForDomain(domainLocaleMap, fallback) {
|
|
937
|
-
if (!domainLocaleMap || typeof window === 'undefined')
|
|
938
|
-
return fallback;
|
|
939
|
-
const hostname = window.location.hostname;
|
|
940
|
-
return domainLocaleMap[hostname] ?? fallback;
|
|
941
|
-
}
|
|
942
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnLanguageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
943
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnLanguageService, providedIn: 'root' });
|
|
944
|
-
}
|
|
945
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnLanguageService, decorators: [{
|
|
946
|
-
type: Injectable,
|
|
947
|
-
args: [{ providedIn: 'root' }]
|
|
948
|
-
}] });
|
|
949
|
-
|
|
950
|
-
/**
|
|
951
|
-
* Provides an APP_INITIALIZER that configures the MnLanguageService and
|
|
952
|
-
* preloads the requested locales during application bootstrap.
|
|
953
|
-
*
|
|
954
|
-
* Usage in app.config.ts:
|
|
955
|
-
* ...provideMnLanguage({
|
|
956
|
-
* urlPattern: 'assets/i18n/{locale}.json',
|
|
957
|
-
* defaultLocale: 'en',
|
|
958
|
-
* preload: ['en', 'nl'],
|
|
959
|
-
* })
|
|
960
|
-
*/
|
|
961
|
-
function provideMnLanguage(config) {
|
|
962
|
-
return [
|
|
963
|
-
{
|
|
964
|
-
provide: APP_INITIALIZER,
|
|
965
|
-
multi: true,
|
|
966
|
-
useFactory: (svc) => async () => {
|
|
967
|
-
if (config.debug) {
|
|
968
|
-
svc.setDebug(true);
|
|
969
|
-
}
|
|
970
|
-
svc.configure(config.urlPattern);
|
|
971
|
-
const effectiveLocale = svc.resolveLocaleForDomain(config.domainLocaleMap, config.defaultLocale);
|
|
972
|
-
const localesToLoad = config.preload ?? [effectiveLocale];
|
|
973
|
-
await Promise.all(localesToLoad.map(l => svc.loadLocale(l)));
|
|
974
|
-
await svc.setLocale(effectiveLocale);
|
|
975
|
-
},
|
|
976
|
-
deps: [MnLanguageService],
|
|
977
|
-
},
|
|
978
|
-
];
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
/**
|
|
982
|
-
* Pipe that translates a key via MnLanguageService.
|
|
983
|
-
*
|
|
984
|
-
* Usage in templates:
|
|
985
|
-
* {{ 'form.email.label' | mnTranslate }}
|
|
986
|
-
* {{ 'greeting' | mnTranslate:{ name: 'World' } }}
|
|
987
|
-
*
|
|
988
|
-
* Note: This pipe is impure so it re-evaluates when the locale changes.
|
|
1030
|
+
}] } });
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Types for mn-lib configuration.
|
|
989
1034
|
*/
|
|
990
|
-
class MnTranslatePipe {
|
|
991
|
-
lang = inject(MnLanguageService);
|
|
992
|
-
transform(key, params) {
|
|
993
|
-
return this.lang.translate(key, params);
|
|
994
|
-
}
|
|
995
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
|
|
996
|
-
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, isStandalone: true, name: "mnTranslate", pure: false });
|
|
997
|
-
}
|
|
998
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, decorators: [{
|
|
999
|
-
type: Pipe,
|
|
1000
|
-
args: [{
|
|
1001
|
-
name: 'mnTranslate',
|
|
1002
|
-
standalone: true,
|
|
1003
|
-
pure: false,
|
|
1004
|
-
}]
|
|
1005
|
-
}] });
|
|
1006
1035
|
|
|
1007
1036
|
function isPlainObject(value) {
|
|
1008
1037
|
return (typeof value === 'object' &&
|
|
@@ -5036,10 +5065,6 @@ function emptyFilterValue(type) {
|
|
|
5036
5065
|
switch (type) {
|
|
5037
5066
|
case 'multi-select':
|
|
5038
5067
|
return [];
|
|
5039
|
-
case 'number-range':
|
|
5040
|
-
return {};
|
|
5041
|
-
case 'date-range':
|
|
5042
|
-
return {};
|
|
5043
5068
|
case 'boolean':
|
|
5044
5069
|
case 'text':
|
|
5045
5070
|
case 'select':
|
|
@@ -5048,10 +5073,10 @@ function emptyFilterValue(type) {
|
|
|
5048
5073
|
}
|
|
5049
5074
|
}
|
|
5050
5075
|
/**
|
|
5051
|
-
* Whether a filter value should actually narrow the rows. Empty strings
|
|
5052
|
-
* arrays
|
|
5053
|
-
*
|
|
5054
|
-
*
|
|
5076
|
+
* Whether a filter value should actually narrow the rows. Empty strings and
|
|
5077
|
+
* empty arrays are inactive; `false` on a boolean filter is active (it means
|
|
5078
|
+
* "show only the false rows"), which is why a plain truthiness check is not
|
|
5079
|
+
* enough.
|
|
5055
5080
|
*/
|
|
5056
5081
|
function isFilterValueActive(value) {
|
|
5057
5082
|
if (value === undefined || value === null)
|
|
@@ -5060,10 +5085,7 @@ function isFilterValueActive(value) {
|
|
|
5060
5085
|
return true;
|
|
5061
5086
|
if (typeof value === 'string')
|
|
5062
5087
|
return value.trim().length > 0;
|
|
5063
|
-
|
|
5064
|
-
return value.length > 0;
|
|
5065
|
-
const range = value;
|
|
5066
|
-
return [range.min, range.max, range.from, range.to].some(bound => bound !== undefined && bound !== null && bound !== '' && !Number.isNaN(bound));
|
|
5088
|
+
return Array.isArray(value) && value.length > 0;
|
|
5067
5089
|
}
|
|
5068
5090
|
/**
|
|
5069
5091
|
* The value a filter compares against for a row: the column's
|
|
@@ -5077,21 +5099,6 @@ function resolveFilterableValue(column, row) {
|
|
|
5077
5099
|
return column.cell(row);
|
|
5078
5100
|
return '';
|
|
5079
5101
|
}
|
|
5080
|
-
/** Parses a `YYYY-MM-DD` bound to a timestamp; `endOfDay` makes the upper bound inclusive. */
|
|
5081
|
-
function parseDateBound(bound, endOfDay) {
|
|
5082
|
-
if (!bound)
|
|
5083
|
-
return undefined;
|
|
5084
|
-
const time = new Date(endOfDay ? `${bound}T23:59:59.999` : `${bound}T00:00:00.000`).getTime();
|
|
5085
|
-
return Number.isNaN(time) ? undefined : time;
|
|
5086
|
-
}
|
|
5087
|
-
/** Coerces a raw cell value to a timestamp for date-range comparison. */
|
|
5088
|
-
function toTimestamp(raw) {
|
|
5089
|
-
if (raw instanceof Date)
|
|
5090
|
-
return raw.getTime();
|
|
5091
|
-
if (typeof raw === 'number')
|
|
5092
|
-
return raw;
|
|
5093
|
-
return new Date(String(raw)).getTime();
|
|
5094
|
-
}
|
|
5095
5102
|
/**
|
|
5096
5103
|
* The default predicate for a filter type, used when the column supplies no
|
|
5097
5104
|
* `filterFn`. Semantics per type:
|
|
@@ -5099,11 +5106,6 @@ function toTimestamp(raw) {
|
|
|
5099
5106
|
* - `select` — exact string equality
|
|
5100
5107
|
* - `multi-select` — equality against any selected value (OR)
|
|
5101
5108
|
* - `boolean` — truthiness of the raw value equals the chosen state
|
|
5102
|
-
* - `number-range` / `date-range` — inclusive bounds, each side optional
|
|
5103
|
-
*
|
|
5104
|
-
* A row whose raw value cannot be interpreted for the type (a non-numeric value
|
|
5105
|
-
* under a number range, an unparsable date) is excluded rather than kept, so an
|
|
5106
|
-
* active filter never silently passes rows it cannot evaluate.
|
|
5107
5109
|
*/
|
|
5108
5110
|
function defaultFilterPredicate(type, raw, value) {
|
|
5109
5111
|
switch (type) {
|
|
@@ -5115,26 +5117,6 @@ function defaultFilterPredicate(type, raw, value) {
|
|
|
5115
5117
|
}
|
|
5116
5118
|
case 'boolean':
|
|
5117
5119
|
return Boolean(raw) === value;
|
|
5118
|
-
case 'number-range': {
|
|
5119
|
-
const { min, max } = value;
|
|
5120
|
-
const numeric = typeof raw === 'number' ? raw : Number(raw);
|
|
5121
|
-
if (Number.isNaN(numeric))
|
|
5122
|
-
return false;
|
|
5123
|
-
if (min !== undefined && numeric < min)
|
|
5124
|
-
return false;
|
|
5125
|
-
return !(max !== undefined && numeric > max);
|
|
5126
|
-
}
|
|
5127
|
-
case 'date-range': {
|
|
5128
|
-
const { from, to } = value;
|
|
5129
|
-
const time = toTimestamp(raw);
|
|
5130
|
-
if (Number.isNaN(time))
|
|
5131
|
-
return false;
|
|
5132
|
-
const start = parseDateBound(from, false);
|
|
5133
|
-
const end = parseDateBound(to, true);
|
|
5134
|
-
if (start !== undefined && time < start)
|
|
5135
|
-
return false;
|
|
5136
|
-
return !(end !== undefined && time > end);
|
|
5137
|
-
}
|
|
5138
5120
|
case 'text':
|
|
5139
5121
|
default:
|
|
5140
5122
|
return String(raw ?? '')
|
|
@@ -5673,6 +5655,28 @@ class MnCollectionBase {
|
|
|
5673
5655
|
onRowsChanged() {
|
|
5674
5656
|
// no-op by default
|
|
5675
5657
|
}
|
|
5658
|
+
/**
|
|
5659
|
+
* Resolves a label three ways, in order: the consumer's explicit key, a
|
|
5660
|
+
* conventional `mnCollection.*` key when the app defines one, and finally a
|
|
5661
|
+
* readable English default.
|
|
5662
|
+
*
|
|
5663
|
+
* The middle step is what makes the components translatable out of the box: an
|
|
5664
|
+
* app that adds the `mnCollection` namespace to its locale files gets every
|
|
5665
|
+
* table, list and grid translated at once, with no per-call-site wiring across
|
|
5666
|
+
* dozens of data sources. An app that does not keeps today's English text rather
|
|
5667
|
+
* than leaking raw keys into the UI.
|
|
5668
|
+
*
|
|
5669
|
+
* @param consumerKey The data source's own translation key, if it set one.
|
|
5670
|
+
* @param defaultKey The conventional key this label falls back to.
|
|
5671
|
+
* @param fallback The English text used when neither key resolves.
|
|
5672
|
+
* @param params Optional interpolation values.
|
|
5673
|
+
* @returns The resolved label.
|
|
5674
|
+
*/
|
|
5675
|
+
resolveLabel(consumerKey, defaultKey, fallback, params) {
|
|
5676
|
+
if (consumerKey)
|
|
5677
|
+
return this.lang.t(consumerKey, params);
|
|
5678
|
+
return this.lang.translateIfPresent(defaultKey, params) ?? fallback;
|
|
5679
|
+
}
|
|
5676
5680
|
/**
|
|
5677
5681
|
* Resolves translation keys to display strings via {@link MnLanguageService}.
|
|
5678
5682
|
* Subclasses override to resolve their own keys; call `super` to keep these.
|
|
@@ -5841,12 +5845,6 @@ class MnSelectableCollectionBase extends MnCollectionBase {
|
|
|
5841
5845
|
* its ids matched a loaded row yet. See {@link beforeInitialFilter}.
|
|
5842
5846
|
*/
|
|
5843
5847
|
pendingInitialEmit = false;
|
|
5844
|
-
/**
|
|
5845
|
-
* The ids that arrived pre-selected, kept apart from {@link selectedIds} so
|
|
5846
|
-
* {@link prioritizeInitialSelection} has a set that does **not** move as the user
|
|
5847
|
-
* clicks. Null when the collection opened with nothing selected.
|
|
5848
|
-
*/
|
|
5849
|
-
pinnedSelectionIds = null;
|
|
5850
5848
|
/**
|
|
5851
5849
|
* Every selected row, in selection order, for the summary. Ids whose row was
|
|
5852
5850
|
* never seen are skipped rather than rendered as a bare id.
|
|
@@ -5866,7 +5864,21 @@ class MnSelectableCollectionBase extends MnCollectionBase {
|
|
|
5866
5864
|
}
|
|
5867
5865
|
/** How many tags to show before collapsing the remainder. */
|
|
5868
5866
|
get selectionSummaryLimit() {
|
|
5869
|
-
return this.dataSource.selectionSummaryLimit ??
|
|
5867
|
+
return this.dataSource.selectionSummaryLimit ?? this.defaultSelectionSummaryLimit;
|
|
5868
|
+
}
|
|
5869
|
+
/**
|
|
5870
|
+
* Tag count the summary collapses at when the data source names no limit.
|
|
5871
|
+
*
|
|
5872
|
+
* Subclasses that know their own width narrow this: the same eight tags that
|
|
5873
|
+
* read as a compact header on a wide table become seven stacked lines in a phone
|
|
5874
|
+
* sheet, pushing the rows they describe off screen. Overridden by
|
|
5875
|
+
* {@link MnCollectionDataSource.selectionSummaryLimit}.
|
|
5876
|
+
*/
|
|
5877
|
+
// Deliberately an accessor, not a readonly field: MnTable overrides it with a
|
|
5878
|
+
// width-dependent getter, and TypeScript cannot override a property with one.
|
|
5879
|
+
// eslint-disable-next-line @typescript-eslint/class-literal-property-style
|
|
5880
|
+
get defaultSelectionSummaryLimit() {
|
|
5881
|
+
return 8;
|
|
5870
5882
|
}
|
|
5871
5883
|
/**
|
|
5872
5884
|
* The tags to render: the first {@link selectionSummaryLimit} rows, or all of them
|
|
@@ -5971,34 +5983,6 @@ class MnSelectableCollectionBase extends MnCollectionBase {
|
|
|
5971
5983
|
defaultSelectionLabel(_row) {
|
|
5972
5984
|
return null;
|
|
5973
5985
|
}
|
|
5974
|
-
/**
|
|
5975
|
-
* Reorders rows so the ones that were already selected when the collection
|
|
5976
|
-
* opened come first, preserving the incoming order within each group.
|
|
5977
|
-
*
|
|
5978
|
-
* Deliberately keyed on the *initial* selection rather than the live one: pinning
|
|
5979
|
-
* what the user is currently ticking would make a row jump to the top the instant
|
|
5980
|
-
* it is clicked, moving the next row under the pointer mid-click. Freezing the set
|
|
5981
|
-
* answers the actual question — "what was already chosen?" — and leaves the list
|
|
5982
|
-
* still while it is being worked with. A row deselected during the session keeps
|
|
5983
|
-
* its place for the same reason.
|
|
5984
|
-
*
|
|
5985
|
-
* Callers apply this only when no explicit sort is active, so a sorted column
|
|
5986
|
-
* always wins.
|
|
5987
|
-
* @param items The rows in their current order.
|
|
5988
|
-
* @returns The rows with the initially-selected ones hoisted to the top.
|
|
5989
|
-
*/
|
|
5990
|
-
prioritizeInitialSelection(items) {
|
|
5991
|
-
const pinned = this.pinnedSelectionIds;
|
|
5992
|
-
if (!pinned?.size)
|
|
5993
|
-
return items;
|
|
5994
|
-
const selected = [];
|
|
5995
|
-
const rest = [];
|
|
5996
|
-
for (const item of items) {
|
|
5997
|
-
(pinned.has(this.dataSource.getID(item)) ? selected : rest).push(item);
|
|
5998
|
-
}
|
|
5999
|
-
// Nothing to hoist (e.g. the pinned rows are on another server-side page).
|
|
6000
|
-
return selected.length === 0 ? items : [...selected, ...rest];
|
|
6001
|
-
}
|
|
6002
5986
|
/** Seeds selection from `initialSelectedIds` before the first filter pass. */
|
|
6003
5987
|
beforeInitialFilter() {
|
|
6004
5988
|
super.beforeInitialFilter();
|
|
@@ -6007,7 +5991,6 @@ class MnSelectableCollectionBase extends MnCollectionBase {
|
|
|
6007
5991
|
for (const id of this.dataSource.initialSelectedIds) {
|
|
6008
5992
|
this.selectedIds.add(id);
|
|
6009
5993
|
}
|
|
6010
|
-
this.pinnedSelectionIds = new Set(this.dataSource.initialSelectedIds);
|
|
6011
5994
|
for (const row of this.dataSource.initialSelectedRows ?? []) {
|
|
6012
5995
|
this.selectedRowsById.set(this.dataSource.getID(row), row);
|
|
6013
5996
|
}
|
|
@@ -6070,6 +6053,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
6070
6053
|
* reacts to the outputs.
|
|
6071
6054
|
*/
|
|
6072
6055
|
class MnCollectionPagination {
|
|
6056
|
+
lang = inject(MnLanguageService);
|
|
6073
6057
|
/** Prefix for the page-size select's id, keeping it unique per host. */
|
|
6074
6058
|
idPrefix = 'mn-collection';
|
|
6075
6059
|
isPaginated = false;
|
|
@@ -6123,25 +6107,25 @@ class MnCollectionPagination {
|
|
|
6123
6107
|
}
|
|
6124
6108
|
return slots;
|
|
6125
6109
|
}
|
|
6126
|
-
/** Wrapper classes for a slot: anchors and their gaps are md+ only. */
|
|
6127
|
-
slotVisibility(slot) {
|
|
6128
|
-
return slot.anchor ? 'hidden md:inline-flex' : 'inline-flex';
|
|
6129
|
-
}
|
|
6130
6110
|
/** e.g. `Page 5 of 50`. */
|
|
6131
6111
|
get pageIndicatorLabel() {
|
|
6132
|
-
return this.fill(this.labels?.pageIndicator
|
|
6112
|
+
return this.fill(this.label(this.labels?.pageIndicator, 'mnCollection.pageIndicator', 'Page {{current}} of {{total}}'), {
|
|
6133
6113
|
current: this.currentPage,
|
|
6134
6114
|
total: this.totalPages,
|
|
6135
6115
|
});
|
|
6136
6116
|
}
|
|
6137
6117
|
/** e.g. `41–50 of 250`. */
|
|
6138
6118
|
get itemRangeLabel() {
|
|
6139
|
-
return this.fill(this.labels?.itemRange
|
|
6119
|
+
return this.fill(this.label(this.labels?.itemRange, 'mnCollection.itemRange', '{{start}}–{{end}} of {{total}}'), {
|
|
6140
6120
|
start: this.rangeStart,
|
|
6141
6121
|
end: this.rangeEnd,
|
|
6142
6122
|
total: this.totalItemCount,
|
|
6143
6123
|
});
|
|
6144
6124
|
}
|
|
6125
|
+
/** "Items per page" label beside the page-size selector. */
|
|
6126
|
+
get rowsPerPageLabel() {
|
|
6127
|
+
return this.label(this.labels?.rowsPerPage, 'mnCollection.rowsPerPage', 'Items per page:');
|
|
6128
|
+
}
|
|
6145
6129
|
/**
|
|
6146
6130
|
* Substitutes `{{name}}` placeholders, matching the interpolation syntax used
|
|
6147
6131
|
* by MnLanguageService so the same translation strings work either way.
|
|
@@ -6149,12 +6133,75 @@ class MnCollectionPagination {
|
|
|
6149
6133
|
fill(template, params) {
|
|
6150
6134
|
return Object.entries(params).reduce((result, [key, value]) => result.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), String(value)), template);
|
|
6151
6135
|
}
|
|
6136
|
+
/** Label for the load-more button. */
|
|
6137
|
+
get loadMoreLabel() {
|
|
6138
|
+
return this.label(this.labels?.loadMore, 'mnCollection.loadMore', 'Load more');
|
|
6139
|
+
}
|
|
6140
|
+
/** Accessible label for the first-page control. */
|
|
6141
|
+
get firstPageLabel() {
|
|
6142
|
+
return this.label(undefined, 'mnCollection.firstPage', 'First page');
|
|
6143
|
+
}
|
|
6144
|
+
/** Accessible label for the previous-page control. */
|
|
6145
|
+
get previousPageLabel() {
|
|
6146
|
+
return this.label(undefined, 'mnCollection.previousPage', 'Previous page');
|
|
6147
|
+
}
|
|
6148
|
+
/** Accessible label for the next-page control. */
|
|
6149
|
+
get nextPageLabel() {
|
|
6150
|
+
return this.label(undefined, 'mnCollection.nextPage', 'Next page');
|
|
6151
|
+
}
|
|
6152
|
+
/** Accessible label for the last-page control. */
|
|
6153
|
+
get lastPageLabel() {
|
|
6154
|
+
return this.label(undefined, 'mnCollection.lastPage', 'Last page');
|
|
6155
|
+
}
|
|
6156
|
+
/**
|
|
6157
|
+
* Wrapper classes for one slot in the page strip, shrinking it in two steps as
|
|
6158
|
+
* the footer narrows. Container queries, so the measurement is the footer's own
|
|
6159
|
+
* width — the same strip is wide on a page and cramped in a modal.
|
|
6160
|
+
*
|
|
6161
|
+
* The first/last anchors and their gaps drop below 640px, where « and » already
|
|
6162
|
+
* jump to either end. Below 380px every number except the current one drops too:
|
|
6163
|
+
* the strip would otherwise wrap onto a second line and push the footer over the
|
|
6164
|
+
* table, and the "Page 3 of 9" readout beside it already says where the user is.
|
|
6165
|
+
* The arrows survive both steps, so navigation never depends on a number.
|
|
6166
|
+
*/
|
|
6167
|
+
slotVisibility(slot) {
|
|
6168
|
+
if (slot.anchor)
|
|
6169
|
+
return 'hidden @min-[640px]:inline-flex';
|
|
6170
|
+
return slot.page === this.currentPage ? 'inline-flex' : 'hidden @min-[380px]:inline-flex';
|
|
6171
|
+
}
|
|
6172
|
+
/**
|
|
6173
|
+
* Accessible label for a page-number button.
|
|
6174
|
+
* @param page The page the button jumps to.
|
|
6175
|
+
* @returns The label, naming the page.
|
|
6176
|
+
*/
|
|
6177
|
+
pageLabel(page) {
|
|
6178
|
+
const template = this.label(undefined, 'mnCollection.page', 'Page {{page}}');
|
|
6179
|
+
return template.replace('{{page}}', String(page));
|
|
6180
|
+
}
|
|
6181
|
+
/**
|
|
6182
|
+
* Resolves a label three ways, in order: the consumer's explicit text, the
|
|
6183
|
+
* conventional `mnCollection.*` key when the app defines one, and finally a
|
|
6184
|
+
* readable English default.
|
|
6185
|
+
*
|
|
6186
|
+
* Mirrors `MnCollectionBase.resolveLabel`; this component is presentational and
|
|
6187
|
+
* does not extend that base, but its chrome must be just as translatable — the
|
|
6188
|
+
* page-size label and the item-range readout are on screen for every paged
|
|
6189
|
+
* collection in the app.
|
|
6190
|
+
*
|
|
6191
|
+
* @param explicit The label the host passed in, if any.
|
|
6192
|
+
* @param key The conventional translation key to try.
|
|
6193
|
+
* @param fallback The English text used when neither resolves.
|
|
6194
|
+
* @returns The resolved label.
|
|
6195
|
+
*/
|
|
6196
|
+
label(explicit, key, fallback) {
|
|
6197
|
+
return explicit ?? this.lang.translateIfPresent(key) ?? fallback;
|
|
6198
|
+
}
|
|
6152
6199
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCollectionPagination, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6153
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnCollectionPagination, isStandalone: true, selector: "mn-collection-pagination", inputs: { idPrefix: "idPrefix", isPaginated: "isPaginated", isServerPaginated: "isServerPaginated", showLoadMore: "showLoadMore", loadingMoreRows: "loadingMoreRows", currentPage: "currentPage", pageSize: "pageSize", totalPages: "totalPages", totalItemCount: "totalItemCount", visiblePages: "visiblePages", pageSizeSelectOptions: "pageSizeSelectOptions", labels: "labels" }, outputs: { loadMore: "loadMore", pageChange: "pageChange", pageSizeChange: "pageSizeChange" }, ngImport: i0, template: "<!-- Load more button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n (click)=\"loadMore.emit()\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'primary' }\"\n [disabled]=\"loadingMoreRows\"\n class=\"px-4 py-1.5 text-sm rounded border border-primary-500 text-primary-500 hover:bg-primary-100 transition-colors disabled:opacity-50\"\n mnButton\n type=\"button\"\n >\n @if (loadingMoreRows) {\n <span\n class=\"inline-block w-3 h-3 border-2 border-primary-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{
|
|
6200
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnCollectionPagination, isStandalone: true, selector: "mn-collection-pagination", inputs: { idPrefix: "idPrefix", isPaginated: "isPaginated", isServerPaginated: "isServerPaginated", showLoadMore: "showLoadMore", loadingMoreRows: "loadingMoreRows", currentPage: "currentPage", pageSize: "pageSize", totalPages: "totalPages", totalItemCount: "totalItemCount", visiblePages: "visiblePages", pageSizeSelectOptions: "pageSizeSelectOptions", labels: "labels" }, outputs: { loadMore: "loadMore", pageChange: "pageChange", pageSizeChange: "pageSizeChange" }, ngImport: i0, template: "<!-- Load more button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n (click)=\"loadMore.emit()\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'primary' }\"\n [disabled]=\"loadingMoreRows\"\n class=\"px-4 py-1.5 text-sm rounded border border-primary-500 text-primary-500 hover:bg-primary-100 transition-colors disabled:opacity-50\"\n mnButton\n type=\"button\"\n >\n @if (loadingMoreRows) {\n <span\n class=\"inline-block w-3 h-3 border-2 border-primary-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{ loadMoreLabel }}\n </button>\n </div>\n}\n\n<!-- Pagination controls -->\n@if (showPagination) {\n <div class=\"@container flex items-center justify-between gap-2 px-2 py-3 text-sm text-base-content\">\n @if (pageSizeSelectOptions.length > 1) {\n <div class=\"hidden @min-[640px]:flex items-center gap-2\">\n <span>{{ rowsPerPageLabel }}</span>\n <mn-lib-select\n (ngModelChange)=\"pageSizeChange.emit($event)\"\n [ngModel]=\"pageSize\"\n [props]=\"{\n id: idPrefix + '-page-size',\n options: pageSizeSelectOptions,\n size: 'sm'\n }\"\n ></mn-lib-select>\n </div>\n } @else {\n <div class=\"hidden @min-[640px]:block\"></div>\n }\n\n <!-- Narrow viewports state the page position outright: the strip's last-page\n anchor only appears once the window stops reaching the end, so it can't\n be relied on to carry the total. -->\n <span aria-live=\"polite\"\n class=\"@min-[640px]:hidden text-xs opacity-70 whitespace-nowrap\">{{ pageIndicatorLabel }}</span>\n\n <div class=\"flex flex-wrap items-center justify-center gap-0.5 @min-[640px]:gap-1\">\n <span class=\"text-xs mr-2 hidden @min-[640px]:inline\">{{ itemRangeLabel }}</span>\n\n <button\n (click)=\"pageChange.emit(1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n [disabled]=\"currentPage === 1\"\n [attr.aria-label]=\"firstPageLabel\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n type=\"button\"\n >\u00AB\n </button>\n\n <button\n (click)=\"pageChange.emit(currentPage - 1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n [disabled]=\"currentPage === 1\"\n [attr.aria-label]=\"previousPageLabel\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n type=\"button\"\n >\u2039\n </button>\n\n @for (slot of pageSlots; track $index) {\n <span [class]=\"slotVisibility(slot)\">\n @if (slot.page !== null) {\n <button\n (click)=\"pageChange.emit(slot.page)\"\n [attr.aria-current]=\"slot.page === currentPage ? 'page' : null\"\n [attr.aria-label]=\"pageLabel(slot.page)\"\n [data]=\"{ size: 'sm', variant: 'text', color: slot.page === currentPage ? 'primary' : 'gray' }\"\n class=\"px-1.5 @min-[640px]:px-2.5 py-1 rounded underline underline-offset-2 transition-colors text-xs\"\n mnButton\n type=\"button\"\n >{{ slot.page }}\n </button>\n } @else {\n <span aria-hidden=\"true\" class=\"px-0.5 text-xs opacity-50 select-none\">\u2026</span>\n }\n </span>\n }\n\n <button\n (click)=\"pageChange.emit(currentPage + 1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n [disabled]=\"currentPage === totalPages\"\n [attr.aria-label]=\"nextPageLabel\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n type=\"button\"\n >\u203A\n </button>\n\n <button\n (click)=\"pageChange.emit(totalPages)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n [disabled]=\"currentPage === totalPages\"\n [attr.aria-label]=\"lastPageLabel\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n type=\"button\"\n >\u00BB\n </button>\n </div>\n </div>\n}\n", dependencies: [{ kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
6154
6201
|
}
|
|
6155
6202
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCollectionPagination, decorators: [{
|
|
6156
6203
|
type: Component,
|
|
6157
|
-
args: [{ selector: 'mn-collection-pagination', standalone: true, imports: [MnButton, MnSelect, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Load more button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n (click)=\"loadMore.emit()\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'primary' }\"\n [disabled]=\"loadingMoreRows\"\n class=\"px-4 py-1.5 text-sm rounded border border-primary-500 text-primary-500 hover:bg-primary-100 transition-colors disabled:opacity-50\"\n mnButton\n type=\"button\"\n >\n @if (loadingMoreRows) {\n <span\n class=\"inline-block w-3 h-3 border-2 border-primary-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{
|
|
6204
|
+
args: [{ selector: 'mn-collection-pagination', standalone: true, imports: [MnButton, MnSelect, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Load more button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n (click)=\"loadMore.emit()\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'primary' }\"\n [disabled]=\"loadingMoreRows\"\n class=\"px-4 py-1.5 text-sm rounded border border-primary-500 text-primary-500 hover:bg-primary-100 transition-colors disabled:opacity-50\"\n mnButton\n type=\"button\"\n >\n @if (loadingMoreRows) {\n <span\n class=\"inline-block w-3 h-3 border-2 border-primary-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{ loadMoreLabel }}\n </button>\n </div>\n}\n\n<!-- Pagination controls -->\n@if (showPagination) {\n <div class=\"@container flex items-center justify-between gap-2 px-2 py-3 text-sm text-base-content\">\n @if (pageSizeSelectOptions.length > 1) {\n <div class=\"hidden @min-[640px]:flex items-center gap-2\">\n <span>{{ rowsPerPageLabel }}</span>\n <mn-lib-select\n (ngModelChange)=\"pageSizeChange.emit($event)\"\n [ngModel]=\"pageSize\"\n [props]=\"{\n id: idPrefix + '-page-size',\n options: pageSizeSelectOptions,\n size: 'sm'\n }\"\n ></mn-lib-select>\n </div>\n } @else {\n <div class=\"hidden @min-[640px]:block\"></div>\n }\n\n <!-- Narrow viewports state the page position outright: the strip's last-page\n anchor only appears once the window stops reaching the end, so it can't\n be relied on to carry the total. -->\n <span aria-live=\"polite\"\n class=\"@min-[640px]:hidden text-xs opacity-70 whitespace-nowrap\">{{ pageIndicatorLabel }}</span>\n\n <div class=\"flex flex-wrap items-center justify-center gap-0.5 @min-[640px]:gap-1\">\n <span class=\"text-xs mr-2 hidden @min-[640px]:inline\">{{ itemRangeLabel }}</span>\n\n <button\n (click)=\"pageChange.emit(1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n [disabled]=\"currentPage === 1\"\n [attr.aria-label]=\"firstPageLabel\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n type=\"button\"\n >\u00AB\n </button>\n\n <button\n (click)=\"pageChange.emit(currentPage - 1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n [disabled]=\"currentPage === 1\"\n [attr.aria-label]=\"previousPageLabel\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n type=\"button\"\n >\u2039\n </button>\n\n @for (slot of pageSlots; track $index) {\n <span [class]=\"slotVisibility(slot)\">\n @if (slot.page !== null) {\n <button\n (click)=\"pageChange.emit(slot.page)\"\n [attr.aria-current]=\"slot.page === currentPage ? 'page' : null\"\n [attr.aria-label]=\"pageLabel(slot.page)\"\n [data]=\"{ size: 'sm', variant: 'text', color: slot.page === currentPage ? 'primary' : 'gray' }\"\n class=\"px-1.5 @min-[640px]:px-2.5 py-1 rounded underline underline-offset-2 transition-colors text-xs\"\n mnButton\n type=\"button\"\n >{{ slot.page }}\n </button>\n } @else {\n <span aria-hidden=\"true\" class=\"px-0.5 text-xs opacity-50 select-none\">\u2026</span>\n }\n </span>\n }\n\n <button\n (click)=\"pageChange.emit(currentPage + 1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n [disabled]=\"currentPage === totalPages\"\n [attr.aria-label]=\"nextPageLabel\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n type=\"button\"\n >\u203A\n </button>\n\n <button\n (click)=\"pageChange.emit(totalPages)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n [disabled]=\"currentPage === totalPages\"\n [attr.aria-label]=\"lastPageLabel\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n type=\"button\"\n >\u00BB\n </button>\n </div>\n </div>\n}\n" }]
|
|
6158
6205
|
}], propDecorators: { idPrefix: [{
|
|
6159
6206
|
type: Input
|
|
6160
6207
|
}], isPaginated: [{
|
|
@@ -6195,8 +6242,6 @@ class MnTable extends MnSelectableCollectionBase {
|
|
|
6195
6242
|
columnFilters = {};
|
|
6196
6243
|
/** Viewport width (px) below which the inline filter row collapses into a panel. */
|
|
6197
6244
|
static FILTER_COLLAPSE_WIDTH = 640;
|
|
6198
|
-
/** Bounds rendered by a number-range filter, in input order. */
|
|
6199
|
-
numberBounds = ['min', 'max'];
|
|
6200
6245
|
/**
|
|
6201
6246
|
* True when the viewport is narrow enough that the per-column filter inputs no
|
|
6202
6247
|
* longer fit under their headers; the inline row is then replaced by a toggle
|
|
@@ -6211,8 +6256,6 @@ class MnTable extends MnSelectableCollectionBase {
|
|
|
6211
6256
|
}
|
|
6212
6257
|
collectionBody;
|
|
6213
6258
|
// ── Column Filters ──
|
|
6214
|
-
/** Bounds rendered by a date-range filter, in input order. */
|
|
6215
|
-
dateBounds = ['from', 'to'];
|
|
6216
6259
|
/** Debounces server-side text filters so typing doesn't fire a request per keystroke. */
|
|
6217
6260
|
filterDebounce = new Subject();
|
|
6218
6261
|
/**
|
|
@@ -6268,25 +6311,6 @@ class MnTable extends MnSelectableCollectionBase {
|
|
|
6268
6311
|
}
|
|
6269
6312
|
this.cdr.markForCheck();
|
|
6270
6313
|
}
|
|
6271
|
-
/** Updates one bound of a range filter, leaving the other side untouched. */
|
|
6272
|
-
onRangeFilter(column, bound, raw) {
|
|
6273
|
-
const current = { ...(this.columnFilters[column.key] ?? {}) };
|
|
6274
|
-
if (bound === 'min' || bound === 'max') {
|
|
6275
|
-
const parsed = Number(raw);
|
|
6276
|
-
// An emptied input clears that bound rather than pinning it to 0.
|
|
6277
|
-
if (raw === '' || Number.isNaN(parsed))
|
|
6278
|
-
delete current[bound];
|
|
6279
|
-
else
|
|
6280
|
-
current[bound] = parsed;
|
|
6281
|
-
}
|
|
6282
|
-
else {
|
|
6283
|
-
if (raw === '')
|
|
6284
|
-
delete current[bound];
|
|
6285
|
-
else
|
|
6286
|
-
current[bound] = raw;
|
|
6287
|
-
}
|
|
6288
|
-
this.onColumnFilter(column, current);
|
|
6289
|
-
}
|
|
6290
6314
|
/** Updates a tri-state boolean filter from its select ('' = any). */
|
|
6291
6315
|
onBooleanFilter(column, raw) {
|
|
6292
6316
|
this.onColumnFilter(column, raw === '' ? '' : raw === 'true');
|
|
@@ -6303,22 +6327,16 @@ class MnTable extends MnSelectableCollectionBase {
|
|
|
6303
6327
|
getFilterMultiSelectOptions(column) {
|
|
6304
6328
|
return (column.filterOptions ?? []).map(opt => ({ label: opt.label, value: String(opt.value) }));
|
|
6305
6329
|
}
|
|
6306
|
-
/**
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
return [
|
|
6310
|
-
{ label: column.filterPlaceholder ?? labels?.any ?? 'Any', value: '' },
|
|
6311
|
-
{ label: labels?.yes ?? 'Yes', value: 'true' },
|
|
6312
|
-
{ label: labels?.no ?? 'No', value: 'false' },
|
|
6313
|
-
];
|
|
6330
|
+
/** Label for the small-screen filters toggle button. */
|
|
6331
|
+
get filtersButtonLabel() {
|
|
6332
|
+
return this.resolveLabel(this.dataSource.filtersLabelKey, 'mnCollection.filters', this.dataSource.filtersLabel ?? 'Filters');
|
|
6314
6333
|
}
|
|
6315
|
-
/**
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
];
|
|
6334
|
+
/**
|
|
6335
|
+
* Summary a multi-select filter collapses to once more than one option is picked.
|
|
6336
|
+
* Resolved with the `{count}` token intact for mn-multi-select to fill in.
|
|
6337
|
+
*/
|
|
6338
|
+
get filterSelectedLabel() {
|
|
6339
|
+
return this.resolveLabel(this.dataSource.filterLabels?.selectedKey, 'mnCollection.filterSelected', this.dataSource.filterLabels?.selected ?? '{count} selected');
|
|
6322
6340
|
}
|
|
6323
6341
|
/** Current text/select filter value for a column. */
|
|
6324
6342
|
textFilterValue(column) {
|
|
@@ -6335,26 +6353,6 @@ class MnTable extends MnSelectableCollectionBase {
|
|
|
6335
6353
|
const value = this.columnFilters[column.key];
|
|
6336
6354
|
return typeof value === 'boolean' ? String(value) : '';
|
|
6337
6355
|
}
|
|
6338
|
-
/** Current value of one bound of a range filter, as an input-ready string. */
|
|
6339
|
-
rangeFilterValue(column, bound) {
|
|
6340
|
-
const value = this.columnFilters[column.key];
|
|
6341
|
-
const bounded = value?.[bound];
|
|
6342
|
-
return bounded === undefined || bounded === null ? '' : String(bounded);
|
|
6343
|
-
}
|
|
6344
|
-
/** Label for a range filter bound, falling back to the English default. */
|
|
6345
|
-
rangeBoundLabel(bound) {
|
|
6346
|
-
const labels = this.dataSource.filterLabels;
|
|
6347
|
-
switch (bound) {
|
|
6348
|
-
case 'min':
|
|
6349
|
-
return labels?.min ?? 'Min';
|
|
6350
|
-
case 'max':
|
|
6351
|
-
return labels?.max ?? 'Max';
|
|
6352
|
-
case 'from':
|
|
6353
|
-
return labels?.from ?? 'From';
|
|
6354
|
-
case 'to':
|
|
6355
|
-
return labels?.to ?? 'To';
|
|
6356
|
-
}
|
|
6357
|
-
}
|
|
6358
6356
|
/** Resets every column filter and re-applies (or re-requests) filtering. */
|
|
6359
6357
|
clearAllFilters() {
|
|
6360
6358
|
this.seedFilterValues();
|
|
@@ -6371,20 +6369,20 @@ class MnTable extends MnSelectableCollectionBase {
|
|
|
6371
6369
|
get hasColumnFilters() {
|
|
6372
6370
|
return this.dataSource.columns.some(c => c.filterable);
|
|
6373
6371
|
}
|
|
6374
|
-
/** Label for the small-screen filters toggle button. */
|
|
6375
|
-
get filtersButtonLabel() {
|
|
6376
|
-
return this.dataSource.filtersLabel ?? 'Filters';
|
|
6377
|
-
}
|
|
6378
|
-
/**
|
|
6379
|
-
* Summary a multi-select filter collapses to once more than one option is picked.
|
|
6380
|
-
* Resolved with the `{count}` token intact for mn-multi-select to fill in.
|
|
6381
|
-
*/
|
|
6382
|
-
get filterSelectedLabel() {
|
|
6383
|
-
return this.dataSource.filterLabels?.selected ?? '{count} selected';
|
|
6384
|
-
}
|
|
6385
6372
|
/** Label for the "clear all filters" action in the small-screen panel. */
|
|
6386
6373
|
get clearFiltersButtonLabel() {
|
|
6387
|
-
return this.dataSource.clearFiltersLabel ?? 'Clear all';
|
|
6374
|
+
return this.resolveLabel(this.dataSource.clearFiltersLabelKey, 'mnCollection.clearAll', this.dataSource.clearFiltersLabel ?? 'Clear all');
|
|
6375
|
+
}
|
|
6376
|
+
/** Heading for the selection summary, with the count filled in. */
|
|
6377
|
+
get selectionSummaryTitle() {
|
|
6378
|
+
const labels = this.dataSource.selectionSummaryLabels;
|
|
6379
|
+
const template = this.resolveLabel(labels?.titleKey, 'mnCollection.selectedCount', labels?.title ?? 'Selected ({{count}})');
|
|
6380
|
+
return template.replace('{{count}}', String(this.selectedIds.size));
|
|
6381
|
+
}
|
|
6382
|
+
/** Label for the summary's clear-everything action. */
|
|
6383
|
+
get selectionClearAllLabel() {
|
|
6384
|
+
const labels = this.dataSource.selectionSummaryLabels;
|
|
6385
|
+
return this.resolveLabel(labels?.clearAllKey, 'mnCollection.clearAll', labels?.clearAll ?? 'Clear all');
|
|
6388
6386
|
}
|
|
6389
6387
|
/** Opens/closes the stacked filter panel shown on small screens. */
|
|
6390
6388
|
toggleFiltersPanel() {
|
|
@@ -6517,31 +6515,35 @@ class MnTable extends MnSelectableCollectionBase {
|
|
|
6517
6515
|
}
|
|
6518
6516
|
/** Page size to use at/above the `md` breakpoint (consumer's pageSize, or the user's selection). */
|
|
6519
6517
|
desktopPageSize = 10;
|
|
6520
|
-
/** Heading for the selection summary, with the count filled in. */
|
|
6521
|
-
get selectionSummaryTitle() {
|
|
6522
|
-
const template = this.dataSource.selectionSummaryLabels?.title ?? 'Selected ({{count}})';
|
|
6523
|
-
return template.replace('{{count}}', String(this.selectedIds.size));
|
|
6524
|
-
}
|
|
6525
|
-
/** Label for the summary's clear-everything action. */
|
|
6526
|
-
get selectionClearAllLabel() {
|
|
6527
|
-
return this.dataSource.selectionSummaryLabels?.clearAll ?? 'Clear all';
|
|
6528
|
-
}
|
|
6529
6518
|
/** Label for the summary's expand/collapse control. */
|
|
6530
6519
|
get selectionSummaryToggleLabel() {
|
|
6531
6520
|
const labels = this.dataSource.selectionSummaryLabels;
|
|
6532
|
-
if (this.selectionSummaryExpanded)
|
|
6533
|
-
return labels?.showLess ?? 'Show less';
|
|
6534
|
-
|
|
6521
|
+
if (this.selectionSummaryExpanded) {
|
|
6522
|
+
return this.resolveLabel(labels?.showLessKey, 'mnCollection.showLess', labels?.showLess ?? 'Show less');
|
|
6523
|
+
}
|
|
6524
|
+
const template = this.resolveLabel(labels?.showMoreKey, 'mnCollection.showMore', labels?.showMore ?? '+{{count}} more');
|
|
6535
6525
|
return template.replace('{{count}}', String(this.hiddenSelectionCount));
|
|
6536
6526
|
}
|
|
6527
|
+
/** Placeholder and accessible name for the search box. */
|
|
6528
|
+
get searchPlaceholderLabel() {
|
|
6529
|
+
return this.resolveLabel(this.dataSource.searchPlaceholderKey, 'mnCollection.search', this.dataSource.searchPlaceholder ?? 'Search...');
|
|
6530
|
+
}
|
|
6531
|
+
/** Accessible name for the scrollable table region. */
|
|
6532
|
+
get tableRegionLabel() {
|
|
6533
|
+
return this.resolveLabel(undefined, 'mnCollection.dataTable', 'Data table');
|
|
6534
|
+
}
|
|
6537
6535
|
/**
|
|
6538
|
-
*
|
|
6539
|
-
*
|
|
6540
|
-
*
|
|
6536
|
+
* Fewer tags once the table is narrow. A tag holding a person's full name takes
|
|
6537
|
+
* a whole line at phone width, so the eight that read as a compact header on a
|
|
6538
|
+
* wide table become eight stacked lines in a modal sheet — the summary then
|
|
6539
|
+
* occupies more of the screen than the rows it is summarising.
|
|
6540
|
+
*
|
|
6541
|
+
* Reuses {@link filtersCollapsed} rather than measuring again: it is already
|
|
6542
|
+
* maintained on every resize and means exactly "this table is under 640px".
|
|
6543
|
+
* The heading still states the true total, so the hidden tags cost no information.
|
|
6541
6544
|
*/
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
return template.replace('{{label}}', this.selectionLabelFor(row));
|
|
6545
|
+
get defaultSelectionSummaryLimit() {
|
|
6546
|
+
return this.filtersCollapsed ? 5 : 8;
|
|
6545
6547
|
}
|
|
6546
6548
|
/** Tracks the desktop page size when the user picks one (selector only shows at >= md). */
|
|
6547
6549
|
onPageSizeChange(newSize) {
|
|
@@ -6561,6 +6563,18 @@ class MnTable extends MnSelectableCollectionBase {
|
|
|
6561
6563
|
get widthsArePinned() {
|
|
6562
6564
|
return this.layoutMode === 'fixed' || (this.layoutMode === 'stable' && this.widthsPinned);
|
|
6563
6565
|
}
|
|
6566
|
+
/** Any / Yes / No options for a boolean column filter. */
|
|
6567
|
+
getBooleanFilterOptions(column) {
|
|
6568
|
+
const labels = this.dataSource.filterLabels;
|
|
6569
|
+
return [
|
|
6570
|
+
{
|
|
6571
|
+
label: column.filterPlaceholder ?? this.resolveLabel(labels?.anyKey, 'mnCollection.filterAny', labels?.any ?? 'Any'),
|
|
6572
|
+
value: ''
|
|
6573
|
+
},
|
|
6574
|
+
{ label: this.resolveLabel(labels?.yesKey, 'mnCollection.filterYes', labels?.yes ?? 'Yes'), value: 'true' },
|
|
6575
|
+
{ label: this.resolveLabel(labels?.noKey, 'mnCollection.filterNo', labels?.no ?? 'No'), value: 'false' },
|
|
6576
|
+
];
|
|
6577
|
+
}
|
|
6564
6578
|
/**
|
|
6565
6579
|
* The width to render for a column: the consumer's own declared width always
|
|
6566
6580
|
* wins, then a width pinned by the `stable` layout, otherwise none.
|
|
@@ -6637,11 +6651,6 @@ class MnTable extends MnSelectableCollectionBase {
|
|
|
6637
6651
|
}
|
|
6638
6652
|
}
|
|
6639
6653
|
items = this.applySorting(items);
|
|
6640
|
-
// With no column sorted, the order carries no meaning the user chose, so spend
|
|
6641
|
-
// it on showing what is already selected. A sorted column always wins.
|
|
6642
|
-
if (!this.currentSort) {
|
|
6643
|
-
items = this.prioritizeInitialSelection(items);
|
|
6644
|
-
}
|
|
6645
6654
|
this.filteredItems = items;
|
|
6646
6655
|
this.applyPagination();
|
|
6647
6656
|
if (searchForItems) {
|
|
@@ -6849,7 +6858,6 @@ class MnTable extends MnSelectableCollectionBase {
|
|
|
6849
6858
|
if (!labels)
|
|
6850
6859
|
return;
|
|
6851
6860
|
const pairs = [
|
|
6852
|
-
['minKey', 'min'], ['maxKey', 'max'], ['fromKey', 'from'], ['toKey', 'to'],
|
|
6853
6861
|
['anyKey', 'any'], ['yesKey', 'yes'], ['noKey', 'no'], ['selectedKey', 'selected'],
|
|
6854
6862
|
];
|
|
6855
6863
|
for (const [keyProp, labelProp] of pairs) {
|
|
@@ -6891,12 +6899,30 @@ class MnTable extends MnSelectableCollectionBase {
|
|
|
6891
6899
|
}
|
|
6892
6900
|
});
|
|
6893
6901
|
}
|
|
6902
|
+
/** Filter options formatted for mn-select for a given column. */
|
|
6903
|
+
getFilterSelectOptions(column) {
|
|
6904
|
+
const placeholder = column.filterPlaceholder ?? this.resolveLabel(undefined, 'mnCollection.filterAll', 'All');
|
|
6905
|
+
return [
|
|
6906
|
+
{ label: placeholder, value: '' },
|
|
6907
|
+
...(column.filterOptions ?? []).map(opt => ({ label: opt.label, value: String(opt.value) })),
|
|
6908
|
+
];
|
|
6909
|
+
}
|
|
6910
|
+
/**
|
|
6911
|
+
* Accessible label for a tag's remove button.
|
|
6912
|
+
* @param row The row the tag stands for.
|
|
6913
|
+
* @returns The label, naming the row so screen readers announce which one goes.
|
|
6914
|
+
*/
|
|
6915
|
+
selectionRemoveLabel(row) {
|
|
6916
|
+
const labels = this.dataSource.selectionSummaryLabels;
|
|
6917
|
+
const template = this.resolveLabel(labels?.removeKey, 'mnCollection.removeSelected', labels?.remove ?? 'Remove {{label}}');
|
|
6918
|
+
return template.replace('{{label}}', this.selectionLabelFor(row));
|
|
6919
|
+
}
|
|
6894
6920
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6895
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTable, isStandalone: true, selector: "mn-table", outputs: { sortChange: "sortChange", rowClick: "rowClick" }, host: { listeners: { "window:resize": "onWindowResize()" }, classAttribute: "block" }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Everything that reflows lives inside a @container, so the breakpoints below\n measure the table's own width rather than the window's. A table in a modal,\n a sidebar or a narrow grid cell is far narrower than the viewport, and\n viewport breakpoints would hand it a desktop layout it has no room for. -->\n<div class=\"@container\">\n <!-- Selection summary: the whole selection, never paged, filtered or sorted. The\n table below answers \"what could I pick?\", which is why it is searched and paged;\n this answers \"what did I pick?\", which a paginated list cannot without hiding\n most of the answer on some other page. -->\n @if (showSelectionSummary) {\n <div class=\"flex flex-col gap-2 rounded-md border border-base-300 bg-base-200/50 p-3 mb-3\">\n <div class=\"flex items-center justify-between gap-2\">\n <span class=\"text-sm font-medium text-base-content\">{{ selectionSummaryTitle }}</span>\n <button\n (click)=\"clearSelection()\"\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"gap-1 shrink-0\"\n mnButton\n type=\"button\"\n >\n <svg [size]=\"14\" lucideX></svg>\n <span>{{ selectionClearAllLabel }}</span>\n </button>\n </div>\n <!-- Only the first few tags render; the rest collapse behind a \"+N more\" control.\n The heading's count always states the true total, so this hides tags, never\n information. Expanded, the list is height-capped and scrolls, so even a\n selection of hundreds cannot push the table off screen. -->\n <ul\n [class.max-h-28]=\"selectionSummaryExpanded\"\n [class.overflow-y-auto]=\"selectionSummaryExpanded\"\n class=\"flex flex-wrap gap-1.5 list-none m-0 p-0\"\n >\n @for (row of visibleSelectionRows; track dataSource.getID(row)) {\n <li\n class=\"inline-flex items-center gap-1 rounded-full bg-base-100 border border-base-300 pl-2.5 pr-1 py-0.5 text-xs text-base-content max-w-full\">\n <span [attr.title]=\"selectionLabelFor(row)\" class=\"truncate\">{{ selectionLabelFor(row) }}</span>\n <button\n (click)=\"removeSelection(row)\"\n [attr.aria-label]=\"selectionRemoveLabel(row)\"\n class=\"shrink-0 rounded-full p-0.5 hover:bg-base-300 transition-colors cursor-pointer\"\n type=\"button\"\n >\n <svg [size]=\"12\" lucideX></svg>\n </button>\n </li>\n }\n @if (hiddenSelectionCount > 0 || selectionSummaryExpanded) {\n <li>\n <button\n (click)=\"toggleSelectionSummary()\"\n [attr.aria-expanded]=\"selectionSummaryExpanded\"\n [data]=\"{ variant: 'text', color: 'primary', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"text-xs\"\n mnButton\n type=\"button\"\n >\n {{ selectionSummaryToggleLabel }}\n </button>\n </li>\n }\n </ul>\n </div>\n }\n\n <!-- Toolbar: custom toolbar template + search. Only rendered when it has content,\n so an unsearchable table without toolbar templates emits no empty spacer. -->\n@if (dataSource.canSearch || dataSource.toolbarLeftTemplate || dataSource.toolbarRightTemplate || (hasColumnFilters && filtersCollapsed)) {\n <div class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full @min-[420px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center gap-2 w-full @min-[420px]:flex-1 @min-[560px]:flex-none @min-[560px]:w-auto\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n class=\"w-full @min-[420px]:flex-1 @min-[560px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n [ngModel]=\"searchValue\"\n (ngModelChange)=\"onSearch($event)\"\n ></mn-lib-input-field>\n }\n @if (dataSource.toolbarRightTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n <!-- Small-screen filter toggle: replaces the inline per-column filter row below 640px -->\n @if (hasColumnFilters && filtersCollapsed) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'primary', size: 'md', borderRadius: 'md', hover: true }\"\n class=\"w-full @min-[420px]:w-auto gap-1.5\"\n [attr.aria-expanded]=\"filtersPanelOpen\"\n aria-controls=\"mn-table-filters-panel\"\n (click)=\"toggleFiltersPanel()\"\n >\n <svg lucideFunnel [size]=\"15\"></svg>\n <span>{{ filtersButtonLabel }}</span>\n </button>\n }\n </div>\n</div>\n}\n\n<!-- Small-screen filter panel: stacked, full-width fields decoupled from column widths -->\n@if (hasColumnFilters && filtersCollapsed) {\n <div\n id=\"mn-table-filters-panel\"\n class=\"grid transition-all duration-200 ease-out motion-reduce:transition-none\"\n [style.grid-template-rows]=\"filtersPanelOpen ? '1fr' : '0fr'\"\n >\n <div class=\"overflow-hidden\" [attr.inert]=\"filtersPanelOpen ? null : ''\">\n <div class=\"flex flex-col gap-3 rounded-md border border-base-300 bg-base-100 p-3 mb-3\">\n @for (column of dataSource.columns; track column.key) {\n @if (column.filterable) {\n <div class=\"flex flex-col gap-1\">\n <label\n class=\"text-xs font-medium text-base-content/70\"\n [attr.for]=\"'mn-table-filter-' + column.key\"\n >\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n {{ column.header }}\n }\n </label>\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'panel' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n </div>\n }\n }\n @if (hasActiveFilters) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"self-start gap-1\"\n (click)=\"clearAllFilters()\"\n >\n <svg lucideX [size]=\"14\"></svg>\n <span>{{ clearFiltersButtonLabel }}</span>\n </button>\n }\n </div>\n </div>\n </div>\n}\n\n<!-- Table wrapper with horizontal scroll -->\n <div #collectionBody [style.min-height.px]=\"bodyMinHeight\" aria-label=\"Data table\"\n class=\"overflow-x-auto\"\n role=\"region\">\n <table [class]=\"tableClasses\">\n <thead>\n <tr class=\"bg-base-100\">\n <!-- Selection checkbox column header -->\n @if (hasSelection) {\n <th class=\"w-10 text-center text-sm px-2 py-2\">\n @if (isMultiSelect) {\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-table-select-all', size: 'sm' }\"\n ></mn-lib-checkbox>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n [attr.data-column-key]=\"column.key\"\n [class.truncate]=\"widthsArePinned\"\n [class.cursor-pointer]=\"isSortable(column)\"\n [class.select-none]=\"isSortable(column)\"\n [class.hover:bg-base-200]=\"isSortable(column)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n class=\"text-sm px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2 whitespace-nowrap\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n <span>{{ column.header }}</span>\n }\n @if (isSortable(column)) {\n <span class=\"text-[0.65rem] opacity-70 min-w-3 inline-block\">{{ getSortIcon(column) }}</span>\n }\n </span>\n </th>\n }\n\n </tr>\n\n <!-- Per-column filter row (wide screens only; collapses into a panel below 640px).\n `font-normal` on the cells is load-bearing: these are `th` elements, which the\n browser renders bold, and the filter inputs and selects inside them inherit\n that. Filter controls are form fields, not headings, and must not look bold.\n The stacked filter panel renders outside any `th`, so it is unaffected. -->\n @if (hasColumnFilters && !filtersCollapsed) {\n <tr class=\"bg-base-100 border-b border-base-300 font-normal\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 font-normal\"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2 font-normal\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n <!-- Every filter renders as an ordinary control right under its\n header. The rich types used to hide behind a button that opened\n a floating panel, which cost a click to discover, a click to\n apply, and hid whether a column was even filterable. -->\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'inline' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <tr>\n @if (hasSelection) {\n <td class=\"px-2 py-3\">\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '1rem', height: '1rem' }\"></mn-skeleton>\n </td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n >\n @if (isTemplateRef(column.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.skeleton)\"></ng-container>\n } @else {\n <mn-skeleton [data]=\"getColumnSkeletonData(column)\"></mn-skeleton>\n }\n </td>\n }\n </tr>\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </td>\n </tr>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </td>\n </tr>\n }\n\n <!-- Data rows -->\n @for (row of paginatedItems; track trackByID($index, row); let odd = $odd; let last = $last) {\n <tr\n class=\"bg-base-100 transition-colors duration-150 hover:cursor-pointer\"\n [ngClass]=\"{'bg-primary/10': isSelected(row)}\"\n [class.bg-base-200]=\"!isSelected(row) && odd && dataSource.appearance?.striped\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onRowClick\"\n [class.border-b]=\"!last\"\n [class.border-base-300]=\"!last\"\n [class.border-b-1]=\"last\"\n [class.border-black]=\"last\"\n [class.shadow-3xl]=\"last\"\n (click)=\"onRowClick(row)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <td (click)=\"$event.stopPropagation()\" class=\"w-10 text-center px-2 py-2\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(row)\"\n [checked]=\"isSelected(row)\"\n [props]=\"{ id: 'mn-table-row-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n [attr.title]=\"cellTitle(column, row)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [class.truncate]=\"widthsArePinned\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n class=\"text-xs px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2\"\n >\n @if (column.cellSm) {\n <!-- Default cell: hidden below the cellSm breakpoint -->\n <span [mnShowAbove]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n </span>\n <!-- Small cell: shown only below the cellSm breakpoint -->\n <span [mnShowBelow]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cellSm.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cellSm.cell)\"></ng-container>\n } @else {\n {{ getCellSmValue(column, row) }}\n }\n </span>\n } @else {\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n }\n </td>\n }\n\n </tr>\n }\n }\n </tbody>\n </table>\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-table\"\n></mn-collection-pagination>\n</div>\n\n<!-- Single source of truth for every filter control, reused by the inline header\n row and the small-screen panel. `idScope` keeps element ids unique across\n both placements. -->\n<ng-template #filterField let-column let-idScope=\"idScope\">\n @switch (filterTypeOf(column)) {\n @case ('select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @case ('multi-select') {\n <mn-lib-multi-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"multiFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterMultiSelectOptions(column),\n placeholder: column.filterPlaceholder ?? '',\n collapsePlaceholder: filterSelectedLabel,\n collapseThreshold: 1,\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-multi-select>\n }\n @case ('boolean') {\n <mn-lib-select\n (ngModelChange)=\"onBooleanFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"booleanFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getBooleanFilterOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @case ('number-range') {\n <div class=\"flex items-end gap-2\">\n @for (bound of numberBounds; track bound) {\n <mn-lib-input-field\n (ngModelChange)=\"onRangeFilter(column, bound, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"rangeFilterValue(column, bound)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key + '-' + bound,\n type: 'number',\n label: rangeBoundLabel(bound),\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"flex-1\"\n ></mn-lib-input-field>\n }\n </div>\n }\n @case ('date-range') {\n <div class=\"flex flex-col gap-2\">\n @for (bound of dateBounds; track bound) {\n <mn-lib-datetime\n (ngModelChange)=\"onRangeFilter(column, bound, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"rangeFilterValue(column, bound)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key + '-' + bound,\n mode: 'date',\n label: rangeBoundLabel(bound),\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n ></mn-lib-datetime>\n }\n </div>\n }\n @default {\n <mn-lib-input-field\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n ariaLabel: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n ></mn-lib-input-field>\n }\n }\n</ng-template>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props", "checked"], outputs: ["checkedChange"] }, { kind: "directive", type: MnHiddenBelowDirective, selector: "[mnHiddenBelow]", inputs: ["mnHiddenBelow"] }, { kind: "directive", type: MnShowAboveDirective, selector: "[mnShowAbove]", inputs: ["mnShowAbove"] }, { kind: "directive", type: MnShowBelowDirective, selector: "[mnShowBelow]", inputs: ["mnShowBelow"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "component", type: MnMultiSelect, selector: "mn-lib-multi-select", inputs: ["props"] }, { kind: "component", type: MnDatetime, selector: "mn-lib-datetime", inputs: ["props"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnCollectionPagination, selector: "mn-collection-pagination", inputs: ["idPrefix", "isPaginated", "isServerPaginated", "showLoadMore", "loadingMoreRows", "currentPage", "pageSize", "totalPages", "totalItemCount", "visiblePages", "pageSizeSelectOptions", "labels"], outputs: ["loadMore", "pageChange", "pageSizeChange"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: LucideFilter, selector: "svg[lucideFunnel], svg[lucideFilter]" }, { kind: "component", type: LucideX, selector: "svg[lucideX]" }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
6921
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTable, isStandalone: true, selector: "mn-table", outputs: { sortChange: "sortChange", rowClick: "rowClick" }, host: { listeners: { "window:resize": "onWindowResize()" }, classAttribute: "block" }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Everything that reflows lives inside a @container, so the breakpoints below\n measure the table's own width rather than the window's. A table in a modal,\n a sidebar or a narrow grid cell is far narrower than the viewport, and\n viewport breakpoints would hand it a desktop layout it has no room for. -->\n<div class=\"@container\">\n <!-- Selection summary: the whole selection, never paged, filtered or sorted. The\n table below answers \"what could I pick?\", which is why it is searched and paged;\n this answers \"what did I pick?\", which a paginated list cannot without hiding\n most of the answer on some other page. -->\n @if (showSelectionSummary) {\n <div class=\"flex flex-col gap-2 rounded-md border border-base-300 bg-base-200/50 p-3 mb-3\">\n <div class=\"flex items-center justify-between gap-2\">\n <span class=\"text-sm font-medium text-base-content\">{{ selectionSummaryTitle }}</span>\n <button\n (click)=\"clearSelection()\"\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"gap-1 shrink-0\"\n mnButton\n type=\"button\"\n >\n <svg [size]=\"14\" lucideX></svg>\n <span>{{ selectionClearAllLabel }}</span>\n </button>\n </div>\n <!-- Only the first few tags render; the rest collapse behind a \"+N more\" control.\n The heading's count always states the true total, so this hides tags, never\n information. Expanded, the list is height-capped and scrolls, so even a\n selection of hundreds cannot push the table off screen. -->\n <ul\n [class.max-h-28]=\"selectionSummaryExpanded\"\n [class.overflow-y-auto]=\"selectionSummaryExpanded\"\n class=\"flex flex-wrap gap-1.5 list-none m-0 p-0\"\n >\n @for (row of visibleSelectionRows; track dataSource.getID(row)) {\n <li\n class=\"inline-flex items-center gap-1 rounded-full bg-base-100 border border-base-300 pl-2.5 pr-1 py-0.5 text-xs text-base-content max-w-full\">\n <span [attr.title]=\"selectionLabelFor(row)\" class=\"truncate\">{{ selectionLabelFor(row) }}</span>\n <button\n (click)=\"removeSelection(row)\"\n [attr.aria-label]=\"selectionRemoveLabel(row)\"\n class=\"shrink-0 rounded-full p-0.5 hover:bg-base-300 transition-colors cursor-pointer\"\n type=\"button\"\n >\n <svg [size]=\"12\" lucideX></svg>\n </button>\n </li>\n }\n @if (hiddenSelectionCount > 0 || selectionSummaryExpanded) {\n <li>\n <button\n (click)=\"toggleSelectionSummary()\"\n [attr.aria-expanded]=\"selectionSummaryExpanded\"\n [data]=\"{ variant: 'text', color: 'primary', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"text-xs\"\n mnButton\n type=\"button\"\n >\n {{ selectionSummaryToggleLabel }}\n </button>\n </li>\n }\n </ul>\n </div>\n }\n\n <!-- Toolbar: custom toolbar template + search. Only rendered when it has content,\n so an unsearchable table without toolbar templates emits no empty spacer. -->\n@if (dataSource.canSearch || dataSource.toolbarLeftTemplate || dataSource.toolbarRightTemplate || (hasColumnFilters && filtersCollapsed)) {\n <div class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full @min-[420px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center gap-2 w-full @min-[420px]:flex-1 @min-[560px]:flex-none @min-[560px]:w-auto\">\n @if (dataSource.canSearch) {\n <!-- Enter must not submit a surrounding form: the search box belongs to the\n table's chrome, not to whatever form the table happens to sit in. -->\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n class=\"w-full @min-[420px]:flex-1 @min-[560px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\n ariaLabel: searchPlaceholderLabel,\n placeholder: searchPlaceholderLabel,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n [ngModel]=\"searchValue\"\n (ngModelChange)=\"onSearch($event)\"\n ></mn-lib-input-field>\n }\n @if (dataSource.toolbarRightTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n <!-- Small-screen filter toggle: replaces the inline per-column filter row below 640px -->\n @if (hasColumnFilters && filtersCollapsed) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'primary', size: 'md', borderRadius: 'md', hover: true }\"\n class=\"w-full @min-[420px]:w-auto gap-1.5\"\n [attr.aria-expanded]=\"filtersPanelOpen\"\n aria-controls=\"mn-table-filters-panel\"\n (click)=\"toggleFiltersPanel()\"\n >\n <svg lucideFunnel [size]=\"15\"></svg>\n <span>{{ filtersButtonLabel }}</span>\n </button>\n }\n </div>\n</div>\n}\n\n<!-- Small-screen filter panel: stacked, full-width fields decoupled from column widths -->\n@if (hasColumnFilters && filtersCollapsed) {\n <div\n id=\"mn-table-filters-panel\"\n class=\"grid transition-all duration-200 ease-out motion-reduce:transition-none\"\n [style.grid-template-rows]=\"filtersPanelOpen ? '1fr' : '0fr'\"\n >\n <div class=\"overflow-hidden\" [attr.inert]=\"filtersPanelOpen ? null : ''\">\n <div class=\"flex flex-col gap-3 rounded-md border border-base-300 bg-base-100 p-3 mb-3\">\n @for (column of dataSource.columns; track column.key) {\n @if (column.filterable) {\n <div class=\"flex flex-col gap-1\">\n <label\n class=\"text-xs font-medium text-base-content/70\"\n [attr.for]=\"'mn-table-filter-' + column.key\"\n >\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n {{ column.header }}\n }\n </label>\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'panel' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n </div>\n }\n }\n @if (hasActiveFilters) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"self-start gap-1\"\n (click)=\"clearAllFilters()\"\n >\n <svg lucideX [size]=\"14\"></svg>\n <span>{{ clearFiltersButtonLabel }}</span>\n </button>\n }\n </div>\n </div>\n </div>\n}\n\n<!-- Table wrapper with horizontal scroll -->\n <div #collectionBody [attr.aria-label]=\"tableRegionLabel\" [style.min-height.px]=\"bodyMinHeight\"\n class=\"overflow-x-auto\"\n role=\"region\">\n <table [class]=\"tableClasses\">\n <thead>\n <tr class=\"bg-base-100\">\n <!-- Selection checkbox column header -->\n @if (hasSelection) {\n <th class=\"w-10 text-center text-sm px-2 py-2\">\n @if (isMultiSelect) {\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-table-select-all', size: 'sm' }\"\n ></mn-lib-checkbox>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n [attr.data-column-key]=\"column.key\"\n [class.truncate]=\"widthsArePinned\"\n [class.cursor-pointer]=\"isSortable(column)\"\n [class.select-none]=\"isSortable(column)\"\n [class.hover:bg-base-200]=\"isSortable(column)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n class=\"text-sm px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2 whitespace-nowrap\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n <span>{{ column.header }}</span>\n }\n @if (isSortable(column)) {\n <span class=\"text-[0.65rem] opacity-70 min-w-3 inline-block\">{{ getSortIcon(column) }}</span>\n }\n </span>\n </th>\n }\n\n </tr>\n\n <!-- Per-column filter row (wide screens only; collapses into a panel below 640px).\n `font-normal` on the cells is load-bearing: these are `th` elements, which the\n browser renders bold, and the filter inputs and selects inside them inherit\n that. Filter controls are form fields, not headings, and must not look bold.\n The stacked filter panel renders outside any `th`, so it is unaffected. -->\n @if (hasColumnFilters && !filtersCollapsed) {\n <tr class=\"bg-base-100 border-b border-base-300 font-normal\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 font-normal\"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2 font-normal\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n <!-- Every filter renders as an ordinary control right under its\n header. The rich types used to hide behind a button that opened\n a floating panel, which cost a click to discover, a click to\n apply, and hid whether a column was even filterable. -->\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'inline' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <tr>\n @if (hasSelection) {\n <td class=\"px-2 py-3\">\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '1rem', height: '1rem' }\"></mn-skeleton>\n </td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n >\n @if (isTemplateRef(column.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.skeleton)\"></ng-container>\n } @else {\n <mn-skeleton [data]=\"getColumnSkeletonData(column)\"></mn-skeleton>\n }\n </td>\n }\n </tr>\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </td>\n </tr>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </td>\n </tr>\n }\n\n <!-- Data rows -->\n @for (row of paginatedItems; track trackByID($index, row); let odd = $odd; let last = $last) {\n <tr\n class=\"bg-base-100 transition-colors duration-150 hover:cursor-pointer\"\n [ngClass]=\"{'bg-primary/10': isSelected(row)}\"\n [class.bg-base-200]=\"!isSelected(row) && odd && dataSource.appearance?.striped\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onRowClick\"\n [class.border-b]=\"!last\"\n [class.border-base-300]=\"!last\"\n [class.border-b-1]=\"last\"\n [class.border-black]=\"last\"\n [class.shadow-3xl]=\"last\"\n (click)=\"onRowClick(row)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <td (click)=\"$event.stopPropagation()\" class=\"w-10 text-center px-2 py-2\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(row)\"\n [checked]=\"isSelected(row)\"\n [props]=\"{ id: 'mn-table-row-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n [attr.title]=\"cellTitle(column, row)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [class.truncate]=\"widthsArePinned\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n class=\"text-xs px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2\"\n >\n @if (column.cellSm) {\n <!-- Default cell: hidden below the cellSm breakpoint -->\n <span [mnShowAbove]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n </span>\n <!-- Small cell: shown only below the cellSm breakpoint -->\n <span [mnShowBelow]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cellSm.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cellSm.cell)\"></ng-container>\n } @else {\n {{ getCellSmValue(column, row) }}\n }\n </span>\n } @else {\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n }\n </td>\n }\n\n </tr>\n }\n }\n </tbody>\n </table>\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-table\"\n></mn-collection-pagination>\n</div>\n\n<!-- Single source of truth for every filter control, reused by the inline header\n row and the small-screen panel. `idScope` keeps element ids unique across\n both placements. -->\n<ng-template #filterField let-column let-idScope=\"idScope\">\n @switch (filterTypeOf(column)) {\n @case ('select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @case ('multi-select') {\n <mn-lib-multi-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"multiFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterMultiSelectOptions(column),\n placeholder: column.filterPlaceholder ?? '',\n collapsePlaceholder: filterSelectedLabel,\n collapseThreshold: 1,\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-multi-select>\n }\n @case ('boolean') {\n <mn-lib-select\n (ngModelChange)=\"onBooleanFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"booleanFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getBooleanFilterOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @default {\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n ariaLabel: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n ></mn-lib-input-field>\n }\n }\n</ng-template>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props", "checked"], outputs: ["checkedChange"] }, { kind: "directive", type: MnHiddenBelowDirective, selector: "[mnHiddenBelow]", inputs: ["mnHiddenBelow"] }, { kind: "directive", type: MnShowAboveDirective, selector: "[mnShowAbove]", inputs: ["mnShowAbove"] }, { kind: "directive", type: MnShowBelowDirective, selector: "[mnShowBelow]", inputs: ["mnShowBelow"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "component", type: MnMultiSelect, selector: "mn-lib-multi-select", inputs: ["props"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnCollectionPagination, selector: "mn-collection-pagination", inputs: ["idPrefix", "isPaginated", "isServerPaginated", "showLoadMore", "loadingMoreRows", "currentPage", "pageSize", "totalPages", "totalItemCount", "visiblePages", "pageSizeSelectOptions", "labels"], outputs: ["loadMore", "pageChange", "pageSizeChange"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: LucideFilter, selector: "svg[lucideFunnel], svg[lucideFilter]" }, { kind: "component", type: LucideX, selector: "svg[lucideX]" }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
6896
6922
|
}
|
|
6897
6923
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, decorators: [{
|
|
6898
6924
|
type: Component,
|
|
6899
|
-
args: [{ selector: 'mn-table', standalone: true, imports: [NgClass, NgTemplateOutlet, MnCheckbox, MnHiddenBelowDirective, MnShowAboveDirective, MnShowBelowDirective, MnInputField, MnSelect, MnMultiSelect, MnDatetime, MnSkeleton, FormsModule, MnCollectionPagination, MnButton, LucideFilter, LucideX, LucideFunnel, LucideDynamicIcon], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'block' }, template: "<!-- Everything that reflows lives inside a @container, so the breakpoints below\n measure the table's own width rather than the window's. A table in a modal,\n a sidebar or a narrow grid cell is far narrower than the viewport, and\n viewport breakpoints would hand it a desktop layout it has no room for. -->\n<div class=\"@container\">\n <!-- Selection summary: the whole selection, never paged, filtered or sorted. The\n table below answers \"what could I pick?\", which is why it is searched and paged;\n this answers \"what did I pick?\", which a paginated list cannot without hiding\n most of the answer on some other page. -->\n @if (showSelectionSummary) {\n <div class=\"flex flex-col gap-2 rounded-md border border-base-300 bg-base-200/50 p-3 mb-3\">\n <div class=\"flex items-center justify-between gap-2\">\n <span class=\"text-sm font-medium text-base-content\">{{ selectionSummaryTitle }}</span>\n <button\n (click)=\"clearSelection()\"\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"gap-1 shrink-0\"\n mnButton\n type=\"button\"\n >\n <svg [size]=\"14\" lucideX></svg>\n <span>{{ selectionClearAllLabel }}</span>\n </button>\n </div>\n <!-- Only the first few tags render; the rest collapse behind a \"+N more\" control.\n The heading's count always states the true total, so this hides tags, never\n information. Expanded, the list is height-capped and scrolls, so even a\n selection of hundreds cannot push the table off screen. -->\n <ul\n [class.max-h-28]=\"selectionSummaryExpanded\"\n [class.overflow-y-auto]=\"selectionSummaryExpanded\"\n class=\"flex flex-wrap gap-1.5 list-none m-0 p-0\"\n >\n @for (row of visibleSelectionRows; track dataSource.getID(row)) {\n <li\n class=\"inline-flex items-center gap-1 rounded-full bg-base-100 border border-base-300 pl-2.5 pr-1 py-0.5 text-xs text-base-content max-w-full\">\n <span [attr.title]=\"selectionLabelFor(row)\" class=\"truncate\">{{ selectionLabelFor(row) }}</span>\n <button\n (click)=\"removeSelection(row)\"\n [attr.aria-label]=\"selectionRemoveLabel(row)\"\n class=\"shrink-0 rounded-full p-0.5 hover:bg-base-300 transition-colors cursor-pointer\"\n type=\"button\"\n >\n <svg [size]=\"12\" lucideX></svg>\n </button>\n </li>\n }\n @if (hiddenSelectionCount > 0 || selectionSummaryExpanded) {\n <li>\n <button\n (click)=\"toggleSelectionSummary()\"\n [attr.aria-expanded]=\"selectionSummaryExpanded\"\n [data]=\"{ variant: 'text', color: 'primary', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"text-xs\"\n mnButton\n type=\"button\"\n >\n {{ selectionSummaryToggleLabel }}\n </button>\n </li>\n }\n </ul>\n </div>\n }\n\n <!-- Toolbar: custom toolbar template + search. Only rendered when it has content,\n so an unsearchable table without toolbar templates emits no empty spacer. -->\n@if (dataSource.canSearch || dataSource.toolbarLeftTemplate || dataSource.toolbarRightTemplate || (hasColumnFilters && filtersCollapsed)) {\n <div class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full @min-[420px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center gap-2 w-full @min-[420px]:flex-1 @min-[560px]:flex-none @min-[560px]:w-auto\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n class=\"w-full @min-[420px]:flex-1 @min-[560px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n [ngModel]=\"searchValue\"\n (ngModelChange)=\"onSearch($event)\"\n ></mn-lib-input-field>\n }\n @if (dataSource.toolbarRightTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n <!-- Small-screen filter toggle: replaces the inline per-column filter row below 640px -->\n @if (hasColumnFilters && filtersCollapsed) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'primary', size: 'md', borderRadius: 'md', hover: true }\"\n class=\"w-full @min-[420px]:w-auto gap-1.5\"\n [attr.aria-expanded]=\"filtersPanelOpen\"\n aria-controls=\"mn-table-filters-panel\"\n (click)=\"toggleFiltersPanel()\"\n >\n <svg lucideFunnel [size]=\"15\"></svg>\n <span>{{ filtersButtonLabel }}</span>\n </button>\n }\n </div>\n</div>\n}\n\n<!-- Small-screen filter panel: stacked, full-width fields decoupled from column widths -->\n@if (hasColumnFilters && filtersCollapsed) {\n <div\n id=\"mn-table-filters-panel\"\n class=\"grid transition-all duration-200 ease-out motion-reduce:transition-none\"\n [style.grid-template-rows]=\"filtersPanelOpen ? '1fr' : '0fr'\"\n >\n <div class=\"overflow-hidden\" [attr.inert]=\"filtersPanelOpen ? null : ''\">\n <div class=\"flex flex-col gap-3 rounded-md border border-base-300 bg-base-100 p-3 mb-3\">\n @for (column of dataSource.columns; track column.key) {\n @if (column.filterable) {\n <div class=\"flex flex-col gap-1\">\n <label\n class=\"text-xs font-medium text-base-content/70\"\n [attr.for]=\"'mn-table-filter-' + column.key\"\n >\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n {{ column.header }}\n }\n </label>\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'panel' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n </div>\n }\n }\n @if (hasActiveFilters) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"self-start gap-1\"\n (click)=\"clearAllFilters()\"\n >\n <svg lucideX [size]=\"14\"></svg>\n <span>{{ clearFiltersButtonLabel }}</span>\n </button>\n }\n </div>\n </div>\n </div>\n}\n\n<!-- Table wrapper with horizontal scroll -->\n <div #collectionBody [style.min-height.px]=\"bodyMinHeight\" aria-label=\"Data table\"\n class=\"overflow-x-auto\"\n role=\"region\">\n <table [class]=\"tableClasses\">\n <thead>\n <tr class=\"bg-base-100\">\n <!-- Selection checkbox column header -->\n @if (hasSelection) {\n <th class=\"w-10 text-center text-sm px-2 py-2\">\n @if (isMultiSelect) {\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-table-select-all', size: 'sm' }\"\n ></mn-lib-checkbox>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n [attr.data-column-key]=\"column.key\"\n [class.truncate]=\"widthsArePinned\"\n [class.cursor-pointer]=\"isSortable(column)\"\n [class.select-none]=\"isSortable(column)\"\n [class.hover:bg-base-200]=\"isSortable(column)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n class=\"text-sm px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2 whitespace-nowrap\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n <span>{{ column.header }}</span>\n }\n @if (isSortable(column)) {\n <span class=\"text-[0.65rem] opacity-70 min-w-3 inline-block\">{{ getSortIcon(column) }}</span>\n }\n </span>\n </th>\n }\n\n </tr>\n\n <!-- Per-column filter row (wide screens only; collapses into a panel below 640px).\n `font-normal` on the cells is load-bearing: these are `th` elements, which the\n browser renders bold, and the filter inputs and selects inside them inherit\n that. Filter controls are form fields, not headings, and must not look bold.\n The stacked filter panel renders outside any `th`, so it is unaffected. -->\n @if (hasColumnFilters && !filtersCollapsed) {\n <tr class=\"bg-base-100 border-b border-base-300 font-normal\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 font-normal\"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2 font-normal\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n <!-- Every filter renders as an ordinary control right under its\n header. The rich types used to hide behind a button that opened\n a floating panel, which cost a click to discover, a click to\n apply, and hid whether a column was even filterable. -->\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'inline' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <tr>\n @if (hasSelection) {\n <td class=\"px-2 py-3\">\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '1rem', height: '1rem' }\"></mn-skeleton>\n </td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n >\n @if (isTemplateRef(column.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.skeleton)\"></ng-container>\n } @else {\n <mn-skeleton [data]=\"getColumnSkeletonData(column)\"></mn-skeleton>\n }\n </td>\n }\n </tr>\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </td>\n </tr>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </td>\n </tr>\n }\n\n <!-- Data rows -->\n @for (row of paginatedItems; track trackByID($index, row); let odd = $odd; let last = $last) {\n <tr\n class=\"bg-base-100 transition-colors duration-150 hover:cursor-pointer\"\n [ngClass]=\"{'bg-primary/10': isSelected(row)}\"\n [class.bg-base-200]=\"!isSelected(row) && odd && dataSource.appearance?.striped\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onRowClick\"\n [class.border-b]=\"!last\"\n [class.border-base-300]=\"!last\"\n [class.border-b-1]=\"last\"\n [class.border-black]=\"last\"\n [class.shadow-3xl]=\"last\"\n (click)=\"onRowClick(row)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <td (click)=\"$event.stopPropagation()\" class=\"w-10 text-center px-2 py-2\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(row)\"\n [checked]=\"isSelected(row)\"\n [props]=\"{ id: 'mn-table-row-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n [attr.title]=\"cellTitle(column, row)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [class.truncate]=\"widthsArePinned\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n class=\"text-xs px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2\"\n >\n @if (column.cellSm) {\n <!-- Default cell: hidden below the cellSm breakpoint -->\n <span [mnShowAbove]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n </span>\n <!-- Small cell: shown only below the cellSm breakpoint -->\n <span [mnShowBelow]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cellSm.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cellSm.cell)\"></ng-container>\n } @else {\n {{ getCellSmValue(column, row) }}\n }\n </span>\n } @else {\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n }\n </td>\n }\n\n </tr>\n }\n }\n </tbody>\n </table>\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-table\"\n></mn-collection-pagination>\n</div>\n\n<!-- Single source of truth for every filter control, reused by the inline header\n row and the small-screen panel. `idScope` keeps element ids unique across\n both placements. -->\n<ng-template #filterField let-column let-idScope=\"idScope\">\n @switch (filterTypeOf(column)) {\n @case ('select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @case ('multi-select') {\n <mn-lib-multi-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"multiFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterMultiSelectOptions(column),\n placeholder: column.filterPlaceholder ?? '',\n collapsePlaceholder: filterSelectedLabel,\n collapseThreshold: 1,\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-multi-select>\n }\n @case ('boolean') {\n <mn-lib-select\n (ngModelChange)=\"onBooleanFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"booleanFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getBooleanFilterOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @case ('number-range') {\n <div class=\"flex items-end gap-2\">\n @for (bound of numberBounds; track bound) {\n <mn-lib-input-field\n (ngModelChange)=\"onRangeFilter(column, bound, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"rangeFilterValue(column, bound)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key + '-' + bound,\n type: 'number',\n label: rangeBoundLabel(bound),\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"flex-1\"\n ></mn-lib-input-field>\n }\n </div>\n }\n @case ('date-range') {\n <div class=\"flex flex-col gap-2\">\n @for (bound of dateBounds; track bound) {\n <mn-lib-datetime\n (ngModelChange)=\"onRangeFilter(column, bound, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"rangeFilterValue(column, bound)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key + '-' + bound,\n mode: 'date',\n label: rangeBoundLabel(bound),\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n ></mn-lib-datetime>\n }\n </div>\n }\n @default {\n <mn-lib-input-field\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n ariaLabel: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n ></mn-lib-input-field>\n }\n }\n</ng-template>\n" }]
|
|
6925
|
+
args: [{ selector: 'mn-table', standalone: true, imports: [NgClass, NgTemplateOutlet, MnCheckbox, MnHiddenBelowDirective, MnShowAboveDirective, MnShowBelowDirective, MnInputField, MnSelect, MnMultiSelect, MnSkeleton, FormsModule, MnCollectionPagination, MnButton, LucideFilter, LucideX, LucideFunnel, LucideDynamicIcon], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'block' }, template: "<!-- Everything that reflows lives inside a @container, so the breakpoints below\n measure the table's own width rather than the window's. A table in a modal,\n a sidebar or a narrow grid cell is far narrower than the viewport, and\n viewport breakpoints would hand it a desktop layout it has no room for. -->\n<div class=\"@container\">\n <!-- Selection summary: the whole selection, never paged, filtered or sorted. The\n table below answers \"what could I pick?\", which is why it is searched and paged;\n this answers \"what did I pick?\", which a paginated list cannot without hiding\n most of the answer on some other page. -->\n @if (showSelectionSummary) {\n <div class=\"flex flex-col gap-2 rounded-md border border-base-300 bg-base-200/50 p-3 mb-3\">\n <div class=\"flex items-center justify-between gap-2\">\n <span class=\"text-sm font-medium text-base-content\">{{ selectionSummaryTitle }}</span>\n <button\n (click)=\"clearSelection()\"\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"gap-1 shrink-0\"\n mnButton\n type=\"button\"\n >\n <svg [size]=\"14\" lucideX></svg>\n <span>{{ selectionClearAllLabel }}</span>\n </button>\n </div>\n <!-- Only the first few tags render; the rest collapse behind a \"+N more\" control.\n The heading's count always states the true total, so this hides tags, never\n information. Expanded, the list is height-capped and scrolls, so even a\n selection of hundreds cannot push the table off screen. -->\n <ul\n [class.max-h-28]=\"selectionSummaryExpanded\"\n [class.overflow-y-auto]=\"selectionSummaryExpanded\"\n class=\"flex flex-wrap gap-1.5 list-none m-0 p-0\"\n >\n @for (row of visibleSelectionRows; track dataSource.getID(row)) {\n <li\n class=\"inline-flex items-center gap-1 rounded-full bg-base-100 border border-base-300 pl-2.5 pr-1 py-0.5 text-xs text-base-content max-w-full\">\n <span [attr.title]=\"selectionLabelFor(row)\" class=\"truncate\">{{ selectionLabelFor(row) }}</span>\n <button\n (click)=\"removeSelection(row)\"\n [attr.aria-label]=\"selectionRemoveLabel(row)\"\n class=\"shrink-0 rounded-full p-0.5 hover:bg-base-300 transition-colors cursor-pointer\"\n type=\"button\"\n >\n <svg [size]=\"12\" lucideX></svg>\n </button>\n </li>\n }\n @if (hiddenSelectionCount > 0 || selectionSummaryExpanded) {\n <li>\n <button\n (click)=\"toggleSelectionSummary()\"\n [attr.aria-expanded]=\"selectionSummaryExpanded\"\n [data]=\"{ variant: 'text', color: 'primary', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"text-xs\"\n mnButton\n type=\"button\"\n >\n {{ selectionSummaryToggleLabel }}\n </button>\n </li>\n }\n </ul>\n </div>\n }\n\n <!-- Toolbar: custom toolbar template + search. Only rendered when it has content,\n so an unsearchable table without toolbar templates emits no empty spacer. -->\n@if (dataSource.canSearch || dataSource.toolbarLeftTemplate || dataSource.toolbarRightTemplate || (hasColumnFilters && filtersCollapsed)) {\n <div class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full @min-[420px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center gap-2 w-full @min-[420px]:flex-1 @min-[560px]:flex-none @min-[560px]:w-auto\">\n @if (dataSource.canSearch) {\n <!-- Enter must not submit a surrounding form: the search box belongs to the\n table's chrome, not to whatever form the table happens to sit in. -->\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n class=\"w-full @min-[420px]:flex-1 @min-[560px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\n ariaLabel: searchPlaceholderLabel,\n placeholder: searchPlaceholderLabel,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n [ngModel]=\"searchValue\"\n (ngModelChange)=\"onSearch($event)\"\n ></mn-lib-input-field>\n }\n @if (dataSource.toolbarRightTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n <!-- Small-screen filter toggle: replaces the inline per-column filter row below 640px -->\n @if (hasColumnFilters && filtersCollapsed) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'primary', size: 'md', borderRadius: 'md', hover: true }\"\n class=\"w-full @min-[420px]:w-auto gap-1.5\"\n [attr.aria-expanded]=\"filtersPanelOpen\"\n aria-controls=\"mn-table-filters-panel\"\n (click)=\"toggleFiltersPanel()\"\n >\n <svg lucideFunnel [size]=\"15\"></svg>\n <span>{{ filtersButtonLabel }}</span>\n </button>\n }\n </div>\n</div>\n}\n\n<!-- Small-screen filter panel: stacked, full-width fields decoupled from column widths -->\n@if (hasColumnFilters && filtersCollapsed) {\n <div\n id=\"mn-table-filters-panel\"\n class=\"grid transition-all duration-200 ease-out motion-reduce:transition-none\"\n [style.grid-template-rows]=\"filtersPanelOpen ? '1fr' : '0fr'\"\n >\n <div class=\"overflow-hidden\" [attr.inert]=\"filtersPanelOpen ? null : ''\">\n <div class=\"flex flex-col gap-3 rounded-md border border-base-300 bg-base-100 p-3 mb-3\">\n @for (column of dataSource.columns; track column.key) {\n @if (column.filterable) {\n <div class=\"flex flex-col gap-1\">\n <label\n class=\"text-xs font-medium text-base-content/70\"\n [attr.for]=\"'mn-table-filter-' + column.key\"\n >\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n {{ column.header }}\n }\n </label>\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'panel' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n </div>\n }\n }\n @if (hasActiveFilters) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"self-start gap-1\"\n (click)=\"clearAllFilters()\"\n >\n <svg lucideX [size]=\"14\"></svg>\n <span>{{ clearFiltersButtonLabel }}</span>\n </button>\n }\n </div>\n </div>\n </div>\n}\n\n<!-- Table wrapper with horizontal scroll -->\n <div #collectionBody [attr.aria-label]=\"tableRegionLabel\" [style.min-height.px]=\"bodyMinHeight\"\n class=\"overflow-x-auto\"\n role=\"region\">\n <table [class]=\"tableClasses\">\n <thead>\n <tr class=\"bg-base-100\">\n <!-- Selection checkbox column header -->\n @if (hasSelection) {\n <th class=\"w-10 text-center text-sm px-2 py-2\">\n @if (isMultiSelect) {\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-table-select-all', size: 'sm' }\"\n ></mn-lib-checkbox>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n [attr.data-column-key]=\"column.key\"\n [class.truncate]=\"widthsArePinned\"\n [class.cursor-pointer]=\"isSortable(column)\"\n [class.select-none]=\"isSortable(column)\"\n [class.hover:bg-base-200]=\"isSortable(column)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n class=\"text-sm px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2 whitespace-nowrap\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n <span>{{ column.header }}</span>\n }\n @if (isSortable(column)) {\n <span class=\"text-[0.65rem] opacity-70 min-w-3 inline-block\">{{ getSortIcon(column) }}</span>\n }\n </span>\n </th>\n }\n\n </tr>\n\n <!-- Per-column filter row (wide screens only; collapses into a panel below 640px).\n `font-normal` on the cells is load-bearing: these are `th` elements, which the\n browser renders bold, and the filter inputs and selects inside them inherit\n that. Filter controls are form fields, not headings, and must not look bold.\n The stacked filter panel renders outside any `th`, so it is unaffected. -->\n @if (hasColumnFilters && !filtersCollapsed) {\n <tr class=\"bg-base-100 border-b border-base-300 font-normal\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 font-normal\"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2 font-normal\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n <!-- Every filter renders as an ordinary control right under its\n header. The rich types used to hide behind a button that opened\n a floating panel, which cost a click to discover, a click to\n apply, and hid whether a column was even filterable. -->\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'inline' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <tr>\n @if (hasSelection) {\n <td class=\"px-2 py-3\">\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '1rem', height: '1rem' }\"></mn-skeleton>\n </td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n >\n @if (isTemplateRef(column.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.skeleton)\"></ng-container>\n } @else {\n <mn-skeleton [data]=\"getColumnSkeletonData(column)\"></mn-skeleton>\n }\n </td>\n }\n </tr>\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </td>\n </tr>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </td>\n </tr>\n }\n\n <!-- Data rows -->\n @for (row of paginatedItems; track trackByID($index, row); let odd = $odd; let last = $last) {\n <tr\n class=\"bg-base-100 transition-colors duration-150 hover:cursor-pointer\"\n [ngClass]=\"{'bg-primary/10': isSelected(row)}\"\n [class.bg-base-200]=\"!isSelected(row) && odd && dataSource.appearance?.striped\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onRowClick\"\n [class.border-b]=\"!last\"\n [class.border-base-300]=\"!last\"\n [class.border-b-1]=\"last\"\n [class.border-black]=\"last\"\n [class.shadow-3xl]=\"last\"\n (click)=\"onRowClick(row)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <td (click)=\"$event.stopPropagation()\" class=\"w-10 text-center px-2 py-2\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(row)\"\n [checked]=\"isSelected(row)\"\n [props]=\"{ id: 'mn-table-row-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n [attr.title]=\"cellTitle(column, row)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [class.truncate]=\"widthsArePinned\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n class=\"text-xs px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2\"\n >\n @if (column.cellSm) {\n <!-- Default cell: hidden below the cellSm breakpoint -->\n <span [mnShowAbove]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n </span>\n <!-- Small cell: shown only below the cellSm breakpoint -->\n <span [mnShowBelow]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cellSm.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cellSm.cell)\"></ng-container>\n } @else {\n {{ getCellSmValue(column, row) }}\n }\n </span>\n } @else {\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n }\n </td>\n }\n\n </tr>\n }\n }\n </tbody>\n </table>\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-table\"\n></mn-collection-pagination>\n</div>\n\n<!-- Single source of truth for every filter control, reused by the inline header\n row and the small-screen panel. `idScope` keeps element ids unique across\n both placements. -->\n<ng-template #filterField let-column let-idScope=\"idScope\">\n @switch (filterTypeOf(column)) {\n @case ('select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @case ('multi-select') {\n <mn-lib-multi-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"multiFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterMultiSelectOptions(column),\n placeholder: column.filterPlaceholder ?? '',\n collapsePlaceholder: filterSelectedLabel,\n collapseThreshold: 1,\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-multi-select>\n }\n @case ('boolean') {\n <mn-lib-select\n (ngModelChange)=\"onBooleanFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"booleanFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getBooleanFilterOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @default {\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n ariaLabel: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n ></mn-lib-input-field>\n }\n }\n</ng-template>\n" }]
|
|
6900
6926
|
}], ctorParameters: () => [], propDecorators: { sortChange: [{
|
|
6901
6927
|
type: Output
|
|
6902
6928
|
}], rowClick: [{
|
|
@@ -7427,6 +7453,11 @@ class MnFormBodyComponent {
|
|
|
7427
7453
|
// sight, which is exactly what the summary exists to prevent. Set
|
|
7428
7454
|
// `selectionSummary: false` on the data source to opt a field out.
|
|
7429
7455
|
ds.selectionSummary ??= true;
|
|
7456
|
+
// A modal is too short to make a rows-per-page choice meaningful, and the
|
|
7457
|
+
// selector costs a row of footer the sheet cannot spare. Pinning the options
|
|
7458
|
+
// to the single configured size hides it through the paginator's existing
|
|
7459
|
+
// "more than one option" rule rather than adding a second way to hide it.
|
|
7460
|
+
ds.pageSizeOptions = [ds.pageSize ?? 10];
|
|
7430
7461
|
// Pre-select rows from the form's initial value
|
|
7431
7462
|
const control = this.form.get(field.key);
|
|
7432
7463
|
if (control && Array.isArray(control.value) && control.value.length > 0) {
|
|
@@ -8219,11 +8250,11 @@ class MnWizardBodyComponent {
|
|
|
8219
8250
|
}
|
|
8220
8251
|
}
|
|
8221
8252
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnWizardBodyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
8222
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnWizardBodyComponent, isStandalone: true, selector: "mn-wizard-body", inputs: { config: "config", modalRef: "modalRef" }, viewQueries: [{ propertyName: "stepScroller", first: true, predicate: ["stepScroller"], descendants: true }, { propertyName: "formBodies", predicate: MnFormBodyComponent, descendants: true }, { propertyName: "stepWrappers", predicate: ["stepWrapper"], descendants: true }], ngImport: i0, template: "<div class=\"flex flex-col flex-auto min-h-0\">\n @if (config.component || config.template) {\n <mn-custom-body-host\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block flex-none px-6 pt-6\"\n ></mn-custom-body-host>\n }\n\n <!-- Steps header \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none px-6 pt-2 pb-4 border-b border-base-300 bg-base-100\">\n <div class=\"relative flex items-start\">\n <!-- Background line spanning from first circle center to last circle center -->\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.right]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-base-300 z-0\"\n ></div>\n\n <!-- Progress fill line -->\n @if (visibleSteps.length > 1) {\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.width]=\"'calc(' + (currentProgressIndex / (visibleSteps.length - 1)) * (100 - 100 / visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-success z-1 transition-all duration-500\"\n ></div>\n }\n\n @for (step of visibleSteps; track step.id; let i = $index; ) {\n <div\n (keyup.enter)=\"isFreeFlow ? goToStep(step) : null\"\n (click)=\"isFreeFlow ? goToStep(step) : null\"\n [attr.tabindex]=\"isFreeFlow && canNavigateToStep(step) ? 0 : null\"\n [class.cursor-pointer]=\"isFreeFlow && canNavigateToStep(step)\"\n class=\"relative flex flex-col items-center gap-1 z-10 w-0 flex-1\"\n >\n <div\n [ngClass]=\"{\n 'bg-primary text-primary-content': step.id === currentStepId,\n 'bg-success text-success-content': visitedStepIds.includes(step.id) && step.id !== currentStepId,\n 'bg-base-200 text-base-content/50': !visitedStepIds.includes(step.id) && step.id !== currentStepId\n }\"\n class=\"w-8 h-8 rounded-full flex items-center justify-center font-semibold transition-all text-sm\"\n >{{ i + 1 }}\n </div>\n <div\n [ngClass]=\"{\n 'text-base-content font-semibold': step.id === currentStepId,\n 'text-base-content/50': step.id !== currentStepId\n }\"\n class=\"text-xs text-center whitespace-nowrap hidden sm:block\"\n >{{ step.title }}\n </div>\n\n </div>\n }\n </div>\n </div>\n\n <!-- Step body \u2014 the only scrolling region in the wizard, shared by every step -->\n <div #stepScroller class=\"flex-auto min-h-0 overflow-y-auto px-6 py-6\">\n <div [style.min-height]=\"measuredMinHeight ? measuredMinHeight + 'px' : null\" class=\"min-h-48\">\n @for (step of config.steps; track step.id) {\n <div #stepWrapper [style.display]=\"step.id === currentStepId ? 'block' : 'none'\">\n <h3 class=\"text-lg font-semibold text-base-content mb-4\">{{ step.title }}</h3>\n <div class=\"text-base-content/80\">\n <!-- Form step -->\n @if (stepFormConfigs[step.id]) {\n <mn-form-body\n [config]=\"stepFormConfigs[step.id]\"\n [modalRef]=\"$any(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n ></mn-form-body>\n }\n\n <!-- Text body -->\n @if (!stepFormConfigs[step.id] && isTextBody(step)) {\n <div>\n {{ step.body }}\n </div>\n }\n\n <!-- Component / template body -->\n @if (stepBodyConfigs[step.id]) {\n <mn-custom-body-host\n [config]=\"stepBodyConfigs[step.id]\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-custom-body-host>\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Wizard-level errors (from onBeforeComplete) \u2014 fixed, above the footer -->\n @if (wizardErrors && (wizardErrors | keyvalue).length > 0) {\n <div class=\"flex flex-col gap-1 mx-6 mb-2 px-2 py-2 bg-red-50 rounded-md\">\n @for (err of wizardErrors | keyvalue; track err.key) {\n <div class=\"text-red-500 text-sm\">\n {{ err.value }}\n </div>\n }\n </div>\n }\n\n <!-- Footer \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none flex gap-3 px-6 pt-4 pb-6 border-t border-base-300 bg-base-100\">\n @if (!currentStep?.hideBack) {\n <button\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n @if (backIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ currentStep?.backLabel || (canGoBack ? labels.back : labels.close) }}\n </button>\n }\n\n <!-- Custom footer actions -->\n @if (config.footerActions) {\n <mn-footer-actions\n [actions]=\"config.footerActions\"\n [showIcons]=\"showActionIcons\"\n (actionClick)=\"handleFooterAction($any($event))\"\n ></mn-footer-actions>\n } @else {\n <div class=\"flex-1\"></div>\n }\n\n @if (!isLastStep) {\n <button\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid }\"\n (click)=\"next()\"\n >\n {{ currentStep?.nextLabel || labels.next }}\n @if (nextIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"ml-2\"></svg>\n }\n </button>\n }\n\n @if (isLastStep) {\n <button\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid || isCompleting }\"\n [disabled]=\"!isCurrentStepValid || isCompleting\"\n (click)=\"complete()\"\n >\n @if (completeIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ currentStep?.nextLabel || (isCompleting ? labels.completing : labels.complete) }}\n </button>\n }\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter", "hideCustomBody"], outputs: ["formStatusChange"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }, { kind: "component", type: MnFooterActionsComponent, selector: "mn-footer-actions", inputs: ["actions", "showIcons"], outputs: ["actionClick"] }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }, { kind: "pipe", type: i1.KeyValuePipe, name: "keyvalue" }] });
|
|
8253
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnWizardBodyComponent, isStandalone: true, selector: "mn-wizard-body", inputs: { config: "config", modalRef: "modalRef" }, viewQueries: [{ propertyName: "stepScroller", first: true, predicate: ["stepScroller"], descendants: true }, { propertyName: "formBodies", predicate: MnFormBodyComponent, descendants: true }, { propertyName: "stepWrappers", predicate: ["stepWrapper"], descendants: true }], ngImport: i0, template: "<div class=\"flex flex-col flex-auto min-h-0\">\n @if (config.component || config.template) {\n <mn-custom-body-host\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block flex-none px-6 pt-6\"\n ></mn-custom-body-host>\n }\n\n <!-- Steps header \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none px-6 pt-2 pb-4 border-b border-base-300 bg-base-100\">\n <div class=\"relative flex items-start\">\n <!-- Background line spanning from first circle center to last circle center -->\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.right]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-base-300 z-0\"\n ></div>\n\n <!-- Progress fill line -->\n @if (visibleSteps.length > 1) {\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.width]=\"'calc(' + (currentProgressIndex / (visibleSteps.length - 1)) * (100 - 100 / visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-success z-1 transition-all duration-500\"\n ></div>\n }\n\n @for (step of visibleSteps; track step.id; let i = $index; ) {\n <div\n (keyup.enter)=\"isFreeFlow ? goToStep(step) : null\"\n (click)=\"isFreeFlow ? goToStep(step) : null\"\n [attr.tabindex]=\"isFreeFlow && canNavigateToStep(step) ? 0 : null\"\n [class.cursor-pointer]=\"isFreeFlow && canNavigateToStep(step)\"\n class=\"relative flex flex-col items-center gap-1 z-10 w-0 flex-1\"\n >\n <div\n [ngClass]=\"{\n 'bg-primary text-primary-content': step.id === currentStepId,\n 'bg-success text-success-content': visitedStepIds.includes(step.id) && step.id !== currentStepId,\n 'bg-base-200 text-base-content/50': !visitedStepIds.includes(step.id) && step.id !== currentStepId\n }\"\n class=\"w-8 h-8 rounded-full flex items-center justify-center font-semibold transition-all text-sm\"\n >{{ i + 1 }}\n </div>\n <div\n [ngClass]=\"{\n 'text-base-content font-semibold': step.id === currentStepId,\n 'text-base-content/50': step.id !== currentStepId\n }\"\n class=\"text-xs text-center whitespace-nowrap hidden sm:block\"\n >{{ step.title }}\n </div>\n\n </div>\n }\n </div>\n </div>\n\n <!-- Step body \u2014 the only scrolling region in the wizard, shared by every step -->\n <div #stepScroller class=\"flex-auto min-h-0 overflow-y-auto px-6 py-6\">\n <div [style.min-height]=\"measuredMinHeight ? measuredMinHeight + 'px' : null\" class=\"min-h-48\">\n @for (step of config.steps; track step.id) {\n <div #stepWrapper [style.display]=\"step.id === currentStepId ? 'block' : 'none'\">\n <h3 class=\"text-lg font-semibold text-base-content mb-4\">{{ step.title }}</h3>\n <div class=\"text-base-content/80\">\n <!-- Form step -->\n @if (stepFormConfigs[step.id]) {\n <mn-form-body\n [config]=\"stepFormConfigs[step.id]\"\n [modalRef]=\"$any(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n ></mn-form-body>\n }\n\n <!-- Text body -->\n @if (!stepFormConfigs[step.id] && isTextBody(step)) {\n <div>\n {{ step.body }}\n </div>\n }\n\n <!-- Component / template body -->\n @if (stepBodyConfigs[step.id]) {\n <mn-custom-body-host\n [config]=\"stepBodyConfigs[step.id]\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-custom-body-host>\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Wizard-level errors (from onBeforeComplete) \u2014 fixed, above the footer -->\n @if (wizardErrors && (wizardErrors | keyvalue).length > 0) {\n <div class=\"flex flex-col gap-1 mx-6 mb-2 px-2 py-2 bg-red-50 rounded-md\">\n @for (err of wizardErrors | keyvalue; track err.key) {\n <div class=\"text-red-500 text-sm\">\n {{ err.value }}\n </div>\n }\n </div>\n }\n\n <!-- Footer \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none flex gap-3 px-6 pt-4 pb-6 border-t border-base-300 bg-base-100\">\n @if (!currentStep?.hideBack) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n @if (backIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ currentStep?.backLabel || (canGoBack ? labels.back : labels.close) }}\n </button>\n }\n\n <!-- Custom footer actions -->\n @if (config.footerActions) {\n <mn-footer-actions\n [actions]=\"config.footerActions\"\n [showIcons]=\"showActionIcons\"\n (actionClick)=\"handleFooterAction($any($event))\"\n ></mn-footer-actions>\n } @else {\n <div class=\"flex-1\"></div>\n }\n\n @if (!isLastStep) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid }\"\n (click)=\"next()\"\n >\n {{ currentStep?.nextLabel || labels.next }}\n @if (nextIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"ml-2\"></svg>\n }\n </button>\n }\n\n @if (isLastStep) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid || isCompleting }\"\n [disabled]=\"!isCurrentStepValid || isCompleting\"\n (click)=\"complete()\"\n >\n @if (completeIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ currentStep?.nextLabel || (isCompleting ? labels.completing : labels.complete) }}\n </button>\n }\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter", "hideCustomBody"], outputs: ["formStatusChange"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }, { kind: "component", type: MnFooterActionsComponent, selector: "mn-footer-actions", inputs: ["actions", "showIcons"], outputs: ["actionClick"] }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }, { kind: "pipe", type: i1.KeyValuePipe, name: "keyvalue" }] });
|
|
8223
8254
|
}
|
|
8224
8255
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnWizardBodyComponent, decorators: [{
|
|
8225
8256
|
type: Component,
|
|
8226
|
-
args: [{ selector: 'mn-wizard-body', standalone: true, imports: [CommonModule, ReactiveFormsModule, MnButton, MnFormBodyComponent, MnCustomBodyHostComponent, MnFooterActionsComponent, LucideDynamicIcon], template: "<div class=\"flex flex-col flex-auto min-h-0\">\n @if (config.component || config.template) {\n <mn-custom-body-host\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block flex-none px-6 pt-6\"\n ></mn-custom-body-host>\n }\n\n <!-- Steps header \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none px-6 pt-2 pb-4 border-b border-base-300 bg-base-100\">\n <div class=\"relative flex items-start\">\n <!-- Background line spanning from first circle center to last circle center -->\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.right]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-base-300 z-0\"\n ></div>\n\n <!-- Progress fill line -->\n @if (visibleSteps.length > 1) {\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.width]=\"'calc(' + (currentProgressIndex / (visibleSteps.length - 1)) * (100 - 100 / visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-success z-1 transition-all duration-500\"\n ></div>\n }\n\n @for (step of visibleSteps; track step.id; let i = $index; ) {\n <div\n (keyup.enter)=\"isFreeFlow ? goToStep(step) : null\"\n (click)=\"isFreeFlow ? goToStep(step) : null\"\n [attr.tabindex]=\"isFreeFlow && canNavigateToStep(step) ? 0 : null\"\n [class.cursor-pointer]=\"isFreeFlow && canNavigateToStep(step)\"\n class=\"relative flex flex-col items-center gap-1 z-10 w-0 flex-1\"\n >\n <div\n [ngClass]=\"{\n 'bg-primary text-primary-content': step.id === currentStepId,\n 'bg-success text-success-content': visitedStepIds.includes(step.id) && step.id !== currentStepId,\n 'bg-base-200 text-base-content/50': !visitedStepIds.includes(step.id) && step.id !== currentStepId\n }\"\n class=\"w-8 h-8 rounded-full flex items-center justify-center font-semibold transition-all text-sm\"\n >{{ i + 1 }}\n </div>\n <div\n [ngClass]=\"{\n 'text-base-content font-semibold': step.id === currentStepId,\n 'text-base-content/50': step.id !== currentStepId\n }\"\n class=\"text-xs text-center whitespace-nowrap hidden sm:block\"\n >{{ step.title }}\n </div>\n\n </div>\n }\n </div>\n </div>\n\n <!-- Step body \u2014 the only scrolling region in the wizard, shared by every step -->\n <div #stepScroller class=\"flex-auto min-h-0 overflow-y-auto px-6 py-6\">\n <div [style.min-height]=\"measuredMinHeight ? measuredMinHeight + 'px' : null\" class=\"min-h-48\">\n @for (step of config.steps; track step.id) {\n <div #stepWrapper [style.display]=\"step.id === currentStepId ? 'block' : 'none'\">\n <h3 class=\"text-lg font-semibold text-base-content mb-4\">{{ step.title }}</h3>\n <div class=\"text-base-content/80\">\n <!-- Form step -->\n @if (stepFormConfigs[step.id]) {\n <mn-form-body\n [config]=\"stepFormConfigs[step.id]\"\n [modalRef]=\"$any(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n ></mn-form-body>\n }\n\n <!-- Text body -->\n @if (!stepFormConfigs[step.id] && isTextBody(step)) {\n <div>\n {{ step.body }}\n </div>\n }\n\n <!-- Component / template body -->\n @if (stepBodyConfigs[step.id]) {\n <mn-custom-body-host\n [config]=\"stepBodyConfigs[step.id]\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-custom-body-host>\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Wizard-level errors (from onBeforeComplete) \u2014 fixed, above the footer -->\n @if (wizardErrors && (wizardErrors | keyvalue).length > 0) {\n <div class=\"flex flex-col gap-1 mx-6 mb-2 px-2 py-2 bg-red-50 rounded-md\">\n @for (err of wizardErrors | keyvalue; track err.key) {\n <div class=\"text-red-500 text-sm\">\n {{ err.value }}\n </div>\n }\n </div>\n }\n\n <!-- Footer \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none flex gap-3 px-6 pt-4 pb-6 border-t border-base-300 bg-base-100\">\n @if (!currentStep?.hideBack) {\n <button\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n @if (backIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ currentStep?.backLabel || (canGoBack ? labels.back : labels.close) }}\n </button>\n }\n\n <!-- Custom footer actions -->\n @if (config.footerActions) {\n <mn-footer-actions\n [actions]=\"config.footerActions\"\n [showIcons]=\"showActionIcons\"\n (actionClick)=\"handleFooterAction($any($event))\"\n ></mn-footer-actions>\n } @else {\n <div class=\"flex-1\"></div>\n }\n\n @if (!isLastStep) {\n <button\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid }\"\n (click)=\"next()\"\n >\n {{ currentStep?.nextLabel || labels.next }}\n @if (nextIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"ml-2\"></svg>\n }\n </button>\n }\n\n @if (isLastStep) {\n <button\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid || isCompleting }\"\n [disabled]=\"!isCurrentStepValid || isCompleting\"\n (click)=\"complete()\"\n >\n @if (completeIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ currentStep?.nextLabel || (isCompleting ? labels.completing : labels.complete) }}\n </button>\n }\n </div>\n</div>\n" }]
|
|
8257
|
+
args: [{ selector: 'mn-wizard-body', standalone: true, imports: [CommonModule, ReactiveFormsModule, MnButton, MnFormBodyComponent, MnCustomBodyHostComponent, MnFooterActionsComponent, LucideDynamicIcon], template: "<div class=\"flex flex-col flex-auto min-h-0\">\n @if (config.component || config.template) {\n <mn-custom-body-host\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block flex-none px-6 pt-6\"\n ></mn-custom-body-host>\n }\n\n <!-- Steps header \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none px-6 pt-2 pb-4 border-b border-base-300 bg-base-100\">\n <div class=\"relative flex items-start\">\n <!-- Background line spanning from first circle center to last circle center -->\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.right]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-base-300 z-0\"\n ></div>\n\n <!-- Progress fill line -->\n @if (visibleSteps.length > 1) {\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.width]=\"'calc(' + (currentProgressIndex / (visibleSteps.length - 1)) * (100 - 100 / visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-success z-1 transition-all duration-500\"\n ></div>\n }\n\n @for (step of visibleSteps; track step.id; let i = $index; ) {\n <div\n (keyup.enter)=\"isFreeFlow ? goToStep(step) : null\"\n (click)=\"isFreeFlow ? goToStep(step) : null\"\n [attr.tabindex]=\"isFreeFlow && canNavigateToStep(step) ? 0 : null\"\n [class.cursor-pointer]=\"isFreeFlow && canNavigateToStep(step)\"\n class=\"relative flex flex-col items-center gap-1 z-10 w-0 flex-1\"\n >\n <div\n [ngClass]=\"{\n 'bg-primary text-primary-content': step.id === currentStepId,\n 'bg-success text-success-content': visitedStepIds.includes(step.id) && step.id !== currentStepId,\n 'bg-base-200 text-base-content/50': !visitedStepIds.includes(step.id) && step.id !== currentStepId\n }\"\n class=\"w-8 h-8 rounded-full flex items-center justify-center font-semibold transition-all text-sm\"\n >{{ i + 1 }}\n </div>\n <div\n [ngClass]=\"{\n 'text-base-content font-semibold': step.id === currentStepId,\n 'text-base-content/50': step.id !== currentStepId\n }\"\n class=\"text-xs text-center whitespace-nowrap hidden sm:block\"\n >{{ step.title }}\n </div>\n\n </div>\n }\n </div>\n </div>\n\n <!-- Step body \u2014 the only scrolling region in the wizard, shared by every step -->\n <div #stepScroller class=\"flex-auto min-h-0 overflow-y-auto px-6 py-6\">\n <div [style.min-height]=\"measuredMinHeight ? measuredMinHeight + 'px' : null\" class=\"min-h-48\">\n @for (step of config.steps; track step.id) {\n <div #stepWrapper [style.display]=\"step.id === currentStepId ? 'block' : 'none'\">\n <h3 class=\"text-lg font-semibold text-base-content mb-4\">{{ step.title }}</h3>\n <div class=\"text-base-content/80\">\n <!-- Form step -->\n @if (stepFormConfigs[step.id]) {\n <mn-form-body\n [config]=\"stepFormConfigs[step.id]\"\n [modalRef]=\"$any(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n ></mn-form-body>\n }\n\n <!-- Text body -->\n @if (!stepFormConfigs[step.id] && isTextBody(step)) {\n <div>\n {{ step.body }}\n </div>\n }\n\n <!-- Component / template body -->\n @if (stepBodyConfigs[step.id]) {\n <mn-custom-body-host\n [config]=\"stepBodyConfigs[step.id]\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-custom-body-host>\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Wizard-level errors (from onBeforeComplete) \u2014 fixed, above the footer -->\n @if (wizardErrors && (wizardErrors | keyvalue).length > 0) {\n <div class=\"flex flex-col gap-1 mx-6 mb-2 px-2 py-2 bg-red-50 rounded-md\">\n @for (err of wizardErrors | keyvalue; track err.key) {\n <div class=\"text-red-500 text-sm\">\n {{ err.value }}\n </div>\n }\n </div>\n }\n\n <!-- Footer \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none flex gap-3 px-6 pt-4 pb-6 border-t border-base-300 bg-base-100\">\n @if (!currentStep?.hideBack) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n @if (backIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ currentStep?.backLabel || (canGoBack ? labels.back : labels.close) }}\n </button>\n }\n\n <!-- Custom footer actions -->\n @if (config.footerActions) {\n <mn-footer-actions\n [actions]=\"config.footerActions\"\n [showIcons]=\"showActionIcons\"\n (actionClick)=\"handleFooterAction($any($event))\"\n ></mn-footer-actions>\n } @else {\n <div class=\"flex-1\"></div>\n }\n\n @if (!isLastStep) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid }\"\n (click)=\"next()\"\n >\n {{ currentStep?.nextLabel || labels.next }}\n @if (nextIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"ml-2\"></svg>\n }\n </button>\n }\n\n @if (isLastStep) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid || isCompleting }\"\n [disabled]=\"!isCurrentStepValid || isCompleting\"\n (click)=\"complete()\"\n >\n @if (completeIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ currentStep?.nextLabel || (isCompleting ? labels.completing : labels.complete) }}\n </button>\n }\n </div>\n</div>\n" }]
|
|
8227
8258
|
}], propDecorators: { config: [{
|
|
8228
8259
|
type: Input
|
|
8229
8260
|
}], modalRef: [{
|
|
@@ -8355,11 +8386,11 @@ class MnConfirmationBodyComponent {
|
|
|
8355
8386
|
return this.config.cancel?.icon ?? MN_MODAL_ACTION_ICONS.cancel;
|
|
8356
8387
|
}
|
|
8357
8388
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnConfirmationBodyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
8358
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnConfirmationBodyComponent, isStandalone: true, selector: "mn-confirmation-body", inputs: { config: "config", modalRef: "modalRef" }, viewQueries: [{ propertyName: "formBody", first: true, predicate: MnFormBodyComponent, descendants: true }], ngImport: i0, template: "<div class=\"flex flex-col gap-6\" [ngClass]=\"toneClass\">\n <!-- Custom Content (Component or Template) -->\n @if (config.component || config.template) {\n <mn-custom-body-host\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-custom-body-host>\n }\n\n <div class=\"flex flex-col items-center text-center gap-6\">\n @if (config.tone === 'warning' || config.tone === 'danger') {\n <div\n class=\"w-12 h-12 rounded-full flex items-center justify-center\"\n [ngClass]=\"{\n 'bg-blue-100 text-blue-500': config.tone !== 'warning' && config.tone !== 'danger',\n 'bg-amber-100 text-amber-500': config.tone === 'warning',\n 'bg-red-100 text-red-500': config.tone === 'danger'\n }\">\n @if (config.tone === 'warning') {\n <svg [size]=\"24\" lucideTriangleAlert></svg>\n }\n @if (config.tone === 'danger') {\n <svg [size]=\"24\" lucideCircleAlert></svg>\n }\n </div>\n }\n\n @if (config.message) {\n <div class=\"text-base text-base-content/80 leading-relaxed max-w-[28rem]\">\n {{ config.message }}\n </div>\n }\n </div>\n\n <!-- Form Fields / Rows -->\n @if (hasFormFields) {\n <mn-form-body\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n (formStatusChange)=\"onFormStatusChange($event)\"\n ></mn-form-body>\n }\n\n <div class=\"flex gap-3 w-full pb-6 sticky bottom-0 bg-base-100 z-10\">\n <button\n mnButton\n [data]=\"{\n variant: getButtonVariant(cancelStyle),\n color: getButtonColor(cancelStyle)\n }\"\n (click)=\"cancel()\"\n >\n @if (cancelIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ cancelLabel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n [data]=\"{\n variant: getButtonVariant(confirmStyle),\n color: getButtonColor(confirmStyle),\n disabled: isConfirmDisabled\n }\"\n [disabled]=\"isConfirmDisabled\"\n (click)=\"confirm()\"\n >\n @if (confirmIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ confirmLabel }}\n </button>\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter", "hideCustomBody"], outputs: ["formStatusChange"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }, { kind: "component", type: LucideTriangleAlert, selector: "svg[lucideTriangleAlert], svg[lucideAlertTriangle]" }, { kind: "component", type: LucideCircleAlert, selector: "svg[lucideCircleAlert], svg[lucideAlertCircle]" }] });
|
|
8389
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnConfirmationBodyComponent, isStandalone: true, selector: "mn-confirmation-body", inputs: { config: "config", modalRef: "modalRef" }, viewQueries: [{ propertyName: "formBody", first: true, predicate: MnFormBodyComponent, descendants: true }], ngImport: i0, template: "<div class=\"flex flex-col gap-6\" [ngClass]=\"toneClass\">\n <!-- Custom Content (Component or Template) -->\n @if (config.component || config.template) {\n <mn-custom-body-host\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-custom-body-host>\n }\n\n <div class=\"flex flex-col items-center text-center gap-6\">\n @if (config.tone === 'warning' || config.tone === 'danger') {\n <div\n class=\"w-12 h-12 rounded-full flex items-center justify-center\"\n [ngClass]=\"{\n 'bg-blue-100 text-blue-500': config.tone !== 'warning' && config.tone !== 'danger',\n 'bg-amber-100 text-amber-500': config.tone === 'warning',\n 'bg-red-100 text-red-500': config.tone === 'danger'\n }\">\n @if (config.tone === 'warning') {\n <svg [size]=\"24\" lucideTriangleAlert></svg>\n }\n @if (config.tone === 'danger') {\n <svg [size]=\"24\" lucideCircleAlert></svg>\n }\n </div>\n }\n\n @if (config.message) {\n <div class=\"text-base text-base-content/80 leading-relaxed max-w-[28rem]\">\n {{ config.message }}\n </div>\n }\n </div>\n\n <!-- Form Fields / Rows -->\n @if (hasFormFields) {\n <mn-form-body\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n (formStatusChange)=\"onFormStatusChange($event)\"\n ></mn-form-body>\n }\n\n <div class=\"flex gap-3 w-full pb-6 sticky bottom-0 bg-base-100 z-10\">\n <button\n type=\"button\"\n mnButton\n [data]=\"{\n variant: getButtonVariant(cancelStyle),\n color: getButtonColor(cancelStyle)\n }\"\n (click)=\"cancel()\"\n >\n @if (cancelIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ cancelLabel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n type=\"button\"\n mnButton\n [data]=\"{\n variant: getButtonVariant(confirmStyle),\n color: getButtonColor(confirmStyle),\n disabled: isConfirmDisabled\n }\"\n [disabled]=\"isConfirmDisabled\"\n (click)=\"confirm()\"\n >\n @if (confirmIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ confirmLabel }}\n </button>\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter", "hideCustomBody"], outputs: ["formStatusChange"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }, { kind: "component", type: LucideTriangleAlert, selector: "svg[lucideTriangleAlert], svg[lucideAlertTriangle]" }, { kind: "component", type: LucideCircleAlert, selector: "svg[lucideCircleAlert], svg[lucideAlertCircle]" }] });
|
|
8359
8390
|
}
|
|
8360
8391
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnConfirmationBodyComponent, decorators: [{
|
|
8361
8392
|
type: Component,
|
|
8362
|
-
args: [{ selector: 'mn-confirmation-body', standalone: true, imports: [CommonModule, MnButton, MnFormBodyComponent, MnCustomBodyHostComponent, ReactiveFormsModule, LucideDynamicIcon, LucideTriangleAlert, LucideCircleAlert], template: "<div class=\"flex flex-col gap-6\" [ngClass]=\"toneClass\">\n <!-- Custom Content (Component or Template) -->\n @if (config.component || config.template) {\n <mn-custom-body-host\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-custom-body-host>\n }\n\n <div class=\"flex flex-col items-center text-center gap-6\">\n @if (config.tone === 'warning' || config.tone === 'danger') {\n <div\n class=\"w-12 h-12 rounded-full flex items-center justify-center\"\n [ngClass]=\"{\n 'bg-blue-100 text-blue-500': config.tone !== 'warning' && config.tone !== 'danger',\n 'bg-amber-100 text-amber-500': config.tone === 'warning',\n 'bg-red-100 text-red-500': config.tone === 'danger'\n }\">\n @if (config.tone === 'warning') {\n <svg [size]=\"24\" lucideTriangleAlert></svg>\n }\n @if (config.tone === 'danger') {\n <svg [size]=\"24\" lucideCircleAlert></svg>\n }\n </div>\n }\n\n @if (config.message) {\n <div class=\"text-base text-base-content/80 leading-relaxed max-w-[28rem]\">\n {{ config.message }}\n </div>\n }\n </div>\n\n <!-- Form Fields / Rows -->\n @if (hasFormFields) {\n <mn-form-body\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n (formStatusChange)=\"onFormStatusChange($event)\"\n ></mn-form-body>\n }\n\n <div class=\"flex gap-3 w-full pb-6 sticky bottom-0 bg-base-100 z-10\">\n <button\n mnButton\n [data]=\"{\n variant: getButtonVariant(cancelStyle),\n color: getButtonColor(cancelStyle)\n }\"\n (click)=\"cancel()\"\n >\n @if (cancelIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ cancelLabel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n [data]=\"{\n variant: getButtonVariant(confirmStyle),\n color: getButtonColor(confirmStyle),\n disabled: isConfirmDisabled\n }\"\n [disabled]=\"isConfirmDisabled\"\n (click)=\"confirm()\"\n >\n @if (confirmIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ confirmLabel }}\n </button>\n </div>\n</div>\n" }]
|
|
8393
|
+
args: [{ selector: 'mn-confirmation-body', standalone: true, imports: [CommonModule, MnButton, MnFormBodyComponent, MnCustomBodyHostComponent, ReactiveFormsModule, LucideDynamicIcon, LucideTriangleAlert, LucideCircleAlert], template: "<div class=\"flex flex-col gap-6\" [ngClass]=\"toneClass\">\n <!-- Custom Content (Component or Template) -->\n @if (config.component || config.template) {\n <mn-custom-body-host\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-custom-body-host>\n }\n\n <div class=\"flex flex-col items-center text-center gap-6\">\n @if (config.tone === 'warning' || config.tone === 'danger') {\n <div\n class=\"w-12 h-12 rounded-full flex items-center justify-center\"\n [ngClass]=\"{\n 'bg-blue-100 text-blue-500': config.tone !== 'warning' && config.tone !== 'danger',\n 'bg-amber-100 text-amber-500': config.tone === 'warning',\n 'bg-red-100 text-red-500': config.tone === 'danger'\n }\">\n @if (config.tone === 'warning') {\n <svg [size]=\"24\" lucideTriangleAlert></svg>\n }\n @if (config.tone === 'danger') {\n <svg [size]=\"24\" lucideCircleAlert></svg>\n }\n </div>\n }\n\n @if (config.message) {\n <div class=\"text-base text-base-content/80 leading-relaxed max-w-[28rem]\">\n {{ config.message }}\n </div>\n }\n </div>\n\n <!-- Form Fields / Rows -->\n @if (hasFormFields) {\n <mn-form-body\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n (formStatusChange)=\"onFormStatusChange($event)\"\n ></mn-form-body>\n }\n\n <div class=\"flex gap-3 w-full pb-6 sticky bottom-0 bg-base-100 z-10\">\n <button\n type=\"button\"\n mnButton\n [data]=\"{\n variant: getButtonVariant(cancelStyle),\n color: getButtonColor(cancelStyle)\n }\"\n (click)=\"cancel()\"\n >\n @if (cancelIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ cancelLabel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n type=\"button\"\n mnButton\n [data]=\"{\n variant: getButtonVariant(confirmStyle),\n color: getButtonColor(confirmStyle),\n disabled: isConfirmDisabled\n }\"\n [disabled]=\"isConfirmDisabled\"\n (click)=\"confirm()\"\n >\n @if (confirmIcon; as icon) {\n <svg [lucideIcon]=\"icon\" [size]=\"actionIconSize\" class=\"mr-2\"></svg>\n }\n {{ confirmLabel }}\n </button>\n </div>\n</div>\n" }]
|
|
8363
8394
|
}], propDecorators: { config: [{
|
|
8364
8395
|
type: Input
|
|
8365
8396
|
}], modalRef: [{
|
|
@@ -8370,6 +8401,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
8370
8401
|
}] } });
|
|
8371
8402
|
|
|
8372
8403
|
class MnModalShellComponent {
|
|
8404
|
+
lang = inject(MnLanguageService);
|
|
8405
|
+
/**
|
|
8406
|
+
* Accessible name for this control. Resolved through the conventional
|
|
8407
|
+
* `mnModal.close` key so an app can translate it, falling back to English when the
|
|
8408
|
+
* key is not defined rather than leaking the raw key into the UI.
|
|
8409
|
+
*/
|
|
8410
|
+
get closeModalLabel() {
|
|
8411
|
+
return this.lang.translateIfPresent('mnModal.close') ?? 'Close modal';
|
|
8412
|
+
}
|
|
8373
8413
|
el = inject(ElementRef);
|
|
8374
8414
|
cdr = inject(ChangeDetectorRef);
|
|
8375
8415
|
/** Downward release speed (px/ms) above which a short drag still dismisses — a "flick".
|
|
@@ -8790,7 +8830,7 @@ class MnModalShellComponent {
|
|
|
8790
8830
|
}
|
|
8791
8831
|
}
|
|
8792
8832
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
8793
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnModalShellComponent, isStandalone: true, selector: "mn-modal-shell", inputs: { config: "config", modalRef: "modalRef" }, host: { listeners: { "document:keydown.escape": "onEscapeKey($event)" }, properties: { "class": "this.hostClasses" } }, viewQueries: [{ propertyName: "wizardBody", first: true, predicate: MnWizardBodyComponent, descendants: true, isSignal: true }], ngImport: i0, template: "@if (showBackdrop) {\n <!-- The backdrop is a visual overlay \u2014 keyboard dismiss is handled at component level via Escape key -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" (click)=\"onBackdropClick()\"></div>\n}\n\n<!--\n The container deliberately does NOT stop click propagation. The backdrop is a\n *sibling* element (absolutely positioned behind this one), never an ancestor, so\n clicks in here can't reach `onBackdropClick()` anyway. Swallowing them instead\n broke every \"click outside me\" handler inside a modal \u2014 component-level\n `document:click` listeners (the multi-select panel, the table filter popover)\n never fired, so those overlays stayed open.\n-->\n<div\n class=\"modal-container relative bg-base-100 rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col\"\n [class.sheet-dragging]=\"isDraggingSheet\"\n [ngClass]=\"containerSizeClass\"\n [style.height]=\"containerHeightStyle\"\n [style.transform]=\"sheetDragY ? 'translateY(' + sheetDragY + 'px)' : null\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"config.title ? 'mn-modal-title' : null\"\n [attr.aria-describedby]=\"config.description ? 'mn-modal-description' : null\"\n tabindex=\"-1\"\n>\n @if (isMobileSheet) {\n <!-- Drag handle for swipe-to-dismiss, visible only on mobile bottom sheets -->\n <div\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n class=\"sm:hidden flex justify-center pt-2 pb-1 touch-none cursor-grab\"\n >\n <div class=\"h-1.5 w-10 rounded-full bg-base-300\"></div>\n </div>\n }\n\n <!-- On mobile the whole header doubles as a swipe-to-dismiss surface (drags that\n start on the close button are ignored). The grabber above is the visual cue. -->\n <div [class.border-b]=\"config.kind !== ModalKind.WIZARD\"\n [class.border-base-300]=\"config.kind !== ModalKind.WIZARD\"\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n [class.cursor-grab]=\"isMobileSheet\"\n [class.sm:cursor-auto]=\"isMobileSheet\"\n [class.sm:touch-auto]=\"isMobileSheet\"\n [class.touch-none]=\"isMobileSheet\"\n class=\"flex items-center justify-between p-6\">\n <div class=\"flex flex-col gap-0.5\">\n @if (config.title) {\n <!-- On small screens the wizard's step labels (under the progress circles) are hidden,\n so surface the active step name here as \"Title - Step\" instead. -->\n <h2 class=\"m-0 text-xl font-semibold text-base-content\"\n id=\"mn-modal-title\">{{ config.title }}@if (config.kind === ModalKind.WIZARD && wizardStepTitle()) {\n <span class=\"sm:hidden font-normal text-base-content/60\"> - {{ wizardStepTitle() }}</span>\n }</h2>\n }\n @if (config.subtitle) {\n <p class=\"m-0 text-sm text-base-content/60 font-normal\">{{ config.subtitle }}</p>\n }\n </div>\n @if (showCloseButton) {\n <div [class]=\"isMobileSheet ? 'hidden sm:flex' : ''\">\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'gray', hover: true, borderRadius: 'md' }\"\n type=\"button\"\n class=\"w-8 h-8\"\n (click)=\"onCloseButtonClick()\"\n aria-label=\"Close modal\"\n >\n <svg lucideX [size]=\"18\"></svg>\n </button>\n </div>\n }\n </div>\n @if (config.description) {\n <p class=\"m-0 px-6 text-sm text-base-content/60 leading-relaxed\" id=\"mn-modal-description\">{{ config.description }}</p>\n }\n\n @if (config.kind === ModalKind.WIZARD) {\n <!-- Wizard manages its own internal scrolling so the steps header and footer\n stay fixed while only the step body scrolls. No padding/scroll here. -->\n <div class=\"flex-auto min-h-0 overflow-hidden flex flex-col\">\n <mn-wizard-body\n [config]=\"asWizard(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"flex-auto min-h-0 flex flex-col\"\n ></mn-wizard-body>\n </div>\n } @else {\n <div class=\"flex-1 overflow-y-auto px-6 pt-6\">\n @if (config.kind === ModalKind.FORM) {\n <mn-form-body\n [config]=\"asForm(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block h-full\"\n ></mn-form-body>\n }\n\n @if (config.kind === ModalKind.CONFIRMATION) {\n <mn-confirmation-body\n [config]=\"asConfirmation(config)\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-confirmation-body>\n }\n\n @if (config.kind === ModalKind.CUSTOM) {\n <mn-custom-body-host\n [config]=\"asCustom(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block pb-6\"\n ></mn-custom-body-host>\n }\n </div>\n }\n\n <!-- Custom Footer Actions (not for wizard modals, they render their own) -->\n @if (hasCustomFooterActions && config.kind !== ModalKind.WIZARD) {\n <div class=\"flex gap-3 p-6 border-t border-base-300\">\n <mn-footer-actions\n [actions]=\"config.footerActions || []\"\n [showIcons]=\"config.showActionIcons !== false\"\n (actionClick)=\"onFooterAction($event)\"\n ></mn-footer-actions>\n </div>\n }\n</div>\n", styles: [":host{--mn-sheet-ease: cubic-bezier(.32, .72, 0, 1);position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;transition:transform .3s ease-in-out,filter .3s ease-in-out,opacity .3s ease-in-out}:host(.is-stacked){transform:scale(.96) translateY(-1rem);filter:brightness(.9) blur(1px);pointer-events:none;opacity:.8}.modal-container{transition:transform .35s var(--mn-sheet-ease),min-height .25s var(--mn-sheet-ease)}.modal-container.sheet-dragging{transition:none}:host(.swipe-dismissing) .modal-container,:host(.swipe-dismissing).closing .modal-container{animation:none!important;transition:transform .3s var(--mn-sheet-ease)}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{opacity:0;transform:translateY(-1rem)}to{opacity:1;transform:translateY(0)}}@keyframes zoomIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes slideOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(1rem)}}@keyframes zoomOut{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}@keyframes slideUpIn{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}:host(.anim-slide) .modal-container{animation:slideIn .2s ease-in-out}:host(.anim-fade) .modal-container{animation:fadeIn .2s ease-in-out}:host(.anim-zoom) .modal-container{animation:zoomIn .2s ease-in-out}:host(.closing) .modal-backdrop{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-slide .modal-container{animation:slideOut .15s ease-in-out forwards}:host(.closing).anim-fade .modal-container{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-zoom .modal-container{animation:zoomOut .15s ease-in-out forwards}@media(max-width:639.98px){:host(.mobile-sheet){align-items:flex-end}:host(.mobile-sheet) .modal-container{width:100%;max-width:100%;min-height:0;max-height:92vh;padding-bottom:env(safe-area-inset-bottom);border-radius:1rem 1rem 0 0;animation:slideUpIn .45s var(--mn-sheet-ease)}:host-context(.mn-keyboard-open).mobile-sheet .modal-container{min-height:92vh}:host(.mobile-sheet).anim-slide .modal-container,:host(.mobile-sheet).anim-fade .modal-container,:host(.mobile-sheet).anim-zoom .modal-container{animation:slideUpIn .45s var(--mn-sheet-ease)}:host(.mobile-sheet).closing .modal-container,:host(.mobile-sheet).closing.anim-slide .modal-container,:host(.mobile-sheet).closing.anim-fade .modal-container,:host(.mobile-sheet).closing.anim-zoom .modal-container{animation:none!important;transform:translateY(100%);opacity:0;transition:transform .45s var(--mn-sheet-ease),opacity .45s var(--mn-sheet-ease)}}@media(prefers-reduced-motion:reduce){:host,:host .modal-backdrop,:host .modal-container{animation-duration:.01ms!important;animation-delay:0ms!important;transition-duration:.01ms!important}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnWizardBodyComponent, selector: "mn-wizard-body", inputs: ["config", "modalRef"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter", "hideCustomBody"], outputs: ["formStatusChange"] }, { kind: "component", type: MnConfirmationBodyComponent, selector: "mn-confirmation-body", inputs: ["config", "modalRef"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }, { kind: "component", type: MnFooterActionsComponent, selector: "mn-footer-actions", inputs: ["actions", "showIcons"], outputs: ["actionClick"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: LucideX, selector: "svg[lucideX]" }] });
|
|
8833
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnModalShellComponent, isStandalone: true, selector: "mn-modal-shell", inputs: { config: "config", modalRef: "modalRef" }, host: { listeners: { "document:keydown.escape": "onEscapeKey($event)" }, properties: { "class": "this.hostClasses" } }, viewQueries: [{ propertyName: "wizardBody", first: true, predicate: MnWizardBodyComponent, descendants: true, isSignal: true }], ngImport: i0, template: "@if (showBackdrop) {\n <!-- The backdrop is a visual overlay \u2014 keyboard dismiss is handled at component level via Escape key -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" (click)=\"onBackdropClick()\"></div>\n}\n\n<!--\n The container deliberately does NOT stop click propagation. The backdrop is a\n *sibling* element (absolutely positioned behind this one), never an ancestor, so\n clicks in here can't reach `onBackdropClick()` anyway. Swallowing them instead\n broke every \"click outside me\" handler inside a modal \u2014 component-level\n `document:click` listeners (the multi-select panel, the table filter popover)\n never fired, so those overlays stayed open.\n-->\n<div\n class=\"modal-container relative bg-base-100 rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col\"\n [class.sheet-dragging]=\"isDraggingSheet\"\n [ngClass]=\"containerSizeClass\"\n [style.height]=\"containerHeightStyle\"\n [style.transform]=\"sheetDragY ? 'translateY(' + sheetDragY + 'px)' : null\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"config.title ? 'mn-modal-title' : null\"\n [attr.aria-describedby]=\"config.description ? 'mn-modal-description' : null\"\n tabindex=\"-1\"\n>\n @if (isMobileSheet) {\n <!-- Drag handle for swipe-to-dismiss, visible only on mobile bottom sheets -->\n <div\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n class=\"sm:hidden flex justify-center pt-2 pb-1 touch-none cursor-grab\"\n >\n <div class=\"h-1.5 w-10 rounded-full bg-base-300\"></div>\n </div>\n }\n\n <!-- On mobile the whole header doubles as a swipe-to-dismiss surface (drags that\n start on the close button are ignored). The grabber above is the visual cue. -->\n <div [class.border-b]=\"config.kind !== ModalKind.WIZARD\"\n [class.border-base-300]=\"config.kind !== ModalKind.WIZARD\"\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n [class.cursor-grab]=\"isMobileSheet\"\n [class.sm:cursor-auto]=\"isMobileSheet\"\n [class.sm:touch-auto]=\"isMobileSheet\"\n [class.touch-none]=\"isMobileSheet\"\n class=\"flex items-center justify-between p-6\">\n <div class=\"flex flex-col gap-0.5\">\n @if (config.title) {\n <!-- On small screens the wizard's step labels (under the progress circles) are hidden,\n so surface the active step name here as \"Title - Step\" instead. -->\n <h2 class=\"m-0 text-xl font-semibold text-base-content\"\n id=\"mn-modal-title\">{{ config.title }}@if (config.kind === ModalKind.WIZARD && wizardStepTitle()) {\n <span class=\"sm:hidden font-normal text-base-content/60\"> - {{ wizardStepTitle() }}</span>\n }</h2>\n }\n @if (config.subtitle) {\n <p class=\"m-0 text-sm text-base-content/60 font-normal\">{{ config.subtitle }}</p>\n }\n </div>\n @if (showCloseButton) {\n <div [class]=\"isMobileSheet ? 'hidden sm:flex' : ''\">\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'gray', hover: true, borderRadius: 'md' }\"\n type=\"button\"\n class=\"w-8 h-8\"\n (click)=\"onCloseButtonClick()\"\n [attr.aria-label]=\"closeModalLabel\"\n >\n <svg lucideX [size]=\"18\"></svg>\n </button>\n </div>\n }\n </div>\n @if (config.description) {\n <p class=\"m-0 px-6 text-sm text-base-content/60 leading-relaxed\" id=\"mn-modal-description\">{{ config.description }}</p>\n }\n\n @if (config.kind === ModalKind.WIZARD) {\n <!-- Wizard manages its own internal scrolling so the steps header and footer\n stay fixed while only the step body scrolls. No padding/scroll here. -->\n <div class=\"flex-auto min-h-0 overflow-hidden flex flex-col\">\n <mn-wizard-body\n [config]=\"asWizard(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"flex-auto min-h-0 flex flex-col\"\n ></mn-wizard-body>\n </div>\n } @else {\n <div class=\"flex-1 overflow-y-auto px-6 pt-6\">\n @if (config.kind === ModalKind.FORM) {\n <mn-form-body\n [config]=\"asForm(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block h-full\"\n ></mn-form-body>\n }\n\n @if (config.kind === ModalKind.CONFIRMATION) {\n <mn-confirmation-body\n [config]=\"asConfirmation(config)\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-confirmation-body>\n }\n\n @if (config.kind === ModalKind.CUSTOM) {\n <mn-custom-body-host\n [config]=\"asCustom(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block pb-6\"\n ></mn-custom-body-host>\n }\n </div>\n }\n\n <!-- Custom Footer Actions (not for wizard modals, they render their own) -->\n @if (hasCustomFooterActions && config.kind !== ModalKind.WIZARD) {\n <div class=\"flex gap-3 p-6 border-t border-base-300\">\n <mn-footer-actions\n [actions]=\"config.footerActions || []\"\n [showIcons]=\"config.showActionIcons !== false\"\n (actionClick)=\"onFooterAction($event)\"\n ></mn-footer-actions>\n </div>\n }\n</div>\n", styles: [":host{--mn-sheet-ease: cubic-bezier(.32, .72, 0, 1);position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;transition:transform .3s ease-in-out,filter .3s ease-in-out,opacity .3s ease-in-out}:host(.is-stacked){transform:scale(.96) translateY(-1rem);filter:brightness(.9) blur(1px);pointer-events:none;opacity:.8}.modal-container{transition:transform .35s var(--mn-sheet-ease),min-height .25s var(--mn-sheet-ease)}.modal-container.sheet-dragging{transition:none}:host(.swipe-dismissing) .modal-container,:host(.swipe-dismissing).closing .modal-container{animation:none!important;transition:transform .3s var(--mn-sheet-ease)}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{opacity:0;transform:translateY(-1rem)}to{opacity:1;transform:translateY(0)}}@keyframes zoomIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes slideOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(1rem)}}@keyframes zoomOut{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}@keyframes slideUpIn{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}:host(.anim-slide) .modal-container{animation:slideIn .2s ease-in-out}:host(.anim-fade) .modal-container{animation:fadeIn .2s ease-in-out}:host(.anim-zoom) .modal-container{animation:zoomIn .2s ease-in-out}:host(.closing) .modal-backdrop{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-slide .modal-container{animation:slideOut .15s ease-in-out forwards}:host(.closing).anim-fade .modal-container{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-zoom .modal-container{animation:zoomOut .15s ease-in-out forwards}@media(max-width:639.98px){:host(.mobile-sheet){align-items:flex-end}:host(.mobile-sheet) .modal-container{width:100%;max-width:100%;min-height:0;max-height:92vh;padding-bottom:env(safe-area-inset-bottom);border-radius:1rem 1rem 0 0;animation:slideUpIn .45s var(--mn-sheet-ease)}:host-context(.mn-keyboard-open).mobile-sheet .modal-container{min-height:92vh}:host(.mobile-sheet).anim-slide .modal-container,:host(.mobile-sheet).anim-fade .modal-container,:host(.mobile-sheet).anim-zoom .modal-container{animation:slideUpIn .45s var(--mn-sheet-ease)}:host(.mobile-sheet).closing .modal-container,:host(.mobile-sheet).closing.anim-slide .modal-container,:host(.mobile-sheet).closing.anim-fade .modal-container,:host(.mobile-sheet).closing.anim-zoom .modal-container{animation:none!important;transform:translateY(100%);opacity:0;transition:transform .45s var(--mn-sheet-ease),opacity .45s var(--mn-sheet-ease)}}@media(prefers-reduced-motion:reduce){:host,:host .modal-backdrop,:host .modal-container{animation-duration:.01ms!important;animation-delay:0ms!important;transition-duration:.01ms!important}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnWizardBodyComponent, selector: "mn-wizard-body", inputs: ["config", "modalRef"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter", "hideCustomBody"], outputs: ["formStatusChange"] }, { kind: "component", type: MnConfirmationBodyComponent, selector: "mn-confirmation-body", inputs: ["config", "modalRef"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }, { kind: "component", type: MnFooterActionsComponent, selector: "mn-footer-actions", inputs: ["actions", "showIcons"], outputs: ["actionClick"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: LucideX, selector: "svg[lucideX]" }] });
|
|
8794
8834
|
}
|
|
8795
8835
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalShellComponent, decorators: [{
|
|
8796
8836
|
type: Component,
|
|
@@ -8803,7 +8843,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
8803
8843
|
MnFooterActionsComponent,
|
|
8804
8844
|
MnButton,
|
|
8805
8845
|
LucideX,
|
|
8806
|
-
], template: "@if (showBackdrop) {\n <!-- The backdrop is a visual overlay \u2014 keyboard dismiss is handled at component level via Escape key -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" (click)=\"onBackdropClick()\"></div>\n}\n\n<!--\n The container deliberately does NOT stop click propagation. The backdrop is a\n *sibling* element (absolutely positioned behind this one), never an ancestor, so\n clicks in here can't reach `onBackdropClick()` anyway. Swallowing them instead\n broke every \"click outside me\" handler inside a modal \u2014 component-level\n `document:click` listeners (the multi-select panel, the table filter popover)\n never fired, so those overlays stayed open.\n-->\n<div\n class=\"modal-container relative bg-base-100 rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col\"\n [class.sheet-dragging]=\"isDraggingSheet\"\n [ngClass]=\"containerSizeClass\"\n [style.height]=\"containerHeightStyle\"\n [style.transform]=\"sheetDragY ? 'translateY(' + sheetDragY + 'px)' : null\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"config.title ? 'mn-modal-title' : null\"\n [attr.aria-describedby]=\"config.description ? 'mn-modal-description' : null\"\n tabindex=\"-1\"\n>\n @if (isMobileSheet) {\n <!-- Drag handle for swipe-to-dismiss, visible only on mobile bottom sheets -->\n <div\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n class=\"sm:hidden flex justify-center pt-2 pb-1 touch-none cursor-grab\"\n >\n <div class=\"h-1.5 w-10 rounded-full bg-base-300\"></div>\n </div>\n }\n\n <!-- On mobile the whole header doubles as a swipe-to-dismiss surface (drags that\n start on the close button are ignored). The grabber above is the visual cue. -->\n <div [class.border-b]=\"config.kind !== ModalKind.WIZARD\"\n [class.border-base-300]=\"config.kind !== ModalKind.WIZARD\"\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n [class.cursor-grab]=\"isMobileSheet\"\n [class.sm:cursor-auto]=\"isMobileSheet\"\n [class.sm:touch-auto]=\"isMobileSheet\"\n [class.touch-none]=\"isMobileSheet\"\n class=\"flex items-center justify-between p-6\">\n <div class=\"flex flex-col gap-0.5\">\n @if (config.title) {\n <!-- On small screens the wizard's step labels (under the progress circles) are hidden,\n so surface the active step name here as \"Title - Step\" instead. -->\n <h2 class=\"m-0 text-xl font-semibold text-base-content\"\n id=\"mn-modal-title\">{{ config.title }}@if (config.kind === ModalKind.WIZARD && wizardStepTitle()) {\n <span class=\"sm:hidden font-normal text-base-content/60\"> - {{ wizardStepTitle() }}</span>\n }</h2>\n }\n @if (config.subtitle) {\n <p class=\"m-0 text-sm text-base-content/60 font-normal\">{{ config.subtitle }}</p>\n }\n </div>\n @if (showCloseButton) {\n <div [class]=\"isMobileSheet ? 'hidden sm:flex' : ''\">\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'gray', hover: true, borderRadius: 'md' }\"\n type=\"button\"\n class=\"w-8 h-8\"\n (click)=\"onCloseButtonClick()\"\n aria-label=\"
|
|
8846
|
+
], template: "@if (showBackdrop) {\n <!-- The backdrop is a visual overlay \u2014 keyboard dismiss is handled at component level via Escape key -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" (click)=\"onBackdropClick()\"></div>\n}\n\n<!--\n The container deliberately does NOT stop click propagation. The backdrop is a\n *sibling* element (absolutely positioned behind this one), never an ancestor, so\n clicks in here can't reach `onBackdropClick()` anyway. Swallowing them instead\n broke every \"click outside me\" handler inside a modal \u2014 component-level\n `document:click` listeners (the multi-select panel, the table filter popover)\n never fired, so those overlays stayed open.\n-->\n<div\n class=\"modal-container relative bg-base-100 rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col\"\n [class.sheet-dragging]=\"isDraggingSheet\"\n [ngClass]=\"containerSizeClass\"\n [style.height]=\"containerHeightStyle\"\n [style.transform]=\"sheetDragY ? 'translateY(' + sheetDragY + 'px)' : null\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"config.title ? 'mn-modal-title' : null\"\n [attr.aria-describedby]=\"config.description ? 'mn-modal-description' : null\"\n tabindex=\"-1\"\n>\n @if (isMobileSheet) {\n <!-- Drag handle for swipe-to-dismiss, visible only on mobile bottom sheets -->\n <div\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n class=\"sm:hidden flex justify-center pt-2 pb-1 touch-none cursor-grab\"\n >\n <div class=\"h-1.5 w-10 rounded-full bg-base-300\"></div>\n </div>\n }\n\n <!-- On mobile the whole header doubles as a swipe-to-dismiss surface (drags that\n start on the close button are ignored). The grabber above is the visual cue. -->\n <div [class.border-b]=\"config.kind !== ModalKind.WIZARD\"\n [class.border-base-300]=\"config.kind !== ModalKind.WIZARD\"\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n [class.cursor-grab]=\"isMobileSheet\"\n [class.sm:cursor-auto]=\"isMobileSheet\"\n [class.sm:touch-auto]=\"isMobileSheet\"\n [class.touch-none]=\"isMobileSheet\"\n class=\"flex items-center justify-between p-6\">\n <div class=\"flex flex-col gap-0.5\">\n @if (config.title) {\n <!-- On small screens the wizard's step labels (under the progress circles) are hidden,\n so surface the active step name here as \"Title - Step\" instead. -->\n <h2 class=\"m-0 text-xl font-semibold text-base-content\"\n id=\"mn-modal-title\">{{ config.title }}@if (config.kind === ModalKind.WIZARD && wizardStepTitle()) {\n <span class=\"sm:hidden font-normal text-base-content/60\"> - {{ wizardStepTitle() }}</span>\n }</h2>\n }\n @if (config.subtitle) {\n <p class=\"m-0 text-sm text-base-content/60 font-normal\">{{ config.subtitle }}</p>\n }\n </div>\n @if (showCloseButton) {\n <div [class]=\"isMobileSheet ? 'hidden sm:flex' : ''\">\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'gray', hover: true, borderRadius: 'md' }\"\n type=\"button\"\n class=\"w-8 h-8\"\n (click)=\"onCloseButtonClick()\"\n [attr.aria-label]=\"closeModalLabel\"\n >\n <svg lucideX [size]=\"18\"></svg>\n </button>\n </div>\n }\n </div>\n @if (config.description) {\n <p class=\"m-0 px-6 text-sm text-base-content/60 leading-relaxed\" id=\"mn-modal-description\">{{ config.description }}</p>\n }\n\n @if (config.kind === ModalKind.WIZARD) {\n <!-- Wizard manages its own internal scrolling so the steps header and footer\n stay fixed while only the step body scrolls. No padding/scroll here. -->\n <div class=\"flex-auto min-h-0 overflow-hidden flex flex-col\">\n <mn-wizard-body\n [config]=\"asWizard(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"flex-auto min-h-0 flex flex-col\"\n ></mn-wizard-body>\n </div>\n } @else {\n <div class=\"flex-1 overflow-y-auto px-6 pt-6\">\n @if (config.kind === ModalKind.FORM) {\n <mn-form-body\n [config]=\"asForm(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block h-full\"\n ></mn-form-body>\n }\n\n @if (config.kind === ModalKind.CONFIRMATION) {\n <mn-confirmation-body\n [config]=\"asConfirmation(config)\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-confirmation-body>\n }\n\n @if (config.kind === ModalKind.CUSTOM) {\n <mn-custom-body-host\n [config]=\"asCustom(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block pb-6\"\n ></mn-custom-body-host>\n }\n </div>\n }\n\n <!-- Custom Footer Actions (not for wizard modals, they render their own) -->\n @if (hasCustomFooterActions && config.kind !== ModalKind.WIZARD) {\n <div class=\"flex gap-3 p-6 border-t border-base-300\">\n <mn-footer-actions\n [actions]=\"config.footerActions || []\"\n [showIcons]=\"config.showActionIcons !== false\"\n (actionClick)=\"onFooterAction($event)\"\n ></mn-footer-actions>\n </div>\n }\n</div>\n", styles: [":host{--mn-sheet-ease: cubic-bezier(.32, .72, 0, 1);position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;transition:transform .3s ease-in-out,filter .3s ease-in-out,opacity .3s ease-in-out}:host(.is-stacked){transform:scale(.96) translateY(-1rem);filter:brightness(.9) blur(1px);pointer-events:none;opacity:.8}.modal-container{transition:transform .35s var(--mn-sheet-ease),min-height .25s var(--mn-sheet-ease)}.modal-container.sheet-dragging{transition:none}:host(.swipe-dismissing) .modal-container,:host(.swipe-dismissing).closing .modal-container{animation:none!important;transition:transform .3s var(--mn-sheet-ease)}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{opacity:0;transform:translateY(-1rem)}to{opacity:1;transform:translateY(0)}}@keyframes zoomIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes slideOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(1rem)}}@keyframes zoomOut{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}@keyframes slideUpIn{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}:host(.anim-slide) .modal-container{animation:slideIn .2s ease-in-out}:host(.anim-fade) .modal-container{animation:fadeIn .2s ease-in-out}:host(.anim-zoom) .modal-container{animation:zoomIn .2s ease-in-out}:host(.closing) .modal-backdrop{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-slide .modal-container{animation:slideOut .15s ease-in-out forwards}:host(.closing).anim-fade .modal-container{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-zoom .modal-container{animation:zoomOut .15s ease-in-out forwards}@media(max-width:639.98px){:host(.mobile-sheet){align-items:flex-end}:host(.mobile-sheet) .modal-container{width:100%;max-width:100%;min-height:0;max-height:92vh;padding-bottom:env(safe-area-inset-bottom);border-radius:1rem 1rem 0 0;animation:slideUpIn .45s var(--mn-sheet-ease)}:host-context(.mn-keyboard-open).mobile-sheet .modal-container{min-height:92vh}:host(.mobile-sheet).anim-slide .modal-container,:host(.mobile-sheet).anim-fade .modal-container,:host(.mobile-sheet).anim-zoom .modal-container{animation:slideUpIn .45s var(--mn-sheet-ease)}:host(.mobile-sheet).closing .modal-container,:host(.mobile-sheet).closing.anim-slide .modal-container,:host(.mobile-sheet).closing.anim-fade .modal-container,:host(.mobile-sheet).closing.anim-zoom .modal-container{animation:none!important;transform:translateY(100%);opacity:0;transition:transform .45s var(--mn-sheet-ease),opacity .45s var(--mn-sheet-ease)}}@media(prefers-reduced-motion:reduce){:host,:host .modal-backdrop,:host .modal-container{animation-duration:.01ms!important;animation-delay:0ms!important;transition-duration:.01ms!important}}\n"] }]
|
|
8807
8847
|
}], propDecorators: { config: [{
|
|
8808
8848
|
type: Input
|
|
8809
8849
|
}], modalRef: [{
|
|
@@ -8916,12 +8956,16 @@ class MnList extends MnSelectableCollectionBase {
|
|
|
8916
8956
|
this.loadMoreRows();
|
|
8917
8957
|
}
|
|
8918
8958
|
}
|
|
8959
|
+
/** Accessible name for the scrollable list region. */
|
|
8960
|
+
get listRegionLabel() {
|
|
8961
|
+
return this.resolveLabel(undefined, 'mnCollection.dataList', 'Data list');
|
|
8962
|
+
}
|
|
8919
8963
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnList, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
8920
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnList, isStandalone: true, selector: "mn-list", outputs: { itemClick: "itemClick" }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n dataSource.canSearch ||\n dataSource.toolbarLeftTemplate ||\n dataSource.toolbarRightTemplate ||\n dataSource.toolbarTemplate\n) {\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-list-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n ></mn-lib-input-field>\n }\n <!-- `toolbarTemplate` is the deprecated name for this slot; honouring it here\n keeps existing callers rendering exactly where they used to. -->\n @if (dataSource.toolbarRightTemplate ?? dataSource.toolbarTemplate; as rightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"rightTemplate\"></ng-container>\n </div>\n }\n </div>\n </div>\n}\n\n<!-- List wrapper -->\n<div\n #collectionBody\n [style.min-height.px]=\"bodyMinHeight\"\n class=\"w-full\"\n [class.border]=\"dataSource.appearance?.bordered\"\n [class.border-base-300]=\"dataSource.appearance?.bordered\"\n [class.rounded]=\"dataSource.appearance?.bordered\"\n role=\"list\"\n aria-label=\"
|
|
8964
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnList, isStandalone: true, selector: "mn-list", outputs: { itemClick: "itemClick" }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n dataSource.canSearch ||\n dataSource.toolbarLeftTemplate ||\n dataSource.toolbarRightTemplate ||\n dataSource.toolbarTemplate\n) {\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <!-- Enter must not submit a surrounding form: the search box is the\n collection's own chrome, not a field of whatever form it sits in. -->\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-list-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n ></mn-lib-input-field>\n }\n <!-- `toolbarTemplate` is the deprecated name for this slot; honouring it here\n keeps existing callers rendering exactly where they used to. -->\n @if (dataSource.toolbarRightTemplate ?? dataSource.toolbarTemplate; as rightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"rightTemplate\"></ng-container>\n </div>\n }\n </div>\n </div>\n}\n\n<!-- List wrapper -->\n<div\n #collectionBody\n [style.min-height.px]=\"bodyMinHeight\"\n class=\"w-full\"\n [class.border]=\"dataSource.appearance?.bordered\"\n [class.border-base-300]=\"dataSource.appearance?.bordered\"\n [class.rounded]=\"dataSource.appearance?.bordered\"\n role=\"list\"\n [attr.aria-label]=\"listRegionLabel\"\n>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <div [class.py-2]=\"dataSource.appearance?.compact\" class=\"px-4 py-3\" role=\"listitem\">\n @if (isTemplateRef(dataSource.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(dataSource.skeleton)\"></ng-container>\n } @else {\n <div class=\"flex flex-col gap-1\">\n @for (line of skeletonLines; track $index) {\n <mn-skeleton [data]=\"line\"></mn-skeleton>\n }\n </div>\n }\n </div>\n @if (!$last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </div>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </div>\n }\n\n <!-- Select all (multi-select) -->\n @if (isMultiSelect && filteredItems.length > 0) {\n <div class=\"flex items-center gap-2 px-4 py-2 bg-base-200 text-sm\">\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-list-select-all', label: 'Select all', size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n @if (dataSource.appearance?.dividers !== false) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n\n <!-- Data items -->\n @for (item of paginatedItems; track trackByID($index, item); let odd = $odd; let last = $last) {\n <div\n class=\"flex items-center gap-2 bg-base-100 transition-colors duration-150\"\n [ngClass]=\"{'bg-primary/10': isSelected(item)}\"\n [class.bg-base-200]=\"!isSelected(item) && odd && dataSource.appearance?.dividers !== false\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n [class.px-4]=\"true\"\n [class.py-3]=\"!dataSource.appearance?.compact\"\n [class.py-2]=\"dataSource.appearance?.compact\"\n role=\"listitem\"\n (keyup.enter)=\"onItemClick(item)\"\n (click)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div (click)=\"$event.stopPropagation()\" class=\"shrink-0\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(item)\"\n [checked]=\"isSelected(item)\"\n [props]=\"{ id: 'mn-list-item-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n }\n\n <!-- Item content via template -->\n <div class=\"flex-1 min-w-0\">\n <ng-container\n [ngTemplateOutlet]=\"dataSource.itemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n ></ng-container>\n </div>\n </div>\n @if (!last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n }\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-list\"\n></mn-collection-pagination>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props", "checked"], outputs: ["checkedChange"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "component", type: MnCollectionPagination, selector: "mn-collection-pagination", inputs: ["idPrefix", "isPaginated", "isServerPaginated", "showLoadMore", "loadingMoreRows", "currentPage", "pageSize", "totalPages", "totalItemCount", "visiblePages", "pageSizeSelectOptions", "labels"], outputs: ["loadMore", "pageChange", "pageSizeChange"] }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
8921
8965
|
}
|
|
8922
8966
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnList, decorators: [{
|
|
8923
8967
|
type: Component,
|
|
8924
|
-
args: [{ selector: 'mn-list', standalone: true, imports: [NgClass, NgTemplateOutlet, FormsModule, MnCheckbox, MnInputField, MnSkeleton, MnCollectionPagination, LucideDynamicIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n dataSource.canSearch ||\n dataSource.toolbarLeftTemplate ||\n dataSource.toolbarRightTemplate ||\n dataSource.toolbarTemplate\n) {\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-list-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n ></mn-lib-input-field>\n }\n <!-- `toolbarTemplate` is the deprecated name for this slot; honouring it here\n keeps existing callers rendering exactly where they used to. -->\n @if (dataSource.toolbarRightTemplate ?? dataSource.toolbarTemplate; as rightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"rightTemplate\"></ng-container>\n </div>\n }\n </div>\n </div>\n}\n\n<!-- List wrapper -->\n<div\n #collectionBody\n [style.min-height.px]=\"bodyMinHeight\"\n class=\"w-full\"\n [class.border]=\"dataSource.appearance?.bordered\"\n [class.border-base-300]=\"dataSource.appearance?.bordered\"\n [class.rounded]=\"dataSource.appearance?.bordered\"\n role=\"list\"\n aria-label=\"
|
|
8968
|
+
args: [{ selector: 'mn-list', standalone: true, imports: [NgClass, NgTemplateOutlet, FormsModule, MnCheckbox, MnInputField, MnSkeleton, MnCollectionPagination, LucideDynamicIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n dataSource.canSearch ||\n dataSource.toolbarLeftTemplate ||\n dataSource.toolbarRightTemplate ||\n dataSource.toolbarTemplate\n) {\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <!-- Enter must not submit a surrounding form: the search box is the\n collection's own chrome, not a field of whatever form it sits in. -->\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-list-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n ></mn-lib-input-field>\n }\n <!-- `toolbarTemplate` is the deprecated name for this slot; honouring it here\n keeps existing callers rendering exactly where they used to. -->\n @if (dataSource.toolbarRightTemplate ?? dataSource.toolbarTemplate; as rightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"rightTemplate\"></ng-container>\n </div>\n }\n </div>\n </div>\n}\n\n<!-- List wrapper -->\n<div\n #collectionBody\n [style.min-height.px]=\"bodyMinHeight\"\n class=\"w-full\"\n [class.border]=\"dataSource.appearance?.bordered\"\n [class.border-base-300]=\"dataSource.appearance?.bordered\"\n [class.rounded]=\"dataSource.appearance?.bordered\"\n role=\"list\"\n [attr.aria-label]=\"listRegionLabel\"\n>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <div [class.py-2]=\"dataSource.appearance?.compact\" class=\"px-4 py-3\" role=\"listitem\">\n @if (isTemplateRef(dataSource.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(dataSource.skeleton)\"></ng-container>\n } @else {\n <div class=\"flex flex-col gap-1\">\n @for (line of skeletonLines; track $index) {\n <mn-skeleton [data]=\"line\"></mn-skeleton>\n }\n </div>\n }\n </div>\n @if (!$last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </div>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </div>\n }\n\n <!-- Select all (multi-select) -->\n @if (isMultiSelect && filteredItems.length > 0) {\n <div class=\"flex items-center gap-2 px-4 py-2 bg-base-200 text-sm\">\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-list-select-all', label: 'Select all', size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n @if (dataSource.appearance?.dividers !== false) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n\n <!-- Data items -->\n @for (item of paginatedItems; track trackByID($index, item); let odd = $odd; let last = $last) {\n <div\n class=\"flex items-center gap-2 bg-base-100 transition-colors duration-150\"\n [ngClass]=\"{'bg-primary/10': isSelected(item)}\"\n [class.bg-base-200]=\"!isSelected(item) && odd && dataSource.appearance?.dividers !== false\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n [class.px-4]=\"true\"\n [class.py-3]=\"!dataSource.appearance?.compact\"\n [class.py-2]=\"dataSource.appearance?.compact\"\n role=\"listitem\"\n (keyup.enter)=\"onItemClick(item)\"\n (click)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div (click)=\"$event.stopPropagation()\" class=\"shrink-0\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(item)\"\n [checked]=\"isSelected(item)\"\n [props]=\"{ id: 'mn-list-item-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n }\n\n <!-- Item content via template -->\n <div class=\"flex-1 min-w-0\">\n <ng-container\n [ngTemplateOutlet]=\"dataSource.itemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n ></ng-container>\n </div>\n </div>\n @if (!last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n }\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-list\"\n></mn-collection-pagination>\n" }]
|
|
8925
8969
|
}], propDecorators: { itemClick: [{
|
|
8926
8970
|
type: Output
|
|
8927
8971
|
}], collectionBody: [{
|
|
@@ -8935,6 +8979,50 @@ const DEFAULT_GRID_SKELETON_LINES = [
|
|
|
8935
8979
|
{ shape: 'text', width: '75%' },
|
|
8936
8980
|
{ shape: 'text', width: '50%', height: '0.75rem' },
|
|
8937
8981
|
];
|
|
8982
|
+
/** Breakpoints a `cols` map may address, ordered small → large. */
|
|
8983
|
+
const GRID_BREAKPOINTS = ['base', 'sm', 'md', 'lg', 'xl'];
|
|
8984
|
+
/** Highest column count with a pre-generated class; larger requests clamp to it. */
|
|
8985
|
+
const MAX_GRID_COLS = 12;
|
|
8986
|
+
/**
|
|
8987
|
+
* Column utilities per breakpoint, indexed by `columns - 1`.
|
|
8988
|
+
*
|
|
8989
|
+
* Spelled out as literals on purpose: the consuming app's Tailwind scanner reads
|
|
8990
|
+
* the shipped bundle, so a name assembled at runtime (`sm:grid-cols-${n}`) would
|
|
8991
|
+
* never be generated. Breakpoints are Tailwind's defaults (sm 640, md 768,
|
|
8992
|
+
* lg 1024, xl 1280), and each unset one simply inherits the next-smaller class.
|
|
8993
|
+
*/
|
|
8994
|
+
const GRID_COL_CLASSES = {
|
|
8995
|
+
base: [
|
|
8996
|
+
'grid-cols-1', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4', 'grid-cols-5', 'grid-cols-6',
|
|
8997
|
+
'grid-cols-7', 'grid-cols-8', 'grid-cols-9', 'grid-cols-10', 'grid-cols-11', 'grid-cols-12',
|
|
8998
|
+
],
|
|
8999
|
+
sm: [
|
|
9000
|
+
'sm:grid-cols-1', 'sm:grid-cols-2', 'sm:grid-cols-3', 'sm:grid-cols-4', 'sm:grid-cols-5', 'sm:grid-cols-6',
|
|
9001
|
+
'sm:grid-cols-7', 'sm:grid-cols-8', 'sm:grid-cols-9', 'sm:grid-cols-10', 'sm:grid-cols-11', 'sm:grid-cols-12',
|
|
9002
|
+
],
|
|
9003
|
+
md: [
|
|
9004
|
+
'md:grid-cols-1', 'md:grid-cols-2', 'md:grid-cols-3', 'md:grid-cols-4', 'md:grid-cols-5', 'md:grid-cols-6',
|
|
9005
|
+
'md:grid-cols-7', 'md:grid-cols-8', 'md:grid-cols-9', 'md:grid-cols-10', 'md:grid-cols-11', 'md:grid-cols-12',
|
|
9006
|
+
],
|
|
9007
|
+
lg: [
|
|
9008
|
+
'lg:grid-cols-1', 'lg:grid-cols-2', 'lg:grid-cols-3', 'lg:grid-cols-4', 'lg:grid-cols-5', 'lg:grid-cols-6',
|
|
9009
|
+
'lg:grid-cols-7', 'lg:grid-cols-8', 'lg:grid-cols-9', 'lg:grid-cols-10', 'lg:grid-cols-11', 'lg:grid-cols-12',
|
|
9010
|
+
],
|
|
9011
|
+
xl: [
|
|
9012
|
+
'xl:grid-cols-1', 'xl:grid-cols-2', 'xl:grid-cols-3', 'xl:grid-cols-4', 'xl:grid-cols-5', 'xl:grid-cols-6',
|
|
9013
|
+
'xl:grid-cols-7', 'xl:grid-cols-8', 'xl:grid-cols-9', 'xl:grid-cols-10', 'xl:grid-cols-11', 'xl:grid-cols-12',
|
|
9014
|
+
],
|
|
9015
|
+
};
|
|
9016
|
+
/**
|
|
9017
|
+
* Picks the column utility for a breakpoint, clamped to the range that has one.
|
|
9018
|
+
* @param breakpoint Breakpoint the class applies from.
|
|
9019
|
+
* @param columns Requested column count.
|
|
9020
|
+
* @returns The Tailwind class name.
|
|
9021
|
+
*/
|
|
9022
|
+
function gridColClass(breakpoint, columns) {
|
|
9023
|
+
const index = Math.min(Math.max(Math.round(columns), 1), MAX_GRID_COLS) - 1;
|
|
9024
|
+
return GRID_COL_CLASSES[breakpoint][index];
|
|
9025
|
+
}
|
|
8938
9026
|
/**
|
|
8939
9027
|
* Responsive card-grid component. Shares the collection chrome (search, every
|
|
8940
9028
|
* pagination mode, loading skeleton, empty state, toolbar, i18n) with
|
|
@@ -8950,6 +9038,40 @@ class MnGrid extends MnCollectionBase {
|
|
|
8950
9038
|
return !!this.dataSource.layout?.minCardWidth;
|
|
8951
9039
|
}
|
|
8952
9040
|
// ── Layout ──
|
|
9041
|
+
/**
|
|
9042
|
+
* Classes for the card container: `grid` plus one column utility per
|
|
9043
|
+
* breakpoint the consumer configured. Omitted for the auto-fit layout, whose
|
|
9044
|
+
* columns come from {@link autoTemplateColumns} instead.
|
|
9045
|
+
*/
|
|
9046
|
+
get gridClasses() {
|
|
9047
|
+
if (this.isAutoLayout) {
|
|
9048
|
+
return 'grid';
|
|
9049
|
+
}
|
|
9050
|
+
const cols = this.dataSource.layout?.cols;
|
|
9051
|
+
const classes = ['grid', gridColClass('base', cols?.base ?? 1)];
|
|
9052
|
+
for (const breakpoint of GRID_BREAKPOINTS) {
|
|
9053
|
+
if (breakpoint === 'base')
|
|
9054
|
+
continue;
|
|
9055
|
+
const columns = cols?.[breakpoint];
|
|
9056
|
+
if (columns != null) {
|
|
9057
|
+
classes.push(gridColClass(breakpoint, columns));
|
|
9058
|
+
}
|
|
9059
|
+
}
|
|
9060
|
+
return classes.join(' ');
|
|
9061
|
+
}
|
|
9062
|
+
/** Gap between cards. */
|
|
9063
|
+
get gridGap() {
|
|
9064
|
+
return this.dataSource.layout?.gap ?? '1rem';
|
|
9065
|
+
}
|
|
9066
|
+
/**
|
|
9067
|
+
* Inline `grid-template-columns` for the auto-fit layout, or null when explicit
|
|
9068
|
+
* `cols` are used (the utilities in {@link gridClasses} then own the columns).
|
|
9069
|
+
* `minCardWidth` is a free-form CSS length, so it can only be expressed inline.
|
|
9070
|
+
*/
|
|
9071
|
+
get autoTemplateColumns() {
|
|
9072
|
+
const minCardWidth = this.dataSource.layout?.minCardWidth;
|
|
9073
|
+
return minCardWidth ? `repeat(auto-fit, minmax(${minCardWidth}, 1fr))` : null;
|
|
9074
|
+
}
|
|
8953
9075
|
/** Skeleton lines for the default/lines placeholder; null when a custom template is used. */
|
|
8954
9076
|
get skeletonLines() {
|
|
8955
9077
|
const skeleton = this.dataSource.skeleton;
|
|
@@ -8989,12 +9111,20 @@ class MnGrid extends MnCollectionBase {
|
|
|
8989
9111
|
this.loadMoreRows();
|
|
8990
9112
|
}
|
|
8991
9113
|
}
|
|
9114
|
+
/** Accessible name for the scrollable grid region. */
|
|
9115
|
+
get gridRegionLabel() {
|
|
9116
|
+
return this.resolveLabel(undefined, 'mnCollection.cardGrid', 'Card grid');
|
|
9117
|
+
}
|
|
9118
|
+
/** Accessible name for the loading placeholder. */
|
|
9119
|
+
get loadingLabel() {
|
|
9120
|
+
return this.resolveLabel(undefined, 'mnCollection.loading', 'Loading');
|
|
9121
|
+
}
|
|
8992
9122
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnGrid, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
8993
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnGrid, isStandalone: true, selector: "mn-grid", outputs: { itemClick: "itemClick" }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n dataSource.canSearch ||\n dataSource.toolbarLeftTemplate ||\n dataSource.toolbarRightTemplate ||\n dataSource.toolbarTemplate\n) {\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-grid-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n ></mn-lib-input-field>\n }\n <!-- `toolbarTemplate` is the deprecated name for this slot; honouring it here\n keeps existing callers rendering exactly where they used to. -->\n @if (dataSource.toolbarRightTemplate ?? dataSource.toolbarTemplate; as rightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"rightTemplate\"></ng-container>\n </div>\n }\n </div>\n </div>\n}\n\n<!-- Body: skeleton/data swap region. Wrapper holds its height during a server reload. -->\n<div #collectionBody [style.min-height.px]=\"bodyMinHeight\">\n<!-- Loading state -->\n @if (isLoadingState) {\n <div\n [class
|
|
9123
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnGrid, isStandalone: true, selector: "mn-grid", outputs: { itemClick: "itemClick" }, host: { classAttribute: "block" }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n dataSource.canSearch ||\n dataSource.toolbarLeftTemplate ||\n dataSource.toolbarRightTemplate ||\n dataSource.toolbarTemplate\n) {\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <!-- Enter must not submit a surrounding form: the search box is the\n collection's own chrome, not a field of whatever form it sits in. -->\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-grid-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n ></mn-lib-input-field>\n }\n <!-- `toolbarTemplate` is the deprecated name for this slot; honouring it here\n keeps existing callers rendering exactly where they used to. -->\n @if (dataSource.toolbarRightTemplate ?? dataSource.toolbarTemplate; as rightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"rightTemplate\"></ng-container>\n </div>\n }\n </div>\n </div>\n}\n\n<!-- Body: skeleton/data swap region. Wrapper holds its height during a server reload. -->\n<div #collectionBody [style.min-height.px]=\"bodyMinHeight\">\n<!-- Loading state -->\n @if (isLoadingState) {\n <div\n [class]=\"gridClasses\"\n [style.gap]=\"gridGap\"\n [style.grid-template-columns]=\"autoTemplateColumns\"\n aria-busy=\"true\"\n [attr.aria-label]=\"loadingLabel\"\n role=\"list\"\n >\n @for (_ of skeletonRows; track $index) {\n <div role=\"listitem\">\n @if (isTemplateRef(dataSource.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(dataSource.skeleton)\"></ng-container>\n } @else {\n <div class=\"flex flex-col gap-2 p-4 border border-base-300 rounded-lg\">\n @for (line of skeletonLines; track $index) {\n <mn-skeleton [data]=\"line\"></mn-skeleton>\n }\n </div>\n }\n </div>\n }\n </div>\n } @else if (isErrorState) {\n <!-- Error state -->\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div\n class=\"flex items-center justify-center min-h-[6rem] py-8 rounded-xl border border-dashed border-error/30 bg-base-100\">\n <p class=\"text-sm text-error\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n} @else {\n <!-- Empty state: a caller-provided template/component (rendered unwrapped, full\n control over its own layout) or, when none is given, the default text. -->\n @if (filteredItems.length === 0) {\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div\n class=\"flex flex-col items-center justify-center gap-2 min-h-[6rem] py-8 rounded-xl border border-dashed border-base-content/15 bg-base-100 text-base-content/40\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n } @else {\n <!-- Card grid -->\n <div\n [class]=\"gridClasses\"\n [style.gap]=\"gridGap\"\n [style.grid-template-columns]=\"autoTemplateColumns\"\n [attr.aria-label]=\"gridRegionLabel\"\n role=\"list\"\n >\n @for (item of paginatedItems; track trackByID($index, item)) {\n <div\n (click)=\"onItemClick(item)\"\n (keyup.enter)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n class=\"transition-colors duration-150\"\n role=\"listitem\"\n >\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n [ngTemplateOutlet]=\"dataSource.cardTemplate\"\n ></ng-container>\n </div>\n }\n </div>\n }\n}\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-grid\"\n></mn-collection-pagination>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnCollectionPagination, selector: "mn-collection-pagination", inputs: ["idPrefix", "isPaginated", "isServerPaginated", "showLoadMore", "loadingMoreRows", "currentPage", "pageSize", "totalPages", "totalItemCount", "visiblePages", "pageSizeSelectOptions", "labels"], outputs: ["loadMore", "pageChange", "pageSizeChange"] }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
8994
9124
|
}
|
|
8995
9125
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnGrid, decorators: [{
|
|
8996
9126
|
type: Component,
|
|
8997
|
-
args: [{ selector: 'mn-grid', standalone: true, imports: [NgTemplateOutlet, FormsModule, MnSkeleton, MnInputField, MnCollectionPagination, LucideDynamicIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n dataSource.canSearch ||\n dataSource.toolbarLeftTemplate ||\n dataSource.toolbarRightTemplate ||\n dataSource.toolbarTemplate\n) {\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-grid-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n ></mn-lib-input-field>\n }\n <!-- `toolbarTemplate` is the deprecated name for this slot; honouring it here\n keeps existing callers rendering exactly where they used to. -->\n @if (dataSource.toolbarRightTemplate ?? dataSource.toolbarTemplate; as rightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"rightTemplate\"></ng-container>\n </div>\n }\n </div>\n </div>\n}\n\n<!-- Body: skeleton/data swap region. Wrapper holds its height during a server reload. -->\n<div #collectionBody [style.min-height.px]=\"bodyMinHeight\">\n<!-- Loading state -->\n @if (isLoadingState) {\n <div\n [class
|
|
9127
|
+
args: [{ selector: 'mn-grid', standalone: true, imports: [NgTemplateOutlet, FormsModule, MnSkeleton, MnInputField, MnCollectionPagination, LucideDynamicIcon], host: { class: 'block' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n dataSource.canSearch ||\n dataSource.toolbarLeftTemplate ||\n dataSource.toolbarRightTemplate ||\n dataSource.toolbarTemplate\n) {\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (dataSource.canSearch) {\n <!-- Enter must not submit a surrounding form: the search box is the\n collection's own chrome, not a field of whatever form it sits in. -->\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-grid-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n ></mn-lib-input-field>\n }\n <!-- `toolbarTemplate` is the deprecated name for this slot; honouring it here\n keeps existing callers rendering exactly where they used to. -->\n @if (dataSource.toolbarRightTemplate ?? dataSource.toolbarTemplate; as rightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"rightTemplate\"></ng-container>\n </div>\n }\n </div>\n </div>\n}\n\n<!-- Body: skeleton/data swap region. Wrapper holds its height during a server reload. -->\n<div #collectionBody [style.min-height.px]=\"bodyMinHeight\">\n<!-- Loading state -->\n @if (isLoadingState) {\n <div\n [class]=\"gridClasses\"\n [style.gap]=\"gridGap\"\n [style.grid-template-columns]=\"autoTemplateColumns\"\n aria-busy=\"true\"\n [attr.aria-label]=\"loadingLabel\"\n role=\"list\"\n >\n @for (_ of skeletonRows; track $index) {\n <div role=\"listitem\">\n @if (isTemplateRef(dataSource.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(dataSource.skeleton)\"></ng-container>\n } @else {\n <div class=\"flex flex-col gap-2 p-4 border border-base-300 rounded-lg\">\n @for (line of skeletonLines; track $index) {\n <mn-skeleton [data]=\"line\"></mn-skeleton>\n }\n </div>\n }\n </div>\n }\n </div>\n } @else if (isErrorState) {\n <!-- Error state -->\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div\n class=\"flex items-center justify-center min-h-[6rem] py-8 rounded-xl border border-dashed border-error/30 bg-base-100\">\n <p class=\"text-sm text-error\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n} @else {\n <!-- Empty state: a caller-provided template/component (rendered unwrapped, full\n control over its own layout) or, when none is given, the default text. -->\n @if (filteredItems.length === 0) {\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div\n class=\"flex flex-col items-center justify-center gap-2 min-h-[6rem] py-8 rounded-xl border border-dashed border-base-content/15 bg-base-100 text-base-content/40\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n } @else {\n <!-- Card grid -->\n <div\n [class]=\"gridClasses\"\n [style.gap]=\"gridGap\"\n [style.grid-template-columns]=\"autoTemplateColumns\"\n [attr.aria-label]=\"gridRegionLabel\"\n role=\"list\"\n >\n @for (item of paginatedItems; track trackByID($index, item)) {\n <div\n (click)=\"onItemClick(item)\"\n (keyup.enter)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n class=\"transition-colors duration-150\"\n role=\"listitem\"\n >\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n [ngTemplateOutlet]=\"dataSource.cardTemplate\"\n ></ng-container>\n </div>\n }\n </div>\n }\n}\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-grid\"\n></mn-collection-pagination>\n" }]
|
|
8998
9128
|
}], propDecorators: { itemClick: [{
|
|
8999
9129
|
type: Output
|
|
9000
9130
|
}], collectionBody: [{
|
|
@@ -9195,6 +9325,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
9195
9325
|
* events on that day. Clicking a cell emits `dayClicked`.
|
|
9196
9326
|
*/
|
|
9197
9327
|
class CalendarMonthComponent {
|
|
9328
|
+
lang = inject(MnLanguageService);
|
|
9329
|
+
/**
|
|
9330
|
+
* Accessible name for this control. Resolved through the conventional
|
|
9331
|
+
* `mnCalendar.monthView` key so an app can translate it, falling back to English when the
|
|
9332
|
+
* key is not defined rather than leaking the raw key into the UI.
|
|
9333
|
+
*/
|
|
9334
|
+
get monthViewLabel() {
|
|
9335
|
+
return this.lang.translateIfPresent('mnCalendar.monthView') ?? 'Month view';
|
|
9336
|
+
}
|
|
9198
9337
|
/** The date whose month is displayed. */
|
|
9199
9338
|
focusDay;
|
|
9200
9339
|
/** Observable that emits the full event list whenever it changes. */
|
|
@@ -9296,11 +9435,11 @@ class CalendarMonthComponent {
|
|
|
9296
9435
|
};
|
|
9297
9436
|
}
|
|
9298
9437
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarMonthComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
9299
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: CalendarMonthComponent, isStandalone: true, selector: "mn-calendar-month", inputs: { focusDay: "focusDay", eventsChanged: "eventsChanged", focusDayChanged: "focusDayChanged", config: "config" }, outputs: { dayClicked: "dayClicked" }, ngImport: i0, template: "<!-- Month grid. Cells are grouped by space rather than boxed by borders, in\n keeping with the date-selector-bar: no rules, soft rounded cells, a hover\n wash, and today marked by a tinted number alone (no circle). -->\n<!-- Rows are a compact fixed height rather than stretched to fill the container:\n a month with few events shouldn't leave each cell mostly empty. The panel\n scrolls if the grid is taller than the space it's given. -->\n<div class=\"w-full flex flex-col\" role=\"grid\"
|
|
9438
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: CalendarMonthComponent, isStandalone: true, selector: "mn-calendar-month", inputs: { focusDay: "focusDay", eventsChanged: "eventsChanged", focusDayChanged: "focusDayChanged", config: "config" }, outputs: { dayClicked: "dayClicked" }, ngImport: i0, template: "<!-- Month grid. Cells are grouped by space rather than boxed by borders, in\n keeping with the date-selector-bar: no rules, soft rounded cells, a hover\n wash, and today marked by a tinted number alone (no circle). -->\n<!-- Rows are a compact fixed height rather than stretched to fill the container:\n a month with few events shouldn't leave each cell mostly empty. The panel\n scrolls if the grid is taller than the space it's given. -->\n<div [attr.aria-label]=\"monthViewLabel\" class=\"w-full flex flex-col\" role=\"grid\">\n <div class=\"grid grid-cols-7 pb-2\">\n @for (day of weekdayLabels; track day) {\n <div class=\"pl-2 text-left text-[11px] font-semibold uppercase tracking-wide opacity-45\" role=\"columnheader\">{{ day }}</div>\n }\n </div>\n <div class=\"grid grid-cols-7 gap-1 auto-rows-[minmax(92px,auto)]\">\n @for (item of monthItems; track item.date.getTime()) {\n <div\n class=\"flex flex-col gap-1 min-h-0 overflow-hidden rounded-xl p-1.5 cursor-pointer transition-colors hover:bg-base-200 focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-primary motion-reduce:transition-none\"\n [class.opacity-40]=\"!item.isCurrentMonth\"\n (keyup.enter)=\"onDayClick(item.date)\"\n (click)=\"onDayClick(item.date)\"\n tabindex=\"0\"\n role=\"gridcell\"\n [attr.aria-label]=\"item.date.toDateString()\">\n <span class=\"text-[13px] font-bold leading-none tabular-nums\" [class.text-primary]=\"item.isToday\">{{ item.dayNumber }}</span>\n <div class=\"flex flex-col gap-0.5 min-h-0\">\n @for (event of item.events.slice(0, 3); track $index) {\n <div\n class=\"flex items-center overflow-hidden rounded-md border-l-[3px] px-1.5 py-0.5 text-[11px] leading-tight\"\n [style.background-color]=\"event.color.secondaryColor\"\n [style.border-left-color]=\"event.color.primaryColor\"\n [style.color]=\"event.color.primaryColor\"\n [title]=\"event.title\">\n <span class=\"truncate font-semibold\">{{ event.title }}</span>\n </div>\n }\n <!-- Overflow indicator. Styled as a pill that lights up on hover so it\n reads as actionable; the whole cell already navigates to the Day\n view for this date on click / Enter, where every event is listed. -->\n @if (item.events.length > 3) {\n <span class=\"mt-0.5 inline-flex w-fit items-center rounded-md px-1.5 py-0.5 text-[10.5px] font-semibold opacity-60 transition-colors hover:bg-base-300 hover:opacity-100 motion-reduce:transition-none\">\n +{{ item.events.length - 3 }} {{ moreEventsLabel }}\n </span>\n }\n </div>\n </div>\n }\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }] });
|
|
9300
9439
|
}
|
|
9301
9440
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarMonthComponent, decorators: [{
|
|
9302
9441
|
type: Component,
|
|
9303
|
-
args: [{ selector: 'mn-calendar-month', standalone: true, imports: [CommonModule], template: "<!-- Month grid. Cells are grouped by space rather than boxed by borders, in\n keeping with the date-selector-bar: no rules, soft rounded cells, a hover\n wash, and today marked by a tinted number alone (no circle). -->\n<!-- Rows are a compact fixed height rather than stretched to fill the container:\n a month with few events shouldn't leave each cell mostly empty. The panel\n scrolls if the grid is taller than the space it's given. -->\n<div class=\"w-full flex flex-col\" role=\"grid\"
|
|
9442
|
+
args: [{ selector: 'mn-calendar-month', standalone: true, imports: [CommonModule], template: "<!-- Month grid. Cells are grouped by space rather than boxed by borders, in\n keeping with the date-selector-bar: no rules, soft rounded cells, a hover\n wash, and today marked by a tinted number alone (no circle). -->\n<!-- Rows are a compact fixed height rather than stretched to fill the container:\n a month with few events shouldn't leave each cell mostly empty. The panel\n scrolls if the grid is taller than the space it's given. -->\n<div [attr.aria-label]=\"monthViewLabel\" class=\"w-full flex flex-col\" role=\"grid\">\n <div class=\"grid grid-cols-7 pb-2\">\n @for (day of weekdayLabels; track day) {\n <div class=\"pl-2 text-left text-[11px] font-semibold uppercase tracking-wide opacity-45\" role=\"columnheader\">{{ day }}</div>\n }\n </div>\n <div class=\"grid grid-cols-7 gap-1 auto-rows-[minmax(92px,auto)]\">\n @for (item of monthItems; track item.date.getTime()) {\n <div\n class=\"flex flex-col gap-1 min-h-0 overflow-hidden rounded-xl p-1.5 cursor-pointer transition-colors hover:bg-base-200 focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-primary motion-reduce:transition-none\"\n [class.opacity-40]=\"!item.isCurrentMonth\"\n (keyup.enter)=\"onDayClick(item.date)\"\n (click)=\"onDayClick(item.date)\"\n tabindex=\"0\"\n role=\"gridcell\"\n [attr.aria-label]=\"item.date.toDateString()\">\n <span class=\"text-[13px] font-bold leading-none tabular-nums\" [class.text-primary]=\"item.isToday\">{{ item.dayNumber }}</span>\n <div class=\"flex flex-col gap-0.5 min-h-0\">\n @for (event of item.events.slice(0, 3); track $index) {\n <div\n class=\"flex items-center overflow-hidden rounded-md border-l-[3px] px-1.5 py-0.5 text-[11px] leading-tight\"\n [style.background-color]=\"event.color.secondaryColor\"\n [style.border-left-color]=\"event.color.primaryColor\"\n [style.color]=\"event.color.primaryColor\"\n [title]=\"event.title\">\n <span class=\"truncate font-semibold\">{{ event.title }}</span>\n </div>\n }\n <!-- Overflow indicator. Styled as a pill that lights up on hover so it\n reads as actionable; the whole cell already navigates to the Day\n view for this date on click / Enter, where every event is listed. -->\n @if (item.events.length > 3) {\n <span class=\"mt-0.5 inline-flex w-fit items-center rounded-md px-1.5 py-0.5 text-[10.5px] font-semibold opacity-60 transition-colors hover:bg-base-300 hover:opacity-100 motion-reduce:transition-none\">\n +{{ item.events.length - 3 }} {{ moreEventsLabel }}\n </span>\n }\n </div>\n </div>\n }\n </div>\n</div>\n" }]
|
|
9304
9443
|
}], ctorParameters: () => [], propDecorators: { focusDay: [{
|
|
9305
9444
|
type: Input
|
|
9306
9445
|
}], eventsChanged: [{
|
|
@@ -9590,6 +9729,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
9590
9729
|
* so they appear side-by-side rather than stacked.
|
|
9591
9730
|
*/
|
|
9592
9731
|
class CalendarWeekComponent {
|
|
9732
|
+
lang = inject(MnLanguageService);
|
|
9733
|
+
/**
|
|
9734
|
+
* Accessible name for this control. Resolved through the conventional
|
|
9735
|
+
* `mnCalendar.weekView` key so an app can translate it, falling back to English when the
|
|
9736
|
+
* key is not defined rather than leaking the raw key into the UI.
|
|
9737
|
+
*/
|
|
9738
|
+
get weekViewLabel() {
|
|
9739
|
+
return this.lang.translateIfPresent('mnCalendar.weekView') ?? 'Week view';
|
|
9740
|
+
}
|
|
9593
9741
|
layoutService = inject(CalendarEventLayoutService);
|
|
9594
9742
|
cdr = inject(ChangeDetectorRef);
|
|
9595
9743
|
/** The date around which the week is centred. */
|
|
@@ -9793,11 +9941,11 @@ class CalendarWeekComponent {
|
|
|
9793
9941
|
}
|
|
9794
9942
|
}
|
|
9795
9943
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarWeekComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
9796
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: CalendarWeekComponent, isStandalone: true, selector: "mn-calendar-week", inputs: { focusDay: "focusDay", eventsChanged: "eventsChanged", focusDayChanged: "focusDayChanged", config: "config", calendarEventComponent: "calendarEventComponent" }, outputs: { eventClicked: "eventClicked" }, providers: [CalendarEventLayoutService], ngImport: i0, template: "<!-- Week grid. Hour rules are quiet hairlines; today's column header is a tinted\n number (no fill); the current time is a line carrying a small time bubble.\n Dynamic row/column placement stays inline \u2014 the layout maths lives in TS. -->\n<div class=\"w-full h-full flex flex-col overflow-hidden\" role=\"grid\"
|
|
9944
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: CalendarWeekComponent, isStandalone: true, selector: "mn-calendar-week", inputs: { focusDay: "focusDay", eventsChanged: "eventsChanged", focusDayChanged: "focusDayChanged", config: "config", calendarEventComponent: "calendarEventComponent" }, outputs: { eventClicked: "eventClicked" }, providers: [CalendarEventLayoutService], ngImport: i0, template: "<!-- Week grid. Hour rules are quiet hairlines; today's column header is a tinted\n number (no fill); the current time is a line carrying a small time bubble.\n Dynamic row/column placement stays inline \u2014 the layout maths lives in TS. -->\n<div [attr.aria-label]=\"weekViewLabel\" class=\"w-full h-full flex flex-col overflow-hidden\" role=\"grid\">\n <div class=\"grid\" [style.grid-template-columns]=\"'60px ' + gridTemplateColumns\">\n <div></div>\n @for (col of columns; track col.dayName; let i = $index) {\n <div class=\"py-2 px-1 text-center\"\n [style.grid-column]=\"getHeaderColumn(i)\"\n role=\"columnheader\">\n <span class=\"block text-[11px] font-semibold uppercase tracking-wide opacity-50\">{{ col.dayName }}</span>\n <span class=\"text-lg font-bold tabular-nums\" [class.text-primary]=\"col.isToday\">{{ col.dayNumber }}</span>\n </div>\n }\n </div>\n <div class=\"grid grid-cols-[60px_1fr] flex-1 min-h-0 overflow-hidden items-stretch\">\n <div class=\"grid h-full min-h-0\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', minmax(1.5rem, 1fr))'\">\n @for (row of hourRows; track row.topRow) {\n <div class=\"flex items-start justify-end min-h-0 overflow-hidden pr-2 text-[11px] tabular-nums opacity-50\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n }\n </div>\n <div class=\"grid relative auto-rows-fr h-full min-h-0\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', minmax(1.5rem, 1fr))'\"\n [style.grid-template-columns]=\"gridTemplateColumns\">\n @for (row of hourRows; track row.topRow) {\n <div class=\"border-t border-base-200 pointer-events-none min-h-0\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n }\n @if (currentTimeRow > 0 && currentTimeCol) {\n <div class=\"relative z-[2] pointer-events-none\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"currentTimeCol\">\n <div class=\"absolute -left-1 -top-1 h-2 w-2 rounded-full bg-error\"></div>\n <div class=\"h-0.5 w-full bg-error\"></div>\n <div class=\"absolute left-0 top-0 -translate-x-2 -translate-y-1/2 rounded-full bg-error px-1.5 py-0.5 text-[10px] font-bold tabular-nums text-error-content shadow\">{{ currentTimeLabel }}</div>\n </div>\n }\n @for (event of displayEvents; track $index) {\n <div class=\"z-[1] min-h-0 overflow-hidden rounded-lg p-0.5 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\"\n (keyup.enter)=\"onEventClick(event)\"\n tabindex=\"0\">\n <mn-calendar-event [customComponent]=\"calendarEventComponent\" [event]=\"event\"></mn-calendar-event>\n </div>\n }\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: CalendarEventComponent, selector: "mn-calendar-event", inputs: ["event", "customComponent"], outputs: ["eventClicked"] }] });
|
|
9797
9945
|
}
|
|
9798
9946
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarWeekComponent, decorators: [{
|
|
9799
9947
|
type: Component,
|
|
9800
|
-
args: [{ selector: 'mn-calendar-week', standalone: true, imports: [CommonModule, CalendarEventComponent], providers: [CalendarEventLayoutService], template: "<!-- Week grid. Hour rules are quiet hairlines; today's column header is a tinted\n number (no fill); the current time is a line carrying a small time bubble.\n Dynamic row/column placement stays inline \u2014 the layout maths lives in TS. -->\n<div class=\"w-full h-full flex flex-col overflow-hidden\" role=\"grid\"
|
|
9948
|
+
args: [{ selector: 'mn-calendar-week', standalone: true, imports: [CommonModule, CalendarEventComponent], providers: [CalendarEventLayoutService], template: "<!-- Week grid. Hour rules are quiet hairlines; today's column header is a tinted\n number (no fill); the current time is a line carrying a small time bubble.\n Dynamic row/column placement stays inline \u2014 the layout maths lives in TS. -->\n<div [attr.aria-label]=\"weekViewLabel\" class=\"w-full h-full flex flex-col overflow-hidden\" role=\"grid\">\n <div class=\"grid\" [style.grid-template-columns]=\"'60px ' + gridTemplateColumns\">\n <div></div>\n @for (col of columns; track col.dayName; let i = $index) {\n <div class=\"py-2 px-1 text-center\"\n [style.grid-column]=\"getHeaderColumn(i)\"\n role=\"columnheader\">\n <span class=\"block text-[11px] font-semibold uppercase tracking-wide opacity-50\">{{ col.dayName }}</span>\n <span class=\"text-lg font-bold tabular-nums\" [class.text-primary]=\"col.isToday\">{{ col.dayNumber }}</span>\n </div>\n }\n </div>\n <div class=\"grid grid-cols-[60px_1fr] flex-1 min-h-0 overflow-hidden items-stretch\">\n <div class=\"grid h-full min-h-0\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', minmax(1.5rem, 1fr))'\">\n @for (row of hourRows; track row.topRow) {\n <div class=\"flex items-start justify-end min-h-0 overflow-hidden pr-2 text-[11px] tabular-nums opacity-50\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n }\n </div>\n <div class=\"grid relative auto-rows-fr h-full min-h-0\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', minmax(1.5rem, 1fr))'\"\n [style.grid-template-columns]=\"gridTemplateColumns\">\n @for (row of hourRows; track row.topRow) {\n <div class=\"border-t border-base-200 pointer-events-none min-h-0\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n }\n @if (currentTimeRow > 0 && currentTimeCol) {\n <div class=\"relative z-[2] pointer-events-none\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"currentTimeCol\">\n <div class=\"absolute -left-1 -top-1 h-2 w-2 rounded-full bg-error\"></div>\n <div class=\"h-0.5 w-full bg-error\"></div>\n <div class=\"absolute left-0 top-0 -translate-x-2 -translate-y-1/2 rounded-full bg-error px-1.5 py-0.5 text-[10px] font-bold tabular-nums text-error-content shadow\">{{ currentTimeLabel }}</div>\n </div>\n }\n @for (event of displayEvents; track $index) {\n <div class=\"z-[1] min-h-0 overflow-hidden rounded-lg p-0.5 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\"\n (keyup.enter)=\"onEventClick(event)\"\n tabindex=\"0\">\n <mn-calendar-event [customComponent]=\"calendarEventComponent\" [event]=\"event\"></mn-calendar-event>\n </div>\n }\n </div>\n </div>\n</div>\n" }]
|
|
9801
9949
|
}], ctorParameters: () => [], propDecorators: { focusDay: [{
|
|
9802
9950
|
type: Input
|
|
9803
9951
|
}], eventsChanged: [{
|
|
@@ -9819,6 +9967,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
9819
9967
|
* {@link CalendarEventLayoutService}.
|
|
9820
9968
|
*/
|
|
9821
9969
|
class CalendarDayComponent {
|
|
9970
|
+
lang = inject(MnLanguageService);
|
|
9971
|
+
/**
|
|
9972
|
+
* Accessible name for this control. Resolved through the conventional
|
|
9973
|
+
* `mnCalendar.dayView` key so an app can translate it, falling back to English when the
|
|
9974
|
+
* key is not defined rather than leaking the raw key into the UI.
|
|
9975
|
+
*/
|
|
9976
|
+
get dayViewLabel() {
|
|
9977
|
+
return this.lang.translateIfPresent('mnCalendar.dayView') ?? 'Day view';
|
|
9978
|
+
}
|
|
9822
9979
|
layoutService = inject(CalendarEventLayoutService);
|
|
9823
9980
|
cdr = inject(ChangeDetectorRef);
|
|
9824
9981
|
/** The date to display. */
|
|
@@ -9964,11 +10121,11 @@ class CalendarDayComponent {
|
|
|
9964
10121
|
}
|
|
9965
10122
|
}
|
|
9966
10123
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarDayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
9967
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: CalendarDayComponent, isStandalone: true, selector: "mn-calendar-day", inputs: { focusDay: "focusDay", eventsChanged: "eventsChanged", focusDayChanged: "focusDayChanged", config: "config", calendarEventComponent: "calendarEventComponent" }, outputs: { eventClicked: "eventClicked" }, providers: [CalendarEventLayoutService], ngImport: i0, template: "<!-- Day grid. One column, its header centered over that column (the toolbar\n already names the date). Today's number is tinted, not filled; the now-line\n carries a time bubble that overhangs into the gutter. -->\n<div class=\"w-full h-full flex flex-col overflow-hidden\" role=\"grid\"
|
|
10124
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: CalendarDayComponent, isStandalone: true, selector: "mn-calendar-day", inputs: { focusDay: "focusDay", eventsChanged: "eventsChanged", focusDayChanged: "focusDayChanged", config: "config", calendarEventComponent: "calendarEventComponent" }, outputs: { eventClicked: "eventClicked" }, providers: [CalendarEventLayoutService], ngImport: i0, template: "<!-- Day grid. One column, its header centered over that column (the toolbar\n already names the date). Today's number is tinted, not filled; the now-line\n carries a time bubble that overhangs into the gutter. -->\n<div [attr.aria-label]=\"dayViewLabel\" class=\"w-full h-full flex flex-col overflow-hidden\" role=\"grid\">\n <div class=\"grid grid-cols-[60px_1fr]\">\n <div></div>\n <div class=\"flex items-center justify-center gap-2 py-2 px-1\" role=\"columnheader\">\n <span class=\"text-[11px] font-semibold uppercase tracking-wide opacity-50\">{{ dayName }}</span>\n <span class=\"text-base font-bold tabular-nums\" [class.text-primary]=\"isToday\">{{ focusDay.getDate() }}</span>\n </div>\n </div>\n <div class=\"grid grid-cols-[60px_1fr] flex-1 min-h-0 overflow-hidden items-stretch\">\n <div class=\"grid h-full min-h-0\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', minmax(1.5rem, 1fr))'\">\n @for (row of hourRows; track row.topRow) {\n <div class=\"flex items-start justify-end min-h-0 overflow-hidden pr-2 text-[11px] tabular-nums opacity-50\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n }\n </div>\n <div class=\"grid relative auto-rows-fr h-full min-h-0\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', minmax(1.5rem, 1fr))'\"\n [style.grid-template-columns]=\"'repeat(' + totalColumns + ', 1fr)'\">\n @for (row of hourRows; track row.topRow) {\n <div class=\"border-t border-base-200 pointer-events-none min-h-0\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n }\n @if (currentTimeRow > 0 && isToday) {\n <div class=\"relative z-[2] pointer-events-none\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"'1 / -1'\">\n <div class=\"absolute -left-1 -top-1 h-2 w-2 rounded-full bg-error\"></div>\n <div class=\"h-0.5 w-full bg-error\"></div>\n <div class=\"absolute left-0 top-0 -translate-x-2 -translate-y-1/2 rounded-full bg-error px-1.5 py-0.5 text-[10px] font-bold tabular-nums text-error-content shadow\">{{ currentTimeLabel }}</div>\n </div>\n }\n @for (event of displayEvents; track $index) {\n <div class=\"z-[1] min-h-0 overflow-hidden rounded-lg p-0.5 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\"\n (keyup.enter)=\"onEventClick(event)\"\n tabindex=\"0\">\n <mn-calendar-event [customComponent]=\"calendarEventComponent\" [event]=\"event\"></mn-calendar-event>\n </div>\n }\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: CalendarEventComponent, selector: "mn-calendar-event", inputs: ["event", "customComponent"], outputs: ["eventClicked"] }] });
|
|
9968
10125
|
}
|
|
9969
10126
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarDayComponent, decorators: [{
|
|
9970
10127
|
type: Component,
|
|
9971
|
-
args: [{ selector: 'mn-calendar-day', standalone: true, imports: [CommonModule, CalendarEventComponent], providers: [CalendarEventLayoutService], template: "<!-- Day grid. One column, its header centered over that column (the toolbar\n already names the date). Today's number is tinted, not filled; the now-line\n carries a time bubble that overhangs into the gutter. -->\n<div class=\"w-full h-full flex flex-col overflow-hidden\" role=\"grid\"
|
|
10128
|
+
args: [{ selector: 'mn-calendar-day', standalone: true, imports: [CommonModule, CalendarEventComponent], providers: [CalendarEventLayoutService], template: "<!-- Day grid. One column, its header centered over that column (the toolbar\n already names the date). Today's number is tinted, not filled; the now-line\n carries a time bubble that overhangs into the gutter. -->\n<div [attr.aria-label]=\"dayViewLabel\" class=\"w-full h-full flex flex-col overflow-hidden\" role=\"grid\">\n <div class=\"grid grid-cols-[60px_1fr]\">\n <div></div>\n <div class=\"flex items-center justify-center gap-2 py-2 px-1\" role=\"columnheader\">\n <span class=\"text-[11px] font-semibold uppercase tracking-wide opacity-50\">{{ dayName }}</span>\n <span class=\"text-base font-bold tabular-nums\" [class.text-primary]=\"isToday\">{{ focusDay.getDate() }}</span>\n </div>\n </div>\n <div class=\"grid grid-cols-[60px_1fr] flex-1 min-h-0 overflow-hidden items-stretch\">\n <div class=\"grid h-full min-h-0\" [style.grid-template-rows]=\"'repeat(' + totalRows + ', minmax(1.5rem, 1fr))'\">\n @for (row of hourRows; track row.topRow) {\n <div class=\"flex items-start justify-end min-h-0 overflow-hidden pr-2 text-[11px] tabular-nums opacity-50\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\">\n {{ row.hourLabel }}\n </div>\n }\n </div>\n <div class=\"grid relative auto-rows-fr h-full min-h-0\"\n [style.grid-template-rows]=\"'repeat(' + totalRows + ', minmax(1.5rem, 1fr))'\"\n [style.grid-template-columns]=\"'repeat(' + totalColumns + ', 1fr)'\">\n @for (row of hourRows; track row.topRow) {\n <div class=\"border-t border-base-200 pointer-events-none min-h-0\"\n [style.grid-row]=\"row.topRow + '/' + row.bottomRow\"\n [style.grid-column]=\"'1 / -1'\">\n </div>\n }\n @if (currentTimeRow > 0 && isToday) {\n <div class=\"relative z-[2] pointer-events-none\"\n [style.grid-row]=\"currentTimeRow\"\n [style.grid-column]=\"'1 / -1'\">\n <div class=\"absolute -left-1 -top-1 h-2 w-2 rounded-full bg-error\"></div>\n <div class=\"h-0.5 w-full bg-error\"></div>\n <div class=\"absolute left-0 top-0 -translate-x-2 -translate-y-1/2 rounded-full bg-error px-1.5 py-0.5 text-[10px] font-bold tabular-nums text-error-content shadow\">{{ currentTimeLabel }}</div>\n </div>\n }\n @for (event of displayEvents; track $index) {\n <div class=\"z-[1] min-h-0 overflow-hidden rounded-lg p-0.5 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary\"\n [style.grid-row]=\"getEventRow(event)\"\n [style.grid-column]=\"getEventColumn(event)\"\n (click)=\"onEventClick(event)\"\n (keyup.enter)=\"onEventClick(event)\"\n tabindex=\"0\">\n <mn-calendar-event [customComponent]=\"calendarEventComponent\" [event]=\"event\"></mn-calendar-event>\n </div>\n }\n </div>\n </div>\n</div>\n" }]
|
|
9972
10129
|
}], ctorParameters: () => [], propDecorators: { focusDay: [{
|
|
9973
10130
|
type: Input
|
|
9974
10131
|
}], eventsChanged: [{
|
|
@@ -10022,6 +10179,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
10022
10179
|
* (events whose end time is in the future), sorted by start time.
|
|
10023
10180
|
*/
|
|
10024
10181
|
class UpcomingEventsComponent {
|
|
10182
|
+
lang = inject(MnLanguageService);
|
|
10183
|
+
/**
|
|
10184
|
+
* Accessible name for this control. Resolved through the conventional
|
|
10185
|
+
* `mnCalendar.upcomingEvents` key so an app can translate it, falling back to English when the
|
|
10186
|
+
* key is not defined rather than leaking the raw key into the UI.
|
|
10187
|
+
*/
|
|
10188
|
+
get upcomingEventsLabel() {
|
|
10189
|
+
return this.lang.translateIfPresent('mnCalendar.upcomingEvents') ?? 'Upcoming events';
|
|
10190
|
+
}
|
|
10025
10191
|
/** Observable that emits the full event list whenever it changes. */
|
|
10026
10192
|
eventsChanged;
|
|
10027
10193
|
/** Resolved calendar configuration passed from the parent view. */
|
|
@@ -10067,11 +10233,11 @@ class UpcomingEventsComponent {
|
|
|
10067
10233
|
return event.id;
|
|
10068
10234
|
}
|
|
10069
10235
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UpcomingEventsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10070
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: UpcomingEventsComponent, isStandalone: true, selector: "mn-upcoming-events", inputs: { eventsChanged: "eventsChanged", config: "config" }, outputs: { eventClicked: "eventClicked" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"p-4\" role=\"complementary\"
|
|
10236
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: UpcomingEventsComponent, isStandalone: true, selector: "mn-upcoming-events", inputs: { eventsChanged: "eventsChanged", config: "config" }, outputs: { eventClicked: "eventClicked" }, usesOnChanges: true, ngImport: i0, template: "<div [attr.aria-label]=\"upcomingEventsLabel\" class=\"p-4\" role=\"complementary\">\n <div class=\"mb-3 flex items-center gap-2\">\n <h3 class=\"text-sm font-bold tracking-wide\">{{ title }}</h3>\n @if (upcomingEvents.length) {\n <span class=\"text-xs font-semibold tabular-nums opacity-45\">{{ upcomingEvents.length }}</span>\n }\n </div>\n @for (event of upcomingEvents; track $index) {\n <mn-upcoming-event-row\n [event]=\"event\"\n (eventClicked)=\"eventClicked.emit($event)\">\n </mn-upcoming-event-row>\n }\n @if (upcomingEvents.length === 0) {\n <div class=\"text-sm opacity-50\">{{ noEventsMessage }}</div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: UpcomingEventRowComponent, selector: "mn-upcoming-event-row", inputs: ["event"], outputs: ["eventClicked"] }] });
|
|
10071
10237
|
}
|
|
10072
10238
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UpcomingEventsComponent, decorators: [{
|
|
10073
10239
|
type: Component,
|
|
10074
|
-
args: [{ selector: 'mn-upcoming-events', standalone: true, imports: [CommonModule, UpcomingEventRowComponent], template: "<div class=\"p-4\" role=\"complementary\"
|
|
10240
|
+
args: [{ selector: 'mn-upcoming-events', standalone: true, imports: [CommonModule, UpcomingEventRowComponent], template: "<div [attr.aria-label]=\"upcomingEventsLabel\" class=\"p-4\" role=\"complementary\">\n <div class=\"mb-3 flex items-center gap-2\">\n <h3 class=\"text-sm font-bold tracking-wide\">{{ title }}</h3>\n @if (upcomingEvents.length) {\n <span class=\"text-xs font-semibold tabular-nums opacity-45\">{{ upcomingEvents.length }}</span>\n }\n </div>\n @for (event of upcomingEvents; track $index) {\n <mn-upcoming-event-row\n [event]=\"event\"\n (eventClicked)=\"eventClicked.emit($event)\">\n </mn-upcoming-event-row>\n }\n @if (upcomingEvents.length === 0) {\n <div class=\"text-sm opacity-50\">{{ noEventsMessage }}</div>\n }\n</div>\n" }]
|
|
10075
10241
|
}], ctorParameters: () => [], propDecorators: { eventsChanged: [{
|
|
10076
10242
|
type: Input
|
|
10077
10243
|
}], config: [{
|
|
@@ -10107,6 +10273,14 @@ let instanceCounter = 0;
|
|
|
10107
10273
|
* ```
|
|
10108
10274
|
*/
|
|
10109
10275
|
class CalendarViewComponent {
|
|
10276
|
+
/**
|
|
10277
|
+
* Accessible name for this control. Resolved through the conventional
|
|
10278
|
+
* `mnCalendar.calendarView` key so an app can translate it, falling back to English when the
|
|
10279
|
+
* key is not defined rather than leaking the raw key into the UI.
|
|
10280
|
+
*/
|
|
10281
|
+
get calendarViewLabel() {
|
|
10282
|
+
return this.lang.translateIfPresent('mnCalendar.calendarView') ?? 'Calendar view';
|
|
10283
|
+
}
|
|
10110
10284
|
/** Whether to show the action button in the toolbar. */
|
|
10111
10285
|
showButton = false;
|
|
10112
10286
|
/** Label text for the action button. */
|
|
@@ -10318,7 +10492,7 @@ class CalendarViewComponent {
|
|
|
10318
10492
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10319
10493
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: CalendarViewComponent, isStandalone: true, selector: "mn-calendar-view", inputs: { showButton: "showButton", buttonTitle: "buttonTitle", buttons: "buttons", CalendarEventComponent: "CalendarEventComponent", NewCalendarItemsEvent: "NewCalendarItemsEvent" }, outputs: { RequestNewCalendarItemsEvent: "RequestNewCalendarItemsEvent", CalendarItemClickedEvent: "CalendarItemClickedEvent", ButtonClickedEvent: "ButtonClickedEvent" }, host: { listeners: { "window:resize": "onResize()" } }, providers: [
|
|
10320
10494
|
provideMnCalendarConfig(DEFAULT_CALENDAR_CONFIG),
|
|
10321
|
-
], ngImport: i0, template: "<div class=\"w-full h-full flex flex-col\" role=\"application\" aria-label=\"Calendar\">\n\n <!-- Toolbar has one layout below the desktop (`lg`) width and one at it. Below\n `lg`: a controls row (Today anchored left, the right-hand cluster anchored\n right) with the \u2039 date \u203A nav centred on its own row beneath \u2014 the same\n shape from phone through tablet, so nothing wraps into a lopsided diagonal.\n At `lg`, where the upcoming-events sidebar also appears, the nav folds up\n inline after Today and the whole toolbar becomes a single row. The one JS\n breakpoint (`mobileBreakpoint`) governs the phone controls \u2014 icon-only\n picker, hidden view-switcher, forced day view \u2014 not the toolbar's shape. -->\n <div class=\"flex flex-wrap items-center gap-x-3 gap-y-2 py-3\">\n\n <button (click)=\"goToToday()\"\n [data]=\"{ variant: 'outline', size: 'md', color: 'primary' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n {{ config.todayLabel }}\n </button>\n\n <!-- Stepping through time. The arrows sit either side of the date they move,\n so the control and the thing it controls read as one unit. Below `lg` it\n sits centred on its own row (basis-full, ordered last) \u2014 a period header\n above the grid, tight rather than flinging the arrows to the row's edges.\n At `lg` it folds inline right after Today, taking a stable min-width so\n the arrows don't jitter as the label's length changes. -->\n <div class=\"flex basis-full order-last justify-center items-center gap-1\n lg:basis-auto lg:order-none lg:justify-start\">\n <button (click)=\"navigate(-1)\"\n [attr.aria-label]=\"config.previousLabel\"\n [data]=\"{ variant: 'text', size: 'md', color: 'gray' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n <svg [size]=\"18\" lucideChevronLeft></svg>\n </button>\n\n <h2 aria-live=\"polite\"\n class=\"text-center text-base font-semibold whitespace-nowrap px-1 lg:min-w-56\">\n {{ periodLabel }}\n </h2>\n\n <button (click)=\"navigate(1)\"\n [attr.aria-label]=\"config.nextLabel\"\n [data]=\"{ variant: 'text', size: 'md', color: 'gray' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n <svg [size]=\"18\" lucideChevronRight></svg>\n </button>\n </div>\n\n <!-- Right-hand controls travel together as one cluster with `ml-auto`, so they\n hold to the right of the row on every width \u2014 and when space runs short the\n whole group wraps to a new line as a unit rather than stranding a lone\n button. On desktop the cluster lands above the upcoming-events sidebar,\n using that otherwise-empty space. -->\n <div class=\"ml-auto flex items-center gap-2\">\n <!-- On mobile the picker collapses to its icon-only variant. -->\n <mn-lib-datetime (ngModelChange)=\"onPickDate($event)\"\n [ngModel]=\"focusDayString\"\n [props]=\"{ id: panelId + '-date', mode: 'date', placeholder: config.pickDateLabel, size: 'md', borderRadius: 'lg', hover: true, iconOnly: isMobileView }\"\n class=\"block\"></mn-lib-datetime>\n\n <!-- View switcher (month / week / day) \u2014 hidden on mobile, which is forced to day view. -->\n @if (!isMobileView) {\n <div aria-label=\"
|
|
10495
|
+
], ngImport: i0, template: "<div class=\"w-full h-full flex flex-col\" role=\"application\" aria-label=\"Calendar\">\n\n <!-- Toolbar has one layout below the desktop (`lg`) width and one at it. Below\n `lg`: a controls row (Today anchored left, the right-hand cluster anchored\n right) with the \u2039 date \u203A nav centred on its own row beneath \u2014 the same\n shape from phone through tablet, so nothing wraps into a lopsided diagonal.\n At `lg`, where the upcoming-events sidebar also appears, the nav folds up\n inline after Today and the whole toolbar becomes a single row. The one JS\n breakpoint (`mobileBreakpoint`) governs the phone controls \u2014 icon-only\n picker, hidden view-switcher, forced day view \u2014 not the toolbar's shape. -->\n <div class=\"flex flex-wrap items-center gap-x-3 gap-y-2 py-3\">\n\n <button (click)=\"goToToday()\"\n [data]=\"{ variant: 'outline', size: 'md', color: 'primary' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n {{ config.todayLabel }}\n </button>\n\n <!-- Stepping through time. The arrows sit either side of the date they move,\n so the control and the thing it controls read as one unit. Below `lg` it\n sits centred on its own row (basis-full, ordered last) \u2014 a period header\n above the grid, tight rather than flinging the arrows to the row's edges.\n At `lg` it folds inline right after Today, taking a stable min-width so\n the arrows don't jitter as the label's length changes. -->\n <div class=\"flex basis-full order-last justify-center items-center gap-1\n lg:basis-auto lg:order-none lg:justify-start\">\n <button (click)=\"navigate(-1)\"\n [attr.aria-label]=\"config.previousLabel\"\n [data]=\"{ variant: 'text', size: 'md', color: 'gray' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n <svg [size]=\"18\" lucideChevronLeft></svg>\n </button>\n\n <h2 aria-live=\"polite\"\n class=\"text-center text-base font-semibold whitespace-nowrap px-1 lg:min-w-56\">\n {{ periodLabel }}\n </h2>\n\n <button (click)=\"navigate(1)\"\n [attr.aria-label]=\"config.nextLabel\"\n [data]=\"{ variant: 'text', size: 'md', color: 'gray' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n <svg [size]=\"18\" lucideChevronRight></svg>\n </button>\n </div>\n\n <!-- Right-hand controls travel together as one cluster with `ml-auto`, so they\n hold to the right of the row on every width \u2014 and when space runs short the\n whole group wraps to a new line as a unit rather than stranding a lone\n button. On desktop the cluster lands above the upcoming-events sidebar,\n using that otherwise-empty space. -->\n <div class=\"ml-auto flex items-center gap-2\">\n <!-- On mobile the picker collapses to its icon-only variant. -->\n <mn-lib-datetime (ngModelChange)=\"onPickDate($event)\"\n [ngModel]=\"focusDayString\"\n [props]=\"{ id: panelId + '-date', mode: 'date', placeholder: config.pickDateLabel, size: 'md', borderRadius: 'lg', hover: true, iconOnly: isMobileView }\"\n class=\"block\"></mn-lib-datetime>\n\n <!-- View switcher (month / week / day) \u2014 hidden on mobile, which is forced to day view. -->\n @if (!isMobileView) {\n <div [attr.aria-label]=\"calendarViewLabel\" class=\"flex border border-base-300 rounded-md overflow-hidden\"\n role=\"tablist\">\n @for (view of viewOptions; track view.value) {\n <button\n (click)=\"switchView(view.value)\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-selected]=\"currentView === view.value\"\n [data]=\"{ size: 'md', variant: currentView === view.value ? 'fill' : 'text', color: 'primary' }\"\n mnButton\n role=\"tab\"\n type=\"button\">\n {{ view.label }}\n </button>\n }\n </div>\n }\n\n <!-- Action buttons (custom buttons + optional showButton CTA). -->\n @if (buttons.length || showButton) {\n @for (btn of buttons; track btn.label) {\n <button (click)=\"btn.onClick()\" [data]=\"btn.buttonData || {}\" mnButton type=\"button\">\n {{ btn.label }}\n </button>\n }\n @if (showButton) {\n <button (click)=\"ButtonClickedEvent.emit()\" [data]=\"{}\" mnButton type=\"button\">\n {{ buttonTitle }}\n </button>\n }\n }\n </div>\n\n </div>\n\n <div class=\"grid grid-cols-1 lg:grid-cols-[1fr_220px] gap-3 flex-1 min-h-0\">\n <div [id]=\"panelId\" class=\"min-w-0 min-h-0 overflow-hidden overflow-y-auto\" role=\"tabpanel\">\n @if (currentView === CalendarView.MONTH) {\n <mn-calendar-month\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n (dayClicked)=\"onMonthDayClick($event)\">\n </mn-calendar-month>\n }\n @if (currentView === CalendarView.WEEK) {\n <mn-calendar-week\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </mn-calendar-week>\n }\n @if (currentView === CalendarView.DAY) {\n <mn-calendar-day\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </mn-calendar-day>\n }\n </div>\n <div class=\"hidden lg:block border-l border-base-300 overflow-auto\">\n <mn-upcoming-events\n [eventsChanged]=\"internalEventsChanged\"\n [config]=\"config\"\n (eventClicked)=\"onEventClick($event)\">\n </mn-upcoming-events>\n </div>\n </div>\n\n</div>\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: CalendarMonthComponent, selector: "mn-calendar-month", inputs: ["focusDay", "eventsChanged", "focusDayChanged", "config"], outputs: ["dayClicked"] }, { kind: "component", type: CalendarWeekComponent, selector: "mn-calendar-week", inputs: ["focusDay", "eventsChanged", "focusDayChanged", "config", "calendarEventComponent"], outputs: ["eventClicked"] }, { kind: "component", type: CalendarDayComponent, selector: "mn-calendar-day", inputs: ["focusDay", "eventsChanged", "focusDayChanged", "config", "calendarEventComponent"], outputs: ["eventClicked"] }, { kind: "component", type: UpcomingEventsComponent, selector: "mn-upcoming-events", inputs: ["eventsChanged", "config"], outputs: ["eventClicked"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnDatetime, selector: "mn-lib-datetime", inputs: ["props"] }, { kind: "component", type: LucideChevronLeft, selector: "svg[lucideChevronLeft]" }, { kind: "component", type: LucideChevronRight, selector: "svg[lucideChevronRight]" }] });
|
|
10322
10496
|
}
|
|
10323
10497
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CalendarViewComponent, decorators: [{
|
|
10324
10498
|
type: Component,
|
|
@@ -10335,7 +10509,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
10335
10509
|
LucideChevronRight
|
|
10336
10510
|
], providers: [
|
|
10337
10511
|
provideMnCalendarConfig(DEFAULT_CALENDAR_CONFIG),
|
|
10338
|
-
], template: "<div class=\"w-full h-full flex flex-col\" role=\"application\" aria-label=\"Calendar\">\n\n <!-- Toolbar has one layout below the desktop (`lg`) width and one at it. Below\n `lg`: a controls row (Today anchored left, the right-hand cluster anchored\n right) with the \u2039 date \u203A nav centred on its own row beneath \u2014 the same\n shape from phone through tablet, so nothing wraps into a lopsided diagonal.\n At `lg`, where the upcoming-events sidebar also appears, the nav folds up\n inline after Today and the whole toolbar becomes a single row. The one JS\n breakpoint (`mobileBreakpoint`) governs the phone controls \u2014 icon-only\n picker, hidden view-switcher, forced day view \u2014 not the toolbar's shape. -->\n <div class=\"flex flex-wrap items-center gap-x-3 gap-y-2 py-3\">\n\n <button (click)=\"goToToday()\"\n [data]=\"{ variant: 'outline', size: 'md', color: 'primary' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n {{ config.todayLabel }}\n </button>\n\n <!-- Stepping through time. The arrows sit either side of the date they move,\n so the control and the thing it controls read as one unit. Below `lg` it\n sits centred on its own row (basis-full, ordered last) \u2014 a period header\n above the grid, tight rather than flinging the arrows to the row's edges.\n At `lg` it folds inline right after Today, taking a stable min-width so\n the arrows don't jitter as the label's length changes. -->\n <div class=\"flex basis-full order-last justify-center items-center gap-1\n lg:basis-auto lg:order-none lg:justify-start\">\n <button (click)=\"navigate(-1)\"\n [attr.aria-label]=\"config.previousLabel\"\n [data]=\"{ variant: 'text', size: 'md', color: 'gray' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n <svg [size]=\"18\" lucideChevronLeft></svg>\n </button>\n\n <h2 aria-live=\"polite\"\n class=\"text-center text-base font-semibold whitespace-nowrap px-1 lg:min-w-56\">\n {{ periodLabel }}\n </h2>\n\n <button (click)=\"navigate(1)\"\n [attr.aria-label]=\"config.nextLabel\"\n [data]=\"{ variant: 'text', size: 'md', color: 'gray' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n <svg [size]=\"18\" lucideChevronRight></svg>\n </button>\n </div>\n\n <!-- Right-hand controls travel together as one cluster with `ml-auto`, so they\n hold to the right of the row on every width \u2014 and when space runs short the\n whole group wraps to a new line as a unit rather than stranding a lone\n button. On desktop the cluster lands above the upcoming-events sidebar,\n using that otherwise-empty space. -->\n <div class=\"ml-auto flex items-center gap-2\">\n <!-- On mobile the picker collapses to its icon-only variant. -->\n <mn-lib-datetime (ngModelChange)=\"onPickDate($event)\"\n [ngModel]=\"focusDayString\"\n [props]=\"{ id: panelId + '-date', mode: 'date', placeholder: config.pickDateLabel, size: 'md', borderRadius: 'lg', hover: true, iconOnly: isMobileView }\"\n class=\"block\"></mn-lib-datetime>\n\n <!-- View switcher (month / week / day) \u2014 hidden on mobile, which is forced to day view. -->\n @if (!isMobileView) {\n <div aria-label=\"
|
|
10512
|
+
], template: "<div class=\"w-full h-full flex flex-col\" role=\"application\" aria-label=\"Calendar\">\n\n <!-- Toolbar has one layout below the desktop (`lg`) width and one at it. Below\n `lg`: a controls row (Today anchored left, the right-hand cluster anchored\n right) with the \u2039 date \u203A nav centred on its own row beneath \u2014 the same\n shape from phone through tablet, so nothing wraps into a lopsided diagonal.\n At `lg`, where the upcoming-events sidebar also appears, the nav folds up\n inline after Today and the whole toolbar becomes a single row. The one JS\n breakpoint (`mobileBreakpoint`) governs the phone controls \u2014 icon-only\n picker, hidden view-switcher, forced day view \u2014 not the toolbar's shape. -->\n <div class=\"flex flex-wrap items-center gap-x-3 gap-y-2 py-3\">\n\n <button (click)=\"goToToday()\"\n [data]=\"{ variant: 'outline', size: 'md', color: 'primary' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n {{ config.todayLabel }}\n </button>\n\n <!-- Stepping through time. The arrows sit either side of the date they move,\n so the control and the thing it controls read as one unit. Below `lg` it\n sits centred on its own row (basis-full, ordered last) \u2014 a period header\n above the grid, tight rather than flinging the arrows to the row's edges.\n At `lg` it folds inline right after Today, taking a stable min-width so\n the arrows don't jitter as the label's length changes. -->\n <div class=\"flex basis-full order-last justify-center items-center gap-1\n lg:basis-auto lg:order-none lg:justify-start\">\n <button (click)=\"navigate(-1)\"\n [attr.aria-label]=\"config.previousLabel\"\n [data]=\"{ variant: 'text', size: 'md', color: 'gray' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n <svg [size]=\"18\" lucideChevronLeft></svg>\n </button>\n\n <h2 aria-live=\"polite\"\n class=\"text-center text-base font-semibold whitespace-nowrap px-1 lg:min-w-56\">\n {{ periodLabel }}\n </h2>\n\n <button (click)=\"navigate(1)\"\n [attr.aria-label]=\"config.nextLabel\"\n [data]=\"{ variant: 'text', size: 'md', color: 'gray' }\"\n class=\"shrink-0\"\n mnButton\n type=\"button\">\n <svg [size]=\"18\" lucideChevronRight></svg>\n </button>\n </div>\n\n <!-- Right-hand controls travel together as one cluster with `ml-auto`, so they\n hold to the right of the row on every width \u2014 and when space runs short the\n whole group wraps to a new line as a unit rather than stranding a lone\n button. On desktop the cluster lands above the upcoming-events sidebar,\n using that otherwise-empty space. -->\n <div class=\"ml-auto flex items-center gap-2\">\n <!-- On mobile the picker collapses to its icon-only variant. -->\n <mn-lib-datetime (ngModelChange)=\"onPickDate($event)\"\n [ngModel]=\"focusDayString\"\n [props]=\"{ id: panelId + '-date', mode: 'date', placeholder: config.pickDateLabel, size: 'md', borderRadius: 'lg', hover: true, iconOnly: isMobileView }\"\n class=\"block\"></mn-lib-datetime>\n\n <!-- View switcher (month / week / day) \u2014 hidden on mobile, which is forced to day view. -->\n @if (!isMobileView) {\n <div [attr.aria-label]=\"calendarViewLabel\" class=\"flex border border-base-300 rounded-md overflow-hidden\"\n role=\"tablist\">\n @for (view of viewOptions; track view.value) {\n <button\n (click)=\"switchView(view.value)\"\n [attr.aria-controls]=\"panelId\"\n [attr.aria-selected]=\"currentView === view.value\"\n [data]=\"{ size: 'md', variant: currentView === view.value ? 'fill' : 'text', color: 'primary' }\"\n mnButton\n role=\"tab\"\n type=\"button\">\n {{ view.label }}\n </button>\n }\n </div>\n }\n\n <!-- Action buttons (custom buttons + optional showButton CTA). -->\n @if (buttons.length || showButton) {\n @for (btn of buttons; track btn.label) {\n <button (click)=\"btn.onClick()\" [data]=\"btn.buttonData || {}\" mnButton type=\"button\">\n {{ btn.label }}\n </button>\n }\n @if (showButton) {\n <button (click)=\"ButtonClickedEvent.emit()\" [data]=\"{}\" mnButton type=\"button\">\n {{ buttonTitle }}\n </button>\n }\n }\n </div>\n\n </div>\n\n <div class=\"grid grid-cols-1 lg:grid-cols-[1fr_220px] gap-3 flex-1 min-h-0\">\n <div [id]=\"panelId\" class=\"min-w-0 min-h-0 overflow-hidden overflow-y-auto\" role=\"tabpanel\">\n @if (currentView === CalendarView.MONTH) {\n <mn-calendar-month\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n (dayClicked)=\"onMonthDayClick($event)\">\n </mn-calendar-month>\n }\n @if (currentView === CalendarView.WEEK) {\n <mn-calendar-week\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </mn-calendar-week>\n }\n @if (currentView === CalendarView.DAY) {\n <mn-calendar-day\n [focusDay]=\"focusDay\"\n [eventsChanged]=\"internalEventsChanged\"\n [focusDayChanged]=\"internalFocusDayChanged\"\n [config]=\"config\"\n [calendarEventComponent]=\"CalendarEventComponent\"\n (eventClicked)=\"onEventClick($event)\">\n </mn-calendar-day>\n }\n </div>\n <div class=\"hidden lg:block border-l border-base-300 overflow-auto\">\n <mn-upcoming-events\n [eventsChanged]=\"internalEventsChanged\"\n [config]=\"config\"\n (eventClicked)=\"onEventClick($event)\">\n </mn-upcoming-events>\n </div>\n </div>\n\n</div>\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100%}\n"] }]
|
|
10339
10513
|
}], ctorParameters: () => [], propDecorators: { showButton: [{
|
|
10340
10514
|
type: Input
|
|
10341
10515
|
}], buttonTitle: [{
|
|
@@ -10525,6 +10699,263 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
10525
10699
|
type: Output
|
|
10526
10700
|
}] } });
|
|
10527
10701
|
|
|
10702
|
+
/**
|
|
10703
|
+
* Toolbar the editor renders when the consumer does not supply one. Deliberately
|
|
10704
|
+
* small: it covers written prose, so there is no colour picker, no font picker
|
|
10705
|
+
* and no image embedding (an image needs an upload and storage path the editor
|
|
10706
|
+
* knows nothing about).
|
|
10707
|
+
*/
|
|
10708
|
+
const DEFAULT_TOOLBAR = [
|
|
10709
|
+
[{ header: [2, 3, false] }],
|
|
10710
|
+
['bold', 'italic', 'underline', 'strike'],
|
|
10711
|
+
[{ list: 'ordered' }, { list: 'bullet' }],
|
|
10712
|
+
['blockquote', 'code-block'],
|
|
10713
|
+
['link'],
|
|
10714
|
+
['clean'],
|
|
10715
|
+
];
|
|
10716
|
+
/** Where each control sits in the toolbar Quill builds. */
|
|
10717
|
+
const CONTROL_SELECTORS = {
|
|
10718
|
+
textStyle: '.ql-header .ql-picker-label',
|
|
10719
|
+
bold: 'button.ql-bold',
|
|
10720
|
+
italic: 'button.ql-italic',
|
|
10721
|
+
underline: 'button.ql-underline',
|
|
10722
|
+
strike: 'button.ql-strike',
|
|
10723
|
+
orderedList: 'button.ql-list[value="ordered"]',
|
|
10724
|
+
bulletList: 'button.ql-list[value="bullet"]',
|
|
10725
|
+
blockquote: 'button.ql-blockquote',
|
|
10726
|
+
codeBlock: 'button.ql-code-block',
|
|
10727
|
+
link: 'button.ql-link',
|
|
10728
|
+
clean: 'button.ql-clean',
|
|
10729
|
+
};
|
|
10730
|
+
/** Hover labels used when the consumer supplies neither a label nor a key. */
|
|
10731
|
+
const DEFAULT_LABELS = {
|
|
10732
|
+
textStyle: 'Text style',
|
|
10733
|
+
bold: 'Bold',
|
|
10734
|
+
italic: 'Italic',
|
|
10735
|
+
underline: 'Underline',
|
|
10736
|
+
strike: 'Strikethrough',
|
|
10737
|
+
orderedList: 'Numbered list',
|
|
10738
|
+
bulletList: 'Bulleted list',
|
|
10739
|
+
blockquote: 'Quote',
|
|
10740
|
+
codeBlock: 'Code block',
|
|
10741
|
+
link: 'Link',
|
|
10742
|
+
clean: 'Clear formatting',
|
|
10743
|
+
};
|
|
10744
|
+
/** Quill's document when it holds no text; treated as an empty value. */
|
|
10745
|
+
const EMPTY_DOCUMENT = '<p><br></p>';
|
|
10746
|
+
/**
|
|
10747
|
+
* Thin wrapper around the {@link Quill} rich-text editor.
|
|
10748
|
+
*
|
|
10749
|
+
* Quill is used **directly** rather than through an Angular wrapper package: the
|
|
10750
|
+
* wrapper libraries carry peer-dependency ranges that lag behind Angular's
|
|
10751
|
+
* release train, and none of them add anything this component needs.
|
|
10752
|
+
*
|
|
10753
|
+
* Consumers must install `quill` themselves (it is an optional peer dependency)
|
|
10754
|
+
* and load its snow theme, e.g. `node_modules/quill/dist/quill.snow.css` in the
|
|
10755
|
+
* `styles` array of `angular.json`. Only the chrome around that theme — radius,
|
|
10756
|
+
* borders, height limits and the toolbar tooltips — belongs to this component;
|
|
10757
|
+
* recolouring Quill's own palette to an app theme stays with the app, because
|
|
10758
|
+
* the same `.ql-snow` markup is normally reused to render stored HTML in places
|
|
10759
|
+
* where no editor is mounted.
|
|
10760
|
+
*
|
|
10761
|
+
* Quill itself is pulled in with a dynamic `import()` when the editor mounts.
|
|
10762
|
+
* This component sits in the library's single entry point, which apps import
|
|
10763
|
+
* eagerly, so a static import would put the whole editor engine in every app's
|
|
10764
|
+
* initial bundle — including the pages that never open one.
|
|
10765
|
+
*
|
|
10766
|
+
* Zoneless notes: nothing here relies on an implicit change-detection tick. The
|
|
10767
|
+
* editor is created inside {@link afterNextRender} (the host element only exists
|
|
10768
|
+
* after the first render pass) and every value that flows back out is written to
|
|
10769
|
+
* a signal or emitted through an `output`, both of which schedule change
|
|
10770
|
+
* detection themselves. No `setTimeout`, no manual `detectChanges`.
|
|
10771
|
+
*
|
|
10772
|
+
* The produced HTML is **not** trusted: sanitise it before rendering it anywhere.
|
|
10773
|
+
*
|
|
10774
|
+
* @example
|
|
10775
|
+
* ```html
|
|
10776
|
+
* <mn-rich-text-editor
|
|
10777
|
+
* [content]="draft()"
|
|
10778
|
+
* [placeholder]="'minutes.placeholder' | mnTranslate"
|
|
10779
|
+
* [labelKeys]="{ bold: 'editor.bold', italic: 'editor.italic' }"
|
|
10780
|
+
* (contentChange)="draft.set($event)">
|
|
10781
|
+
* </mn-rich-text-editor>
|
|
10782
|
+
* ```
|
|
10783
|
+
*/
|
|
10784
|
+
class MnRichTextEditor {
|
|
10785
|
+
/**
|
|
10786
|
+
* The initial HTML content. Later changes are applied only when they differ
|
|
10787
|
+
* from what the editor currently holds, so a parent echoing the emitted value
|
|
10788
|
+
* back never moves the caret.
|
|
10789
|
+
*/
|
|
10790
|
+
content = input('', ...(ngDevMode ? [{ debugName: "content" }] : []));
|
|
10791
|
+
/** Placeholder shown while the editor is empty. */
|
|
10792
|
+
placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
|
|
10793
|
+
/** Accessible label for the editing surface. */
|
|
10794
|
+
ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
10795
|
+
/** Toolbar layout, in Quill's own format. Defaults to a prose-oriented set. */
|
|
10796
|
+
toolbar = input(DEFAULT_TOOLBAR, ...(ngDevMode ? [{ debugName: "toolbar" }] : []));
|
|
10797
|
+
/** Literal hover labels per toolbar control. */
|
|
10798
|
+
labels = input({}, ...(ngDevMode ? [{ debugName: "labels" }] : []));
|
|
10799
|
+
/** Translation keys per toolbar control; takes precedence over `labels`. */
|
|
10800
|
+
labelKeys = input({}, ...(ngDevMode ? [{ debugName: "labelKeys" }] : []));
|
|
10801
|
+
/**
|
|
10802
|
+
* Utilities applied to the wrapper, for sizing the writing surface. Overriding
|
|
10803
|
+
* this replaces the default height limits, so pass both bounds when you do.
|
|
10804
|
+
*/
|
|
10805
|
+
editorClass = input('[&_.ql-editor]:max-h-104 [&_.ql-editor]:min-h-72 [&_.ql-editor]:overflow-y-auto', ...(ngDevMode ? [{ debugName: "editorClass" }] : []));
|
|
10806
|
+
/** Emits the editor's HTML on every user edit. */
|
|
10807
|
+
contentChange = output();
|
|
10808
|
+
/**
|
|
10809
|
+
* Chrome around Quill's snow theme: the field's radius, border and surface.
|
|
10810
|
+
*
|
|
10811
|
+
* Descendant variants rather than a stylesheet — the utilities come from the
|
|
10812
|
+
* consuming app's Tailwind build (which scans this bundle), so they follow the
|
|
10813
|
+
* app's theme tokens the same way the rest of the library does.
|
|
10814
|
+
*/
|
|
10815
|
+
chromeClass = '[&_.ql-container.ql-snow]:rounded-b-xl [&_.ql-container.ql-snow]:border-base-300 ' +
|
|
10816
|
+
'[&_.ql-container.ql-snow]:bg-base-100 [&_.ql-container.ql-snow]:text-base ' +
|
|
10817
|
+
'[&_.ql-toolbar.ql-snow]:rounded-t-xl [&_.ql-toolbar.ql-snow]:border-base-300 ' +
|
|
10818
|
+
'[&_.ql-toolbar.ql-snow]:bg-base-100';
|
|
10819
|
+
/** Host element, used to keep DOM queries inside this component. */
|
|
10820
|
+
host = inject(ElementRef);
|
|
10821
|
+
/** Language service, used to resolve the toolbar labels from keys. */
|
|
10822
|
+
lang = inject(MnLanguageService);
|
|
10823
|
+
/** The container Quill mounts into. */
|
|
10824
|
+
editorHost = viewChild.required('editorHost');
|
|
10825
|
+
/** The live editor instance, or null before Quill has loaded. */
|
|
10826
|
+
quill = null;
|
|
10827
|
+
/** Whether the component is gone, so a late Quill load knows to stop. */
|
|
10828
|
+
destroyed = false;
|
|
10829
|
+
/** The last HTML this component emitted, used to skip redundant writes. */
|
|
10830
|
+
lastEmitted = signal('', ...(ngDevMode ? [{ debugName: "lastEmitted" }] : []));
|
|
10831
|
+
constructor() {
|
|
10832
|
+
afterNextRender(() => void this.createEditor());
|
|
10833
|
+
// Push a changed `content` input into a live editor. `lastEmitted` is read
|
|
10834
|
+
// `untracked` on purpose: it must gate the write (skip when the incoming HTML
|
|
10835
|
+
// already matches what we hold) WITHOUT making the effect depend on it. If it
|
|
10836
|
+
// were tracked, every keystroke (which updates `lastEmitted`) would re-run the
|
|
10837
|
+
// effect and re-paste the now-stale `content` seed — wiping the user's typing
|
|
10838
|
+
// and resetting the caret after any wholesale reseed.
|
|
10839
|
+
effect(() => {
|
|
10840
|
+
const incoming = this.content();
|
|
10841
|
+
if (!this.quill || incoming === untracked(this.lastEmitted))
|
|
10842
|
+
return;
|
|
10843
|
+
this.setEditorHtml(incoming);
|
|
10844
|
+
});
|
|
10845
|
+
}
|
|
10846
|
+
/** Drops the editor reference so the instance can be garbage collected. */
|
|
10847
|
+
ngOnDestroy() {
|
|
10848
|
+
this.destroyed = true;
|
|
10849
|
+
this.quill = null;
|
|
10850
|
+
}
|
|
10851
|
+
/** Moves focus into the editing surface. */
|
|
10852
|
+
focusEditor() {
|
|
10853
|
+
this.quill?.focus();
|
|
10854
|
+
if (!this.quill) {
|
|
10855
|
+
this.host.nativeElement.querySelector('.ql-editor')?.focus();
|
|
10856
|
+
}
|
|
10857
|
+
}
|
|
10858
|
+
/**
|
|
10859
|
+
* Loads Quill, builds the instance and wires its change handler.
|
|
10860
|
+
*
|
|
10861
|
+
* Nothing awaits this beyond the component itself: the surface appears once
|
|
10862
|
+
* the engine has loaded, and until then the `content` effect is a no-op that
|
|
10863
|
+
* the seeding below makes good.
|
|
10864
|
+
*/
|
|
10865
|
+
async createEditor() {
|
|
10866
|
+
const { default: QuillEditor } = await import('quill');
|
|
10867
|
+
if (this.destroyed)
|
|
10868
|
+
return;
|
|
10869
|
+
const container = this.editorHost().nativeElement;
|
|
10870
|
+
this.quill = new QuillEditor(container, {
|
|
10871
|
+
theme: 'snow',
|
|
10872
|
+
placeholder: this.placeholder(),
|
|
10873
|
+
modules: { toolbar: this.toolbar() },
|
|
10874
|
+
});
|
|
10875
|
+
const label = this.ariaLabel();
|
|
10876
|
+
if (label) {
|
|
10877
|
+
this.quill.root.setAttribute('aria-label', label);
|
|
10878
|
+
}
|
|
10879
|
+
this.applyToolbarLabels();
|
|
10880
|
+
this.setEditorHtml(this.content());
|
|
10881
|
+
this.quill.on('text-change', () => this.emitCurrentHtml());
|
|
10882
|
+
}
|
|
10883
|
+
/**
|
|
10884
|
+
* Gives each toolbar control a hover label, so hovering explains what the
|
|
10885
|
+
* style does. Set on the Quill-generated DOM after init; a control the current
|
|
10886
|
+
* toolbar does not render is simply skipped.
|
|
10887
|
+
*/
|
|
10888
|
+
applyToolbarLabels() {
|
|
10889
|
+
const toolbar = this.host.nativeElement.querySelector('.ql-toolbar');
|
|
10890
|
+
if (!toolbar)
|
|
10891
|
+
return;
|
|
10892
|
+
const labels = this.labels();
|
|
10893
|
+
const keys = this.labelKeys();
|
|
10894
|
+
for (const [control, selector] of Object.entries(CONTROL_SELECTORS)) {
|
|
10895
|
+
const element = toolbar.querySelector(selector);
|
|
10896
|
+
if (!element)
|
|
10897
|
+
continue;
|
|
10898
|
+
element.classList.add('mn-rte-tooltip');
|
|
10899
|
+
element.setAttribute('data-tip', this.resolveLabel(control, labels, keys));
|
|
10900
|
+
}
|
|
10901
|
+
}
|
|
10902
|
+
/**
|
|
10903
|
+
* Picks the hover label for one control.
|
|
10904
|
+
* @param control The control being labelled.
|
|
10905
|
+
* @param labels Literal labels supplied by the consumer.
|
|
10906
|
+
* @param keys Translation keys supplied by the consumer.
|
|
10907
|
+
* @returns The translated key, the literal label, or the built-in default.
|
|
10908
|
+
*/
|
|
10909
|
+
resolveLabel(control, labels, keys) {
|
|
10910
|
+
const key = keys[control];
|
|
10911
|
+
if (key) {
|
|
10912
|
+
// `t()` echoes the key back when the consumer has no translation for it;
|
|
10913
|
+
// that is a miss, not a label, so fall through to the remaining sources.
|
|
10914
|
+
const translated = this.lang.t(key);
|
|
10915
|
+
if (translated !== key)
|
|
10916
|
+
return translated;
|
|
10917
|
+
}
|
|
10918
|
+
return labels[control] ?? DEFAULT_LABELS[control];
|
|
10919
|
+
}
|
|
10920
|
+
/**
|
|
10921
|
+
* Replaces the editor content with stored HTML. Quill parses it into its own
|
|
10922
|
+
* document model, which silently drops anything it has no format for — a
|
|
10923
|
+
* useful extra filter on top of the consumer's sanitiser.
|
|
10924
|
+
* @param html The HTML to load into the editor.
|
|
10925
|
+
*/
|
|
10926
|
+
setEditorHtml(html) {
|
|
10927
|
+
if (!this.quill)
|
|
10928
|
+
return;
|
|
10929
|
+
// `dangerouslyPasteHTML` is Quill's documented name for "parse this HTML".
|
|
10930
|
+
this.quill.clipboard.dangerouslyPasteHTML(html ?? '', 'silent');
|
|
10931
|
+
this.lastEmitted.set(this.readHtml());
|
|
10932
|
+
}
|
|
10933
|
+
/** Emits the editor's current HTML, normalising Quill's "empty" document. */
|
|
10934
|
+
emitCurrentHtml() {
|
|
10935
|
+
const html = this.readHtml();
|
|
10936
|
+
this.lastEmitted.set(html);
|
|
10937
|
+
this.contentChange.emit(html);
|
|
10938
|
+
}
|
|
10939
|
+
/**
|
|
10940
|
+
* Reads the editor's HTML.
|
|
10941
|
+
* @returns The current HTML, or an empty string when the editor is blank.
|
|
10942
|
+
*/
|
|
10943
|
+
readHtml() {
|
|
10944
|
+
if (!this.quill)
|
|
10945
|
+
return '';
|
|
10946
|
+
// Quill leaves an empty paragraph behind after a clear; treat that as empty
|
|
10947
|
+
// so an untouched editor does not count as authored content.
|
|
10948
|
+
const html = this.quill.root.innerHTML;
|
|
10949
|
+
return html === EMPTY_DOCUMENT ? '' : html;
|
|
10950
|
+
}
|
|
10951
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnRichTextEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10952
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.3", type: MnRichTextEditor, isStandalone: true, selector: "mn-rich-text-editor", inputs: { content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, toolbar: { classPropertyName: "toolbar", publicName: "toolbar", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null }, labelKeys: { classPropertyName: "labelKeys", publicName: "labelKeys", isSignal: true, isRequired: false, transformFunction: null }, editorClass: { classPropertyName: "editorClass", publicName: "editorClass", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { contentChange: "contentChange" }, viewQueries: [{ propertyName: "editorHost", first: true, predicate: ["editorHost"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"mn-rich-text-editor {{ chromeClass }} {{ editorClass() }}\">\n <div #editorHost></div>\n</div>\n", styles: [".mn-rich-text-editor .mn-rte-tooltip{position:relative}.mn-rich-text-editor .mn-rte-tooltip[data-tip]:after{content:attr(data-tip);position:absolute;bottom:100%;left:50%;translate:-50% -.375rem;padding:.25rem .5rem;border-radius:.375rem;background-color:var(--color-secondary);color:var(--color-secondary-content);font-size:.75rem;line-height:1rem;white-space:nowrap;pointer-events:none;opacity:0;transition:opacity .15s;z-index:50}.mn-rich-text-editor .mn-rte-tooltip[data-tip]:hover:after,.mn-rich-text-editor .mn-rte-tooltip[data-tip]:focus-visible:after{opacity:1}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
10953
|
+
}
|
|
10954
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnRichTextEditor, decorators: [{
|
|
10955
|
+
type: Component,
|
|
10956
|
+
args: [{ selector: 'mn-rich-text-editor', standalone: true, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"mn-rich-text-editor {{ chromeClass }} {{ editorClass() }}\">\n <div #editorHost></div>\n</div>\n", styles: [".mn-rich-text-editor .mn-rte-tooltip{position:relative}.mn-rich-text-editor .mn-rte-tooltip[data-tip]:after{content:attr(data-tip);position:absolute;bottom:100%;left:50%;translate:-50% -.375rem;padding:.25rem .5rem;border-radius:.375rem;background-color:var(--color-secondary);color:var(--color-secondary-content);font-size:.75rem;line-height:1rem;white-space:nowrap;pointer-events:none;opacity:0;transition:opacity .15s;z-index:50}.mn-rich-text-editor .mn-rte-tooltip[data-tip]:hover:after,.mn-rich-text-editor .mn-rte-tooltip[data-tip]:focus-visible:after{opacity:1}\n"] }]
|
|
10957
|
+
}], ctorParameters: () => [], propDecorators: { content: [{ type: i0.Input, args: [{ isSignal: true, alias: "content", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], toolbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolbar", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], labelKeys: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelKeys", required: false }] }], editorClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "editorClass", required: false }] }], contentChange: [{ type: i0.Output, args: ["contentChange"] }], editorHost: [{ type: i0.ViewChild, args: ['editorHost', { isSignal: true }] }] } });
|
|
10958
|
+
|
|
10528
10959
|
const mnIconVariants = tv({
|
|
10529
10960
|
base: 'inline-flex shrink-0',
|
|
10530
10961
|
variants: {
|
|
@@ -11147,5 +11578,5 @@ function enableMnPreviewMode(configService, langService, allowedOrigins) {
|
|
|
11147
11578
|
* Generated bundle index. Do not edit.
|
|
11148
11579
|
*/
|
|
11149
11580
|
|
|
11150
|
-
export { API_BASE_URL, ActionStyle, BackdropMode, BaseModalBuilder, CALENDAR_CONFIG, CALENDAR_DATE_FORMATTER, CalendarDayComponent, CalendarEventComponent, CalendarEventDefaultComponent, CalendarEventLayoutService, CalendarMonthComponent, CalendarUtility, CalendarView, CalendarViewComponent, CalendarWeekComponent, CloseMode, ColumnSortType, ConfirmationModalBuilder, ConfirmationTone, CrudService, CustomModalBuilder, DEFAULT_CALENDAR_CONFIG, DEFAULT_MN_ALERT_CONFIG, DefaultCalendarDateFormatter, FieldAppearance, FieldKind, FormLayoutMode, FormModalBuilder, KeyboardMode, MN_ALERT_CONFIG, MN_CALENDAR_COMPONENT_NAME, MN_CALENDAR_CONFIG, MN_CHECKBOX_CONFIG, MN_DATETIME_CONFIG, MN_HAPTICS, MN_ICON_MAP, MN_INPUT_FIELD_CONFIG, MN_INSTANCE_ID, MN_LIB_DUAL_HORIZONTAL_IMAGE, MN_MODAL_ACTION_ICONS, MN_MULTI_SELECT_CONFIG, MN_SECTION_PATH, MN_SELECT_CONFIG, MN_TEXTAREA_CONFIG, MODAL_ACTION_ICON_SIZE, MODAL_ACTION_ICON_SIZE_SM, MnAlertOutletComponent, MnAlertService, MnAlertStore, MnBadge, MnButton, MnCheckbox, MnCollectionBase, MnCollectionPagination, MnCollectionState, MnConfigService, MnConfirmationBodyComponent, MnCustomBodyHostComponent, MnDateSelectorBar, MnDatetime, MnDualHorizontalImage, MnFileInput, MnFormBodyComponent, MnGrid, MnHiddenBelowDirective, MnHttpService, MnIcon, MnIconAttributes, MnInformationCard, MnInputField, MnInstanceDirective, MnLanguageService, MnList, MnModalRef, MnModalService, MnModalShellComponent, MnMultiSelect, MnSectionDirective, MnSelect, MnSelectableCollectionBase, MnShowAboveDirective, MnShowBelowDirective, MnSkeleton, MnTabComponent, MnTable, MnTextarea, MnTranslatePipe, MnWizardBodyComponent, ModalBuilder, ModalCloseReason, ModalIntent, ModalKind, ModalSize, NavigationDirection, OptionState, SelectionMode, StepBuilder, StepState, SubmitMode, UpcomingEventRowComponent, UpcomingEventsComponent, ValidationCode, ValidationStatus, WizardFlowMode, WizardModalBuilder, dateTimeAdapter, defaultFilterPredicate, defaultIconForStyle, defaultTextAdapter, emptyFilterValue, enableMnPreviewMode, isFilterValueActive, isTranslatable, matchesColumnFilter, mnAlertVariants, mnBadgeVariants, mnButtonVariants, mnCheckboxVariants, mnCheckboxWrapperVariants, mnDatetimeVariants, mnFileInputVariants, mnIconVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnSelectVariants, mnSkeletonVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnCalendarConfig, provideMnComponentConfig, provideMnConfig, provideMnLanguage, resolveCalendarConfig, resolveFilterableValue };
|
|
11581
|
+
export { API_BASE_URL, ActionStyle, BackdropMode, BaseModalBuilder, CALENDAR_CONFIG, CALENDAR_DATE_FORMATTER, CalendarDayComponent, CalendarEventComponent, CalendarEventDefaultComponent, CalendarEventLayoutService, CalendarMonthComponent, CalendarUtility, CalendarView, CalendarViewComponent, CalendarWeekComponent, CloseMode, ColumnSortType, ConfirmationModalBuilder, ConfirmationTone, CrudService, CustomModalBuilder, DEFAULT_CALENDAR_CONFIG, DEFAULT_MN_ALERT_CONFIG, DefaultCalendarDateFormatter, FieldAppearance, FieldKind, FormLayoutMode, FormModalBuilder, KeyboardMode, MN_ALERT_CONFIG, MN_CALENDAR_COMPONENT_NAME, MN_CALENDAR_CONFIG, MN_CHECKBOX_CONFIG, MN_DATETIME_CONFIG, MN_HAPTICS, MN_ICON_MAP, MN_INPUT_FIELD_CONFIG, MN_INSTANCE_ID, MN_LIB_DUAL_HORIZONTAL_IMAGE, MN_MODAL_ACTION_ICONS, MN_MULTI_SELECT_CONFIG, MN_SECTION_PATH, MN_SELECT_CONFIG, MN_TEXTAREA_CONFIG, MODAL_ACTION_ICON_SIZE, MODAL_ACTION_ICON_SIZE_SM, MnAlertOutletComponent, MnAlertService, MnAlertStore, MnBadge, MnButton, MnCheckbox, MnCollectionBase, MnCollectionPagination, MnCollectionState, MnConfigService, MnConfirmationBodyComponent, MnCustomBodyHostComponent, MnDateSelectorBar, MnDatetime, MnDualHorizontalImage, MnFileInput, MnFormBodyComponent, MnGrid, MnHiddenBelowDirective, MnHttpService, MnIcon, MnIconAttributes, MnInformationCard, MnInputField, MnInstanceDirective, MnLanguageService, MnList, MnModalRef, MnModalService, MnModalShellComponent, MnMultiSelect, MnRichTextEditor, MnSectionDirective, MnSelect, MnSelectableCollectionBase, MnShowAboveDirective, MnShowBelowDirective, MnSkeleton, MnTabComponent, MnTable, MnTextarea, MnTranslatePipe, MnWizardBodyComponent, ModalBuilder, ModalCloseReason, ModalIntent, ModalKind, ModalSize, NavigationDirection, OptionState, SelectionMode, StepBuilder, StepState, SubmitMode, UpcomingEventRowComponent, UpcomingEventsComponent, ValidationCode, ValidationStatus, WizardFlowMode, WizardModalBuilder, dateTimeAdapter, defaultFilterPredicate, defaultIconForStyle, defaultTextAdapter, emptyFilterValue, enableMnPreviewMode, isFilterValueActive, isTranslatable, matchesColumnFilter, mnAlertVariants, mnBadgeVariants, mnButtonVariants, mnCheckboxVariants, mnCheckboxWrapperVariants, mnDatetimeVariants, mnFileInputVariants, mnIconVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnSelectVariants, mnSkeletonVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnCalendarConfig, provideMnComponentConfig, provideMnConfig, provideMnLanguage, resolveCalendarConfig, resolveFilterableValue };
|
|
11151
11582
|
//# sourceMappingURL=mn-angular-lib.mjs.map
|