mn-angular-lib 1.0.155 → 1.0.156
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.
|
@@ -11,6 +11,7 @@ import * as i1$1 from '@angular/forms';
|
|
|
11
11
|
import { NgControl, Validators, FormsModule, NG_VALUE_ACCESSOR, FormBuilder, ReactiveFormsModule } from '@angular/forms';
|
|
12
12
|
import JSON5 from 'json5';
|
|
13
13
|
import { LucideFile, LucideImagePlus, LucideTrash2, LucideUpload, LucideX, LucideCalendarDays, LucideChevronLeft, LucideChevronRight, LucideChevronDown, LucideEllipsisVertical, LucideSearchX, LucideDynamicIcon, LucideArrowLeft, LucideArrowRight, LucideCheck, LucideInbox, LucideFilter, LucideFunnel, LucideTriangleAlert, LucideCircleAlert } from '@lucide/angular';
|
|
14
|
+
import { Router, ActivatedRoute } from '@angular/router';
|
|
14
15
|
import { DomSanitizer } from '@angular/platform-browser';
|
|
15
16
|
|
|
16
17
|
// projects/mn-angular-lib/src/lib/mn-mn-alert/mn-mn-alert.tokens.ts
|
|
@@ -5460,11 +5461,11 @@ class MnKeyboard {
|
|
|
5460
5461
|
this.valueChange.emit(clamped);
|
|
5461
5462
|
}
|
|
5462
5463
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnKeyboard, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5463
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnKeyboard, isStandalone: true, selector: "mn-keyboard", inputs: { value: "value", layout: "layout", presentation: "presentation", labels: "labels", uppercase: "uppercase", allowSpace: "allowSpace", showSubmit: "showSubmit", submitDisabled: "submitDisabled", maxLength: "maxLength", sheetMaxHeightVh: "sheetMaxHeightVh" }, outputs: { valueChange: "valueChange", submitted: "submitted", dismissed: "dismissed" }, host: { properties: { "class": "this.hostClasses" } }, ngImport: i0, template: "<!-- The key block. Rendered directly for `inline`, or projected into the sheet below. -->\n<ng-template #keys>\n <div [attr.aria-label]=\"labels.keyboard || null\" class=\"mn-keyboard-keys flex flex-col gap-2 p-3\" role=\"group\">\n @for (row of rows; track $index) {\n <div class=\"flex justify-center gap-2\">\n @for (key of row; track key) {\n <button\n (click)=\"press(key)\"\n class=\"mn-keyboard-key flex-1 rounded-lg bg-base-
|
|
5464
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnKeyboard, isStandalone: true, selector: "mn-keyboard", inputs: { value: "value", layout: "layout", presentation: "presentation", labels: "labels", uppercase: "uppercase", allowSpace: "allowSpace", showSubmit: "showSubmit", submitDisabled: "submitDisabled", maxLength: "maxLength", sheetMaxHeightVh: "sheetMaxHeightVh" }, outputs: { valueChange: "valueChange", submitted: "submitted", dismissed: "dismissed" }, host: { properties: { "class": "this.hostClasses" } }, ngImport: i0, template: "<!-- The key block. Rendered directly for `inline`, or projected into the sheet below. -->\n<ng-template #keys>\n <div [attr.aria-label]=\"labels.keyboard || null\" class=\"mn-keyboard-keys flex flex-col gap-2 p-3\" role=\"group\">\n @for (row of rows; track $index) {\n <div class=\"flex justify-center gap-2\">\n @for (key of row; track key) {\n <button\n (click)=\"press(key)\"\n class=\"mn-keyboard-key flex-1 rounded-lg border border-base-300 bg-base-100 py-4 text-xl font-semibold text-base-content shadow-sm transition-colors hover:bg-base-200 active:bg-base-300\"\n type=\"button\"\n >\n {{ key }}\n </button>\n }\n </div>\n }\n\n <div class=\"flex justify-center gap-2\">\n <button\n (click)=\"backspace()\"\n [attr.aria-label]=\"labels.backspace\"\n class=\"mn-keyboard-key mn-keyboard-key-action flex-1 rounded-lg border border-base-300 bg-base-200 py-4 text-base font-medium text-base-content shadow-sm transition-colors hover:bg-base-300\"\n type=\"button\"\n >\n {{ labels.backspace }}\n </button>\n\n @if (spaceVisible) {\n <button\n (click)=\"pressSpace()\"\n [attr.aria-label]=\"labels.space\"\n class=\"mn-keyboard-key mn-keyboard-key-space flex-1 rounded-lg border border-base-300 bg-base-200 py-4 text-base font-medium text-base-content shadow-sm transition-colors hover:bg-base-300\"\n style=\"flex-grow: 3\"\n type=\"button\"\n >\n {{ labels.space }}\n </button>\n }\n\n <button\n (click)=\"clear()\"\n [attr.aria-label]=\"labels.clear\"\n class=\"mn-keyboard-key mn-keyboard-key-action flex-1 rounded-lg border border-base-300 bg-base-200 py-4 text-base font-medium text-base-content shadow-sm transition-colors hover:bg-base-300\"\n type=\"button\"\n >\n {{ labels.clear }}\n </button>\n\n @if (showSubmit) {\n <button\n (click)=\"submit()\"\n [attr.aria-label]=\"labels.submit\"\n [disabled]=\"submitDisabled\"\n class=\"mn-keyboard-key mn-keyboard-key-submit flex-1 rounded-lg bg-primary py-4 text-base font-semibold text-primary-content shadow-sm transition-colors hover:opacity-90 disabled:opacity-50\"\n style=\"flex-grow: 2\"\n type=\"button\"\n >\n {{ labels.submit }}\n </button>\n }\n </div>\n </div>\n</ng-template>\n\n@if (presentation === 'sheet') {\n <mn-bottom-sheet\n (dismiss)=\"onDismiss()\"\n [ariaLabel]=\"labels.keyboard || undefined\"\n [maxHeightVh]=\"sheetMaxHeightVh\"\n >\n <ng-container [ngTemplateOutlet]=\"keys\"></ng-container>\n </mn-bottom-sheet>\n} @else {\n <ng-container [ngTemplateOutlet]=\"keys\"></ng-container>\n}\n", styles: [":host{display:block}.mn-keyboard-key{-webkit-tap-highlight-color:transparent;-webkit-user-select:none;user-select:none;touch-action:manipulation}\n"], dependencies: [{ kind: "component", type: MnBottomSheet, selector: "mn-bottom-sheet", inputs: ["showBackdrop", "showGrabber", "dismissible", "minHeightPx", "maxHeightVh", "containerClass", "ariaLabel", "ariaLabelledby", "growWithKeyboard", "dismissGuard"], outputs: ["dismiss"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
|
|
5464
5465
|
}
|
|
5465
5466
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnKeyboard, decorators: [{
|
|
5466
5467
|
type: Component,
|
|
5467
|
-
args: [{ selector: 'mn-keyboard', standalone: true, imports: [MnBottomSheet, NgTemplateOutlet], template: "<!-- The key block. Rendered directly for `inline`, or projected into the sheet below. -->\n<ng-template #keys>\n <div [attr.aria-label]=\"labels.keyboard || null\" class=\"mn-keyboard-keys flex flex-col gap-2 p-3\" role=\"group\">\n @for (row of rows; track $index) {\n <div class=\"flex justify-center gap-2\">\n @for (key of row; track key) {\n <button\n (click)=\"press(key)\"\n class=\"mn-keyboard-key flex-1 rounded-lg bg-base-
|
|
5468
|
+
args: [{ selector: 'mn-keyboard', standalone: true, imports: [MnBottomSheet, NgTemplateOutlet], template: "<!-- The key block. Rendered directly for `inline`, or projected into the sheet below. -->\n<ng-template #keys>\n <div [attr.aria-label]=\"labels.keyboard || null\" class=\"mn-keyboard-keys flex flex-col gap-2 p-3\" role=\"group\">\n @for (row of rows; track $index) {\n <div class=\"flex justify-center gap-2\">\n @for (key of row; track key) {\n <button\n (click)=\"press(key)\"\n class=\"mn-keyboard-key flex-1 rounded-lg border border-base-300 bg-base-100 py-4 text-xl font-semibold text-base-content shadow-sm transition-colors hover:bg-base-200 active:bg-base-300\"\n type=\"button\"\n >\n {{ key }}\n </button>\n }\n </div>\n }\n\n <div class=\"flex justify-center gap-2\">\n <button\n (click)=\"backspace()\"\n [attr.aria-label]=\"labels.backspace\"\n class=\"mn-keyboard-key mn-keyboard-key-action flex-1 rounded-lg border border-base-300 bg-base-200 py-4 text-base font-medium text-base-content shadow-sm transition-colors hover:bg-base-300\"\n type=\"button\"\n >\n {{ labels.backspace }}\n </button>\n\n @if (spaceVisible) {\n <button\n (click)=\"pressSpace()\"\n [attr.aria-label]=\"labels.space\"\n class=\"mn-keyboard-key mn-keyboard-key-space flex-1 rounded-lg border border-base-300 bg-base-200 py-4 text-base font-medium text-base-content shadow-sm transition-colors hover:bg-base-300\"\n style=\"flex-grow: 3\"\n type=\"button\"\n >\n {{ labels.space }}\n </button>\n }\n\n <button\n (click)=\"clear()\"\n [attr.aria-label]=\"labels.clear\"\n class=\"mn-keyboard-key mn-keyboard-key-action flex-1 rounded-lg border border-base-300 bg-base-200 py-4 text-base font-medium text-base-content shadow-sm transition-colors hover:bg-base-300\"\n type=\"button\"\n >\n {{ labels.clear }}\n </button>\n\n @if (showSubmit) {\n <button\n (click)=\"submit()\"\n [attr.aria-label]=\"labels.submit\"\n [disabled]=\"submitDisabled\"\n class=\"mn-keyboard-key mn-keyboard-key-submit flex-1 rounded-lg bg-primary py-4 text-base font-semibold text-primary-content shadow-sm transition-colors hover:opacity-90 disabled:opacity-50\"\n style=\"flex-grow: 2\"\n type=\"button\"\n >\n {{ labels.submit }}\n </button>\n }\n </div>\n </div>\n</ng-template>\n\n@if (presentation === 'sheet') {\n <mn-bottom-sheet\n (dismiss)=\"onDismiss()\"\n [ariaLabel]=\"labels.keyboard || undefined\"\n [maxHeightVh]=\"sheetMaxHeightVh\"\n >\n <ng-container [ngTemplateOutlet]=\"keys\"></ng-container>\n </mn-bottom-sheet>\n} @else {\n <ng-container [ngTemplateOutlet]=\"keys\"></ng-container>\n}\n", styles: [":host{display:block}.mn-keyboard-key{-webkit-tap-highlight-color:transparent;-webkit-user-select:none;user-select:none;touch-action:manipulation}\n"] }]
|
|
5468
5469
|
}], propDecorators: { value: [{
|
|
5469
5470
|
type: Input
|
|
5470
5471
|
}], layout: [{
|
|
@@ -10991,6 +10992,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
10991
10992
|
*/
|
|
10992
10993
|
class CalendarMonthComponent {
|
|
10993
10994
|
lang = inject(MnLanguageService);
|
|
10995
|
+
cdr = inject(ChangeDetectorRef);
|
|
10994
10996
|
/**
|
|
10995
10997
|
* Accessible name for this control. Resolved through the conventional
|
|
10996
10998
|
* `mnCalendar.monthView` key so an app can translate it, falling back to English when the
|
|
@@ -11026,16 +11028,22 @@ class CalendarMonthComponent {
|
|
|
11026
11028
|
this.weekdayLabels = resolved.shortDayNames;
|
|
11027
11029
|
this.moreEventsLabel = resolved.moreEventsLabel;
|
|
11028
11030
|
this.buildMonth();
|
|
11031
|
+
// Both subscriptions mark the view: the grid is rebuilt into plain fields, so in a zoneless
|
|
11032
|
+
// app nothing else tells Angular this component has to be re-rendered. Without it a month
|
|
11033
|
+
// whose events arrive from a stream — a fetch, a parent seeding its list — stays blank until
|
|
11034
|
+
// some unrelated interaction happens to trigger change detection.
|
|
11029
11035
|
if (this.eventsChanged) {
|
|
11030
11036
|
this.eventsChanged.pipe(takeUntil(this.destroy$)).subscribe(events => {
|
|
11031
11037
|
this.events = events;
|
|
11032
11038
|
this.buildMonth();
|
|
11039
|
+
this.cdr.markForCheck();
|
|
11033
11040
|
});
|
|
11034
11041
|
}
|
|
11035
11042
|
if (this.focusDayChanged) {
|
|
11036
11043
|
this.focusDayChanged.pipe(takeUntil(this.destroy$)).subscribe(date => {
|
|
11037
11044
|
this.focusDay = date;
|
|
11038
11045
|
this.buildMonth();
|
|
11046
|
+
this.cdr.markForCheck();
|
|
11039
11047
|
});
|
|
11040
11048
|
}
|
|
11041
11049
|
}
|
|
@@ -11845,6 +11853,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
11845
11853
|
*/
|
|
11846
11854
|
class UpcomingEventsComponent {
|
|
11847
11855
|
lang = inject(MnLanguageService);
|
|
11856
|
+
cdr = inject(ChangeDetectorRef);
|
|
11848
11857
|
/**
|
|
11849
11858
|
* Accessible name for this control. Resolved through the conventional
|
|
11850
11859
|
* `mnCalendar.upcomingEvents` key so an app can translate it, falling back to English when the
|
|
@@ -11879,6 +11888,9 @@ class UpcomingEventsComponent {
|
|
|
11879
11888
|
const resolved = this.config ? resolveCalendarConfig(this.config) : { ...DEFAULT_CALENDAR_CONFIG };
|
|
11880
11889
|
this.title = resolved.upcomingEventsTitle;
|
|
11881
11890
|
this.noEventsMessage = resolved.noUpcomingEvents;
|
|
11891
|
+
// Marked because the list is a plain field: in a zoneless app a stream emission is not by
|
|
11892
|
+
// itself a reason for Angular to re-render, so the sidebar would keep showing the events it
|
|
11893
|
+
// was first given.
|
|
11882
11894
|
if (this.eventsChanged) {
|
|
11883
11895
|
this.eventsChanged.pipe(takeUntil(this.destroy$)).subscribe(events => {
|
|
11884
11896
|
const now = new Date();
|
|
@@ -11886,6 +11898,7 @@ class UpcomingEventsComponent {
|
|
|
11886
11898
|
.filter(e => e.endTime > now)
|
|
11887
11899
|
.sort((a, b) => a.startTime.getTime() - b.startTime.getTime())
|
|
11888
11900
|
.slice(0, 10);
|
|
11901
|
+
this.cdr.markForCheck();
|
|
11889
11902
|
});
|
|
11890
11903
|
}
|
|
11891
11904
|
}
|
|
@@ -11979,6 +11992,7 @@ class CalendarViewComponent {
|
|
|
11979
11992
|
mnConfigRef;
|
|
11980
11993
|
destroyRef = inject(DestroyRef);
|
|
11981
11994
|
lang = inject(MnLanguageService);
|
|
11995
|
+
cdr = inject(ChangeDetectorRef);
|
|
11982
11996
|
constructor() {
|
|
11983
11997
|
const mnConfig = inject(MN_CALENDAR_CONFIG, { optional: true });
|
|
11984
11998
|
const legacyConfig = inject(CALENDAR_CONFIG, { optional: true });
|
|
@@ -11993,9 +12007,12 @@ class CalendarViewComponent {
|
|
|
11993
12007
|
}
|
|
11994
12008
|
ngOnInit() {
|
|
11995
12009
|
this.rebuildFromConfig();
|
|
11996
|
-
// Re-resolve config when locale changes (supports $translate in mn-config).
|
|
12010
|
+
// Re-resolve config when locale changes (supports $translate in mn-config). Marked because
|
|
12011
|
+
// the labels it rebuilds are plain fields: a locale switch arrives on a stream, which in a
|
|
12012
|
+
// zoneless app is not by itself a reason for Angular to re-render the toolbar.
|
|
11997
12013
|
const sub = this.lang.locale$.pipe(skip(1)).subscribe(() => {
|
|
11998
12014
|
this.rebuildFromConfig();
|
|
12015
|
+
this.cdr.markForCheck();
|
|
11999
12016
|
});
|
|
12000
12017
|
this.destroyRef.onDestroy(() => sub.unsubscribe());
|
|
12001
12018
|
this.checkMobileView();
|
|
@@ -12200,11 +12217,59 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
12200
12217
|
|
|
12201
12218
|
/** Fallback number of skeleton tabs when no items are known and no count is given. */
|
|
12202
12219
|
const DEFAULT_SKELETON_TAB_COUNT = 3;
|
|
12220
|
+
/** Query parameter the active tab is mirrored in unless the data source names another. */
|
|
12221
|
+
const DEFAULT_TAB_URL_PARAM = 'tab';
|
|
12222
|
+
/** Key used for a label that slugs to nothing (e.g. punctuation only). */
|
|
12223
|
+
const FALLBACK_TAB_URL_KEY = 'tab';
|
|
12224
|
+
/**
|
|
12225
|
+
* Slugs a tab label into the value it takes in the URL: the last segment of a
|
|
12226
|
+
* translation key, kebab-cased. `matches.hub.tab.entrants` → `entrants`,
|
|
12227
|
+
* `members.tabMembers` → `tab-members`.
|
|
12228
|
+
* @param label - The tab's label or translation key.
|
|
12229
|
+
*/
|
|
12230
|
+
function tabUrlKey(label) {
|
|
12231
|
+
const segment = label.split('.').pop() ?? label;
|
|
12232
|
+
const slug = segment
|
|
12233
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
12234
|
+
.replace(/[^a-zA-Z0-9]+/g, '-')
|
|
12235
|
+
.replace(/^-+|-+$/g, '')
|
|
12236
|
+
.toLowerCase();
|
|
12237
|
+
return slug || FALLBACK_TAB_URL_KEY;
|
|
12238
|
+
}
|
|
12203
12239
|
/**
|
|
12204
12240
|
* Tab component that renders a horizontal tab bar.
|
|
12205
12241
|
* Supports translation keys for labels via MnTranslatePipe.
|
|
12242
|
+
*
|
|
12243
|
+
* The active tab is mirrored in the URL query string by default, so a reload,
|
|
12244
|
+
* a back button or a shared link lands on the same tab; see
|
|
12245
|
+
* {@link MnTabDataSource.urlParam} to rename that parameter or switch it off.
|
|
12206
12246
|
*/
|
|
12207
12247
|
class MnTabComponent {
|
|
12248
|
+
/**
|
|
12249
|
+
* Router the active tab is written to. Optional: a tab bar used outside a
|
|
12250
|
+
* routed application still works, it just has no URL to mirror into.
|
|
12251
|
+
*/
|
|
12252
|
+
router = inject(Router, { optional: true });
|
|
12253
|
+
/** Route the tab value is read back from; absent for the same reason as {@link router}. */
|
|
12254
|
+
route = inject(ActivatedRoute, { optional: true });
|
|
12255
|
+
/** Watches the URL so a deep link, a back button or an in-app link moves the tab bar. */
|
|
12256
|
+
queryParamsSub;
|
|
12257
|
+
/**
|
|
12258
|
+
* URL keys of the current items, memoised on the items array so a slug is
|
|
12259
|
+
* computed once per tab set rather than on every change-detection pass.
|
|
12260
|
+
*/
|
|
12261
|
+
urlKeyCache;
|
|
12262
|
+
/** URL key of {@link currentActive}, so a rebuilt tab set can be recognised as the same tab. */
|
|
12263
|
+
currentKey;
|
|
12264
|
+
/**
|
|
12265
|
+
* Key of a selection whose URL write has not landed yet. Navigation is
|
|
12266
|
+
* asynchronous, so a consumer that rebuilds its tabs in response to the click
|
|
12267
|
+
* can be resolved against a URL that still names the previous tab; until the
|
|
12268
|
+
* write completes, this is the truth about what the user picked.
|
|
12269
|
+
*/
|
|
12270
|
+
pendingKey;
|
|
12271
|
+
/** Set on destroy so a deferred restore can't announce a tab nobody is showing. */
|
|
12272
|
+
destroyed = false;
|
|
12208
12273
|
/** The horizontally-scrolling wrapper the edge fade is painted onto. */
|
|
12209
12274
|
scrollContainer;
|
|
12210
12275
|
/** The tab row; queried for the active tab so the indicator can measure it. */
|
|
@@ -12268,6 +12333,16 @@ class MnTabComponent {
|
|
|
12268
12333
|
(this.dataSource.items.length || DEFAULT_SKELETON_TAB_COUNT);
|
|
12269
12334
|
return Array.from({ length: count }, (_, index) => index);
|
|
12270
12335
|
}
|
|
12336
|
+
constructor() {
|
|
12337
|
+
// The URL is a second source of truth for the selection: a deep link, an
|
|
12338
|
+
// in-app link into another tab of the page already on screen, or the back
|
|
12339
|
+
// button all change it without a click landing on this component.
|
|
12340
|
+
this.queryParamsSub = this.route?.queryParamMap.subscribe((params) => {
|
|
12341
|
+
const param = this.urlParam();
|
|
12342
|
+
if (param)
|
|
12343
|
+
this.activateUrlKey(params.get(param));
|
|
12344
|
+
});
|
|
12345
|
+
}
|
|
12271
12346
|
/**
|
|
12272
12347
|
* Re-resolves the active tab on every change-detection pass.
|
|
12273
12348
|
*
|
|
@@ -12307,6 +12382,8 @@ class MnTabComponent {
|
|
|
12307
12382
|
this.updateIndicator(false);
|
|
12308
12383
|
}
|
|
12309
12384
|
ngOnDestroy() {
|
|
12385
|
+
this.destroyed = true;
|
|
12386
|
+
this.queryParamsSub?.unsubscribe();
|
|
12310
12387
|
this.resizeObserver?.disconnect();
|
|
12311
12388
|
if (this.indicatorFrame !== undefined)
|
|
12312
12389
|
cancelAnimationFrame(this.indicatorFrame);
|
|
@@ -12340,16 +12417,28 @@ class MnTabComponent {
|
|
|
12340
12417
|
el.style.setProperty('-webkit-mask-image', mask);
|
|
12341
12418
|
}
|
|
12342
12419
|
/**
|
|
12343
|
-
* Sets the given tab item as active, invoking deactivate/activate callbacks
|
|
12420
|
+
* Sets the given tab item as active, invoking deactivate/activate callbacks,
|
|
12421
|
+
* and records the selection in the URL so it survives a reload or a share.
|
|
12344
12422
|
* @param item - The tab item to activate.
|
|
12345
12423
|
*/
|
|
12346
12424
|
setActive(item) {
|
|
12347
12425
|
if (this.currentActive === item) {
|
|
12348
12426
|
return;
|
|
12349
12427
|
}
|
|
12428
|
+
this.activate(item);
|
|
12429
|
+
this.writeUrl(item);
|
|
12430
|
+
}
|
|
12431
|
+
/**
|
|
12432
|
+
* Moves the selection to `item` and tells the consumer about it: the
|
|
12433
|
+
* deactivate/activate/emit sequence a click produces, shared by the click
|
|
12434
|
+
* path and the URL-driven ones (deep link, back button), which owe the
|
|
12435
|
+
* consumer the same notifications.
|
|
12436
|
+
* @param item - The tab item to activate.
|
|
12437
|
+
*/
|
|
12438
|
+
activate(item) {
|
|
12350
12439
|
this.currentActive?.onDeactivate?.();
|
|
12351
12440
|
item.onClick?.();
|
|
12352
|
-
this.
|
|
12441
|
+
this.select(item);
|
|
12353
12442
|
this.activeChange.emit(item);
|
|
12354
12443
|
// Slide the underline to the new tab. Measure on the next frame, after
|
|
12355
12444
|
// change detection has applied the active tab's `font-bold` (which widens
|
|
@@ -12425,14 +12514,16 @@ class MnTabComponent {
|
|
|
12425
12514
|
}
|
|
12426
12515
|
/**
|
|
12427
12516
|
* Ensures {@link currentActive} references a tab that still exists in the data
|
|
12428
|
-
* source,
|
|
12429
|
-
*
|
|
12517
|
+
* source, preferring the tab named in the URL and falling back to the
|
|
12518
|
+
* configured default tab when the current selection is missing or stale
|
|
12519
|
+
* (e.g. after the items array is replaced).
|
|
12430
12520
|
*/
|
|
12431
12521
|
syncActiveTab() {
|
|
12432
12522
|
const items = this.dataSource?.items;
|
|
12433
12523
|
if (!items || items.length === 0) {
|
|
12434
12524
|
if (this.currentActive !== undefined) {
|
|
12435
12525
|
this.currentActive = undefined;
|
|
12526
|
+
this.currentKey = undefined;
|
|
12436
12527
|
this.scheduleIndicator(false);
|
|
12437
12528
|
}
|
|
12438
12529
|
return;
|
|
@@ -12442,9 +12533,131 @@ class MnTabComponent {
|
|
|
12442
12533
|
}
|
|
12443
12534
|
const defaultIndex = this.dataSource.defaultActive;
|
|
12444
12535
|
const index = defaultIndex >= 0 && defaultIndex < items.length ? defaultIndex : 0;
|
|
12445
|
-
|
|
12536
|
+
const fallback = items[index];
|
|
12537
|
+
const restored = this.itemFromUrl(items);
|
|
12538
|
+
const previousKey = this.currentKey;
|
|
12539
|
+
this.select(restored ?? fallback);
|
|
12446
12540
|
// Selection resolved from data (not a user click): snap, don't slide.
|
|
12447
12541
|
this.scheduleIndicator(false);
|
|
12542
|
+
if (restored && restored !== fallback && this.currentKey !== previousKey) {
|
|
12543
|
+
// The URL asks for a tab the consumer has not rendered, so this selection
|
|
12544
|
+
// has to be announced like a click's would be — but only the first time,
|
|
12545
|
+
// or a consumer that rebuilds its items array would re-run the tab's
|
|
12546
|
+
// callbacks on every rebuild. Deferred out of the change-detection pass
|
|
12547
|
+
// that resolved it: the consumer will flip its own state in response, and
|
|
12548
|
+
// doing that mid-pass writes to bindings that have already been checked.
|
|
12549
|
+
queueMicrotask(() => this.announceRestored(restored));
|
|
12550
|
+
}
|
|
12551
|
+
}
|
|
12552
|
+
/**
|
|
12553
|
+
* Records `item` as the selection, remembering its URL key so the same tab is
|
|
12554
|
+
* recognised after the consumer rebuilds the items array.
|
|
12555
|
+
* @param item - The newly selected tab.
|
|
12556
|
+
*/
|
|
12557
|
+
select(item) {
|
|
12558
|
+
const items = this.dataSource.items;
|
|
12559
|
+
this.currentActive = item;
|
|
12560
|
+
this.currentKey = this.urlKeys(items)[items.indexOf(item)];
|
|
12561
|
+
}
|
|
12562
|
+
/**
|
|
12563
|
+
* Runs the restored tab's callbacks a change-detection pass later, unless the
|
|
12564
|
+
* selection moved on in the meantime (a click, or another tab set arriving).
|
|
12565
|
+
* @param item - The tab restored from the URL.
|
|
12566
|
+
*/
|
|
12567
|
+
announceRestored(item) {
|
|
12568
|
+
if (this.destroyed || this.currentActive !== item)
|
|
12569
|
+
return;
|
|
12570
|
+
item.onClick?.();
|
|
12571
|
+
this.activeChange.emit(item);
|
|
12572
|
+
}
|
|
12573
|
+
/**
|
|
12574
|
+
* Activates the tab a URL value names, when it is not the tab already on
|
|
12575
|
+
* screen. Values naming no known tab are ignored: another tab bar on the page
|
|
12576
|
+
* may own that parameter, and a stale link should leave the default standing.
|
|
12577
|
+
* @param key - The value read from the query parameter, if any.
|
|
12578
|
+
*/
|
|
12579
|
+
activateUrlKey(key) {
|
|
12580
|
+
const items = this.dataSource?.items;
|
|
12581
|
+
if (!key || !items?.length)
|
|
12582
|
+
return;
|
|
12583
|
+
const item = items[this.urlKeys(items).indexOf(key)];
|
|
12584
|
+
if (!item || item === this.currentActive)
|
|
12585
|
+
return;
|
|
12586
|
+
this.activate(item);
|
|
12587
|
+
}
|
|
12588
|
+
/**
|
|
12589
|
+
* The query parameter this tab bar mirrors into, or undefined when there is
|
|
12590
|
+
* nothing to mirror into (no router) or the consumer switched it off.
|
|
12591
|
+
*/
|
|
12592
|
+
urlParam() {
|
|
12593
|
+
if (!this.router || !this.route)
|
|
12594
|
+
return undefined;
|
|
12595
|
+
const param = this.dataSource?.urlParam ?? DEFAULT_TAB_URL_PARAM;
|
|
12596
|
+
return param === false || param === '' ? undefined : param;
|
|
12597
|
+
}
|
|
12598
|
+
/**
|
|
12599
|
+
* The tab the current URL asks for, if it names one of `items`.
|
|
12600
|
+
* @param items - The tab set to resolve the URL value against.
|
|
12601
|
+
*/
|
|
12602
|
+
itemFromUrl(items) {
|
|
12603
|
+
const param = this.urlParam();
|
|
12604
|
+
if (!param)
|
|
12605
|
+
return undefined;
|
|
12606
|
+
const key = this.pendingKey ?? this.route?.snapshot.queryParamMap.get(param);
|
|
12607
|
+
if (!key)
|
|
12608
|
+
return undefined;
|
|
12609
|
+
const index = this.urlKeys(items).indexOf(key);
|
|
12610
|
+
return index === -1 ? undefined : items[index];
|
|
12611
|
+
}
|
|
12612
|
+
/**
|
|
12613
|
+
* Records the active tab in the query string, replacing the current history
|
|
12614
|
+
* entry: switching tabs is not a navigation to walk back through, and back
|
|
12615
|
+
* should leave the page rather than retrace its tabs.
|
|
12616
|
+
* @param item - The tab that just became active.
|
|
12617
|
+
*/
|
|
12618
|
+
writeUrl(item) {
|
|
12619
|
+
const param = this.urlParam();
|
|
12620
|
+
if (!param || !this.router)
|
|
12621
|
+
return;
|
|
12622
|
+
const items = this.dataSource.items;
|
|
12623
|
+
const key = this.urlKeys(items)[items.indexOf(item)];
|
|
12624
|
+
if (!key)
|
|
12625
|
+
return;
|
|
12626
|
+
this.pendingKey = key;
|
|
12627
|
+
// No path commands and no `relativeTo`, so only the query string changes.
|
|
12628
|
+
// That holds wherever the tab bar sits, including a modal body, which has
|
|
12629
|
+
// no route of its own to be relative to.
|
|
12630
|
+
void this.router
|
|
12631
|
+
.navigate([], {
|
|
12632
|
+
queryParams: { [param]: key },
|
|
12633
|
+
queryParamsHandling: 'merge',
|
|
12634
|
+
replaceUrl: true,
|
|
12635
|
+
})
|
|
12636
|
+
.catch(() => undefined)
|
|
12637
|
+
.then(() => {
|
|
12638
|
+
// Only clear our own write; a later click already owns the pending key.
|
|
12639
|
+
if (this.pendingKey === key)
|
|
12640
|
+
this.pendingKey = undefined;
|
|
12641
|
+
});
|
|
12642
|
+
}
|
|
12643
|
+
/**
|
|
12644
|
+
* The URL key of every tab, in item order: the item's `id`, else a slug of
|
|
12645
|
+
* its label. Repeats are numbered so each tab still round-trips through the
|
|
12646
|
+
* URL; give such tabs an explicit `id` to choose the value yourself.
|
|
12647
|
+
* @param items - The tab set to key.
|
|
12648
|
+
*/
|
|
12649
|
+
urlKeys(items) {
|
|
12650
|
+
if (this.urlKeyCache?.items === items)
|
|
12651
|
+
return this.urlKeyCache.keys;
|
|
12652
|
+
const used = new Map();
|
|
12653
|
+
const keys = items.map((item) => {
|
|
12654
|
+
const base = item.id ?? tabUrlKey(item.label);
|
|
12655
|
+
const taken = used.get(base) ?? 0;
|
|
12656
|
+
used.set(base, taken + 1);
|
|
12657
|
+
return taken === 0 ? base : `${base}-${taken + 1}`;
|
|
12658
|
+
});
|
|
12659
|
+
this.urlKeyCache = { items, keys };
|
|
12660
|
+
return keys;
|
|
12448
12661
|
}
|
|
12449
12662
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTabComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
12450
12663
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTabComponent, isStandalone: true, selector: "mn-tab", inputs: { dataSource: "dataSource", scrollable: "scrollable", justified: "justified" }, outputs: { activeChange: "activeChange" }, viewQueries: [{ propertyName: "scrollContainer", first: true, predicate: ["scrollContainer"], descendants: true }, { propertyName: "tabList", first: true, predicate: ["tabList"], descendants: true }, { propertyName: "indicator", first: true, predicate: ["indicator"], descendants: true }], ngImport: i0, template: "<div class=\"mb-10\">\n <div\n #scrollContainer\n (scroll)=\"updateEdgeFades()\"\n class=\"flex justify-start scrollbar-hide\"\n [class.overflow-x-auto]=\"scrollable\"\n [class.overflow-y-hidden]=\"scrollable\"\n >\n <div\n #tabList\n role=\"tablist\"\n class=\"tabs relative flex flex-nowrap -mb-[1px] border-b border-base-300\"\n [class.w-full]=\"justified\"\n >\n <!--\n Shared sliding indicator: a single underline that travels to the active\n tab, rather than each tab flipping its own border on/off (which snaps).\n Pinned to left-0/top-auto so offsetLeft maps 1:1 regardless of the\n flex justify-content or the justified full-width layout; position and\n width are measured and set from TS. Sits on the same baseline as the\n hover ::after so hover \u2192 select reads as one continuous underline.\n -->\n <div\n #indicator\n aria-hidden=\"true\"\n class=\"pointer-events-none absolute left-0 top-auto bottom-0 h-[2px] w-0 bg-primary opacity-0 transition-[transform,width] duration-300 ease-out motion-reduce:transition-none\"\n ></div>\n @if (isLoadingState) {\n @for (i of skeletonTabs; track i) {\n <div\n [class.flex-1]=\"justified\"\n class=\"tab px-4 py-2 border-b-2 border-transparent flex items-center justify-center\"\n >\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '4.5rem', height: '1rem' }\"></mn-skeleton>\n </div>\n }\n } @else {\n @for (item of dataSource.items; track item.label) {\n <div\n (click)=\"setActive(item)\"\n (keyup.enter)=\"setActive(item)\"\n (keyup.space)=\"setActive(item)\"\n [attr.aria-selected]=\"currentActive === item\"\n [class.hover:after:scale-x-100]=\"currentActive !== item\"\n [class.flex-1]=\"justified\"\n [class.font-bold]=\"currentActive === item\"\n [class.text-base-content]=\"currentActive !== item\"\n [class.text-primary]=\"currentActive === item\"\n class=\"tab relative px-4 py-2 border-b-2 border-transparent cursor-pointer select-none transition-colors whitespace-nowrap text-center flex items-center gap-2 after:content-[''] after:absolute after:inset-x-0 after:-bottom-[2px] after:h-[2px] after:bg-primary/60 after:origin-center after:scale-x-0 after:transition-transform after:duration-300 after:ease-out\"\n role=\"tab\"\n tabindex=\"0\"\n >\n {{ item.label | mnTranslate }}\n @let badge = getBadge(item);\n @if (badge && badge > 0) {\n <span [data]=\"{ size: 'sm', color: 'accent', variant: 'fill' }\" mnBadge>{{ badge }}</span>\n }\n </div>\n }\n }\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: MnBadge, selector: "span[mnBadge]", inputs: ["data"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "pipe", type: MnTranslatePipe, name: "mnTranslate" }] });
|
|
@@ -12452,7 +12665,7 @@ class MnTabComponent {
|
|
|
12452
12665
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTabComponent, decorators: [{
|
|
12453
12666
|
type: Component,
|
|
12454
12667
|
args: [{ selector: 'mn-tab', standalone: true, imports: [MnTranslatePipe, CommonModule, MnBadge, MnSkeleton], template: "<div class=\"mb-10\">\n <div\n #scrollContainer\n (scroll)=\"updateEdgeFades()\"\n class=\"flex justify-start scrollbar-hide\"\n [class.overflow-x-auto]=\"scrollable\"\n [class.overflow-y-hidden]=\"scrollable\"\n >\n <div\n #tabList\n role=\"tablist\"\n class=\"tabs relative flex flex-nowrap -mb-[1px] border-b border-base-300\"\n [class.w-full]=\"justified\"\n >\n <!--\n Shared sliding indicator: a single underline that travels to the active\n tab, rather than each tab flipping its own border on/off (which snaps).\n Pinned to left-0/top-auto so offsetLeft maps 1:1 regardless of the\n flex justify-content or the justified full-width layout; position and\n width are measured and set from TS. Sits on the same baseline as the\n hover ::after so hover \u2192 select reads as one continuous underline.\n -->\n <div\n #indicator\n aria-hidden=\"true\"\n class=\"pointer-events-none absolute left-0 top-auto bottom-0 h-[2px] w-0 bg-primary opacity-0 transition-[transform,width] duration-300 ease-out motion-reduce:transition-none\"\n ></div>\n @if (isLoadingState) {\n @for (i of skeletonTabs; track i) {\n <div\n [class.flex-1]=\"justified\"\n class=\"tab px-4 py-2 border-b-2 border-transparent flex items-center justify-center\"\n >\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '4.5rem', height: '1rem' }\"></mn-skeleton>\n </div>\n }\n } @else {\n @for (item of dataSource.items; track item.label) {\n <div\n (click)=\"setActive(item)\"\n (keyup.enter)=\"setActive(item)\"\n (keyup.space)=\"setActive(item)\"\n [attr.aria-selected]=\"currentActive === item\"\n [class.hover:after:scale-x-100]=\"currentActive !== item\"\n [class.flex-1]=\"justified\"\n [class.font-bold]=\"currentActive === item\"\n [class.text-base-content]=\"currentActive !== item\"\n [class.text-primary]=\"currentActive === item\"\n class=\"tab relative px-4 py-2 border-b-2 border-transparent cursor-pointer select-none transition-colors whitespace-nowrap text-center flex items-center gap-2 after:content-[''] after:absolute after:inset-x-0 after:-bottom-[2px] after:h-[2px] after:bg-primary/60 after:origin-center after:scale-x-0 after:transition-transform after:duration-300 after:ease-out\"\n role=\"tab\"\n tabindex=\"0\"\n >\n {{ item.label | mnTranslate }}\n @let badge = getBadge(item);\n @if (badge && badge > 0) {\n <span [data]=\"{ size: 'sm', color: 'accent', variant: 'fill' }\" mnBadge>{{ badge }}</span>\n }\n </div>\n }\n }\n </div>\n </div>\n</div>\n" }]
|
|
12455
|
-
}], propDecorators: { scrollContainer: [{
|
|
12668
|
+
}], ctorParameters: () => [], propDecorators: { scrollContainer: [{
|
|
12456
12669
|
type: ViewChild,
|
|
12457
12670
|
args: ['scrollContainer']
|
|
12458
12671
|
}], tabList: [{
|