@propbinder/mobile-design 0.2.48 → 0.2.50
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/ng-package.json +24 -0
- package/package.json +3 -39
- package/src/animations/page-transitions.ts +165 -0
- package/src/assets/fonts/brockmann-mediumitalic-webfont.woff2 +0 -0
- package/src/assets/fonts/brockmann-regularitalic-webfont.woff2 +0 -0
- package/src/assets/fonts/brockmann-semibolditalic-webfont.woff2 +0 -0
- package/src/components/action-list-item/ds-mobile-action-list-item.ts +102 -0
- package/src/components/action-list-item/index.ts +2 -0
- package/src/components/app-icon/ds-app-icon.ts +133 -0
- package/src/components/app-icon/index.ts +2 -0
- package/src/components/attachment-preview/ds-mobile-attachment-preview.css +139 -0
- package/src/components/attachment-preview/ds-mobile-attachment-preview.ts +164 -0
- package/src/components/attachment-preview/index.ts +1 -0
- package/src/components/avatar-with-badge/ds-avatar-with-badge.ts +142 -0
- package/src/components/avatar-with-badge/index.ts +2 -0
- package/src/components/booking-modal/ds-mobile-booking-confirmation-wrapper.ts +71 -0
- package/src/components/booking-modal/ds-mobile-booking-modal.service.ts +121 -0
- package/src/components/booking-modal/ds-mobile-booking-modal.ts +598 -0
- package/src/components/booking-modal/ds-mobile-booking-summary.ts +161 -0
- package/src/components/booking-modal/index.ts +4 -0
- package/src/components/bottom-sheet/ds-mobile-actions-bottom-sheet.ts +266 -0
- package/src/components/bottom-sheet/ds-mobile-bottom-sheet-header.ts +146 -0
- package/src/components/bottom-sheet/ds-mobile-bottom-sheet-wrapper.ts +156 -0
- package/src/components/bottom-sheet/ds-mobile-bottom-sheet.css +101 -0
- package/src/components/bottom-sheet/ds-mobile-bottom-sheet.service.ts +169 -0
- package/src/components/bottom-sheet/ds-mobile-confirmation-sheet.ts +211 -0
- package/src/components/bottom-sheet/ds-mobile-post-create-bottom-sheet.ts +578 -0
- package/src/components/bottom-sheet/ds-mobile-profile-actions-sheet.ts +614 -0
- package/src/components/bottom-sheet/index.ts +8 -0
- package/src/components/bottom-sheet/modal-shadow-fix.ts +42 -0
- package/src/components/card-inline/ds-mobile-card-inline.ts +301 -0
- package/src/components/card-inline/index.ts +2 -0
- package/src/components/card-inline-banner/ds-mobile-card-inline-banner.ts +118 -0
- package/src/components/card-inline-banner/index.ts +1 -0
- package/src/components/card-inline-contact/ds-mobile-card-inline-contact.ts +120 -0
- package/src/components/card-inline-contact/index.ts +1 -0
- package/src/components/card-inline-file/ds-mobile-card-inline-file.ts +141 -0
- package/src/components/card-inline-file/index.ts +1 -0
- package/src/components/chat-modal/ds-mobile-chat-modal.css +159 -0
- package/src/components/chat-modal/ds-mobile-chat-modal.service.ts +105 -0
- package/src/components/chat-modal/ds-mobile-chat-modal.ts +918 -0
- package/src/components/chat-modal/index.ts +8 -0
- package/src/components/comment/ds-mobile-comment.ts +568 -0
- package/src/components/comment/index.ts +2 -0
- package/src/components/contact-list-item/ds-mobile-contact-list-item.ts +182 -0
- package/src/components/contact-list-item/index.ts +2 -0
- package/src/components/content/ds-mobile-content.ts +139 -0
- package/src/components/content/index.ts +2 -0
- package/src/components/dropdown/ds-mobile-dropdown.css +199 -0
- package/src/components/dropdown/ds-mobile-dropdown.ts +340 -0
- package/src/components/dropdown/index.ts +2 -0
- package/src/components/ds-mobile-tabs.css +407 -0
- package/src/components/ds-mobile-tabs.ts +216 -0
- package/src/components/empty-state/ds-mobile-empty-state.ts +120 -0
- package/src/components/empty-state/index.ts +2 -0
- package/src/components/fab/ds-mobile-fab.ts +315 -0
- package/src/components/fab/index.ts +1 -0
- package/src/components/facility-creation-modal/ds-mobile-facility-creation-confirmation-wrapper.ts +121 -0
- package/src/components/facility-creation-modal/ds-mobile-facility-creation-modal.css +189 -0
- package/src/components/facility-creation-modal/ds-mobile-facility-creation-modal.service.ts +135 -0
- package/src/components/facility-creation-modal/ds-mobile-facility-creation-modal.ts +656 -0
- package/src/components/facility-creation-modal/index.ts +9 -0
- package/src/components/facility-creation-modal/sheets/ds-mobile-access-sheet.ts +105 -0
- package/src/components/facility-creation-modal/sheets/ds-mobile-price-sheet.ts +188 -0
- package/src/components/facility-creation-modal/sheets/ds-mobile-when-can-book-sheet.ts +460 -0
- package/src/components/facility-creation-modal/sheets/ds-mobile-who-can-book-sheet.ts +134 -0
- package/src/components/facility-detail-modal/ds-mobile-facility-detail-modal.service.ts +69 -0
- package/src/components/facility-detail-modal/ds-mobile-facility-detail-modal.ts +379 -0
- package/src/components/facility-detail-modal/index.ts +2 -0
- package/src/components/file-attachment/ds-mobile-file-attachment.ts +164 -0
- package/src/components/file-attachment/index.ts +2 -0
- package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.css +214 -0
- package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.service.ts +84 -0
- package/src/components/handbook-detail-modal/ds-mobile-handbook-detail-modal.ts +424 -0
- package/src/components/handbook-detail-modal/index.ts +3 -0
- package/src/components/handbook-folder/ds-mobile-handbook-folder-mini.ts +175 -0
- package/src/components/handbook-folder/ds-mobile-handbook-folder.ts +533 -0
- package/src/components/handbook-folder/index.ts +4 -0
- package/src/components/header-content/ds-mobile-header-content.ts +222 -0
- package/src/components/header-content/index.ts +2 -0
- package/src/components/illustration/ds-mobile-illustration.ts +124 -0
- package/src/components/illustration/index.ts +2 -0
- package/src/components/index.ts +124 -0
- package/src/components/inline-photo/ds-mobile-inline-photo.ts +361 -0
- package/src/components/inline-photo/index.ts +1 -0
- package/src/components/inline-tabs/ds-mobile-inline-tabs.ts +132 -0
- package/src/components/inline-tabs/index.ts +2 -0
- package/src/components/interactive-list-item-booking/ds-mobile-interactive-list-item-booking.ts +350 -0
- package/src/components/interactive-list-item-booking/index.ts +1 -0
- package/src/components/interactive-list-item-inquiry/ds-mobile-interactive-list-item-inquiry.ts +321 -0
- package/src/components/interactive-list-item-inquiry/index.ts +2 -0
- package/src/components/interactive-list-item-message/ds-mobile-interactive-list-item-message.ts +237 -0
- package/src/components/interactive-list-item-message/index.ts +2 -0
- package/src/components/interactive-list-item-post/ds-mobile-interactive-list-item-post.ts +549 -0
- package/src/components/interactive-list-item-post/ds-mobile-post-pdf-attachment.ts +124 -0
- package/src/components/interactive-list-item-post/index.ts +13 -0
- package/src/components/lightbox/ds-mobile-lightbox-footer.ts +315 -0
- package/src/components/lightbox/ds-mobile-lightbox-header.ts +202 -0
- package/src/components/lightbox/ds-mobile-lightbox-image.ts +484 -0
- package/src/components/lightbox/ds-mobile-lightbox-pdf.css +377 -0
- package/src/components/lightbox/ds-mobile-lightbox-pdf.ts +374 -0
- package/src/components/lightbox/ds-mobile-lightbox.css +587 -0
- package/src/components/lightbox/ds-mobile-lightbox.service.ts +296 -0
- package/src/components/lightbox/ds-mobile-lightbox.ts +529 -0
- package/src/components/lightbox/index.ts +22 -0
- package/src/components/list-item/ds-mobile-list-item.ts +603 -0
- package/src/components/list-item/index.ts +2 -0
- package/src/components/list-item-static/ds-mobile-list-item-static.ts +133 -0
- package/src/components/list-item-static/index.ts +2 -0
- package/src/components/loader-overlay/ds-mobile-loader-overlay.css +49 -0
- package/src/components/loader-overlay/ds-mobile-loader-overlay.ts +77 -0
- package/src/components/loader-overlay/index.ts +1 -0
- package/src/components/logo/ds-logo.ts +95 -0
- package/src/components/logo/index.ts +2 -0
- package/src/components/message-bubble/ds-mobile-message-bubble.ts +633 -0
- package/src/components/message-bubble/index.ts +7 -0
- package/src/components/message-composer/ds-mobile-message-composer.ts +1146 -0
- package/src/components/message-composer/index.ts +7 -0
- package/src/components/modal/ds-mobile-modal.css +163 -0
- package/src/components/modal/ds-mobile-modal.service.ts +329 -0
- package/src/components/modal/index.ts +8 -0
- package/src/components/modal-base/ds-mobile-modal-base.css +378 -0
- package/src/components/modal-base/ds-mobile-modal-base.ts +261 -0
- package/src/components/modal-base/index.ts +2 -0
- package/src/components/new-inquiry-modal/ds-mobile-new-inquiry-modal.css +112 -0
- package/src/components/new-inquiry-modal/ds-mobile-new-inquiry-modal.service.ts +93 -0
- package/src/components/new-inquiry-modal/ds-mobile-new-inquiry-modal.ts +442 -0
- package/src/components/new-inquiry-modal/index.ts +4 -0
- package/src/components/offline-banner/ds-mobile-offline-banner.ts +135 -0
- package/src/components/offline-banner/index.ts +1 -0
- package/src/components/page-details/ds-mobile-page-details.css +83 -0
- package/src/components/page-details/ds-mobile-page-details.ts +282 -0
- package/src/components/page-details/index.ts +2 -0
- package/src/components/page-main/ds-mobile-page-main.css +68 -0
- package/src/components/page-main/ds-mobile-page-main.ts +421 -0
- package/src/components/page-main/index.ts +2 -0
- package/src/components/post-composer/ds-mobile-post-composer.ts +140 -0
- package/src/components/post-composer/index.ts +2 -0
- package/src/components/post-detail-modal/ds-mobile-post-detail-modal.css +390 -0
- package/src/components/post-detail-modal/ds-mobile-post-detail-modal.service.ts +108 -0
- package/src/components/post-detail-modal/ds-mobile-post-detail-modal.ts +722 -0
- package/src/components/post-detail-modal/index.ts +9 -0
- package/src/components/property-banner/ds-mobile-property-banner.ts +95 -0
- package/src/components/property-banner/index.ts +2 -0
- package/src/components/section/ds-mobile-section.ts +263 -0
- package/src/components/section/index.ts +2 -0
- package/src/components/shared/directives/index.ts +2 -0
- package/src/components/shared/directives/long-press.directive.ts +212 -0
- package/src/components/shared/index.ts +3 -0
- package/src/components/shared/mobile-modal-base.ts +457 -0
- package/src/components/shared/mobile-page-base.ts +204 -0
- package/src/components/swiper/ds-mobile-swiper-with-nav.ts +160 -0
- package/src/components/swiper/ds-mobile-swiper.ts +327 -0
- package/src/components/swiper/index.ts +3 -0
- package/src/components/system-message-banner/ds-mobile-system-message-banner.ts +129 -0
- package/src/components/system-message-banner/index.ts +2 -0
- package/src/components/tab-bar/ds-mobile-tab-bar.css +533 -0
- package/src/components/tab-bar/ds-mobile-tab-bar.ts +735 -0
- package/src/components/tab-bar/index.ts +2 -0
- package/src/components/tabs/ds-mobile-tabs.css +25 -0
- package/src/components/tabs/ds-mobile-tabs.ts +89 -0
- package/src/components/tabs/index.ts +2 -0
- package/src/components/text-input/ds-text-input.ts +287 -0
- package/src/components/text-input/index.ts +2 -0
- package/src/examples/booking.page.ts +434 -0
- package/src/examples/community.page.ts +776 -0
- package/src/examples/handbook.page.ts +324 -0
- package/src/examples/home.page.ts +347 -0
- package/src/examples/index.ts +12 -0
- package/src/examples/inquiries.example.ts +273 -0
- package/src/examples/inquiry-detail.example.css +189 -0
- package/src/examples/inquiry-detail.example.ts +415 -0
- package/src/examples/mobile-tabs-example.component.ts +208 -0
- package/src/examples/post-create.page.ts +311 -0
- package/src/examples/post-detail.page.ts +296 -0
- package/src/examples/sign-in.page.ts +291 -0
- package/src/examples/whitelabel-demo-modal.component.ts +1094 -0
- package/src/examples/whitelabel-demo-modal.service.ts +77 -0
- package/src/models/index.ts +7 -0
- package/src/models/post.model.ts +41 -0
- package/src/pages/community.page.ts +769 -0
- package/src/pages/handbook.page.ts +388 -0
- package/src/pages/home.page.ts +303 -0
- package/src/pages/index.ts +11 -0
- package/src/pages/inquiries.example.ts +273 -0
- package/src/pages/inquiry-detail.example.css +189 -0
- package/src/pages/inquiry-detail.example.ts +415 -0
- package/src/pages/mobile-tabs-example.component.ts +179 -0
- package/src/pages/post-create.page.ts +311 -0
- package/src/pages/post-detail.page.ts +296 -0
- package/src/pages/sign-in.page.ts +291 -0
- package/src/pages/whitelabel-demo-modal.component.ts +1094 -0
- package/src/pages/whitelabel-demo-modal.service.ts +77 -0
- package/src/public-api.ts +6 -0
- package/src/services/base-modal.service.ts +101 -0
- package/src/services/index.ts +11 -0
- package/src/services/posts.service.ts +542 -0
- package/src/services/tracking-permission.service.ts +88 -0
- package/src/services/user.service.ts +60 -0
- package/src/services/whitelabel.service.ts +675 -0
- package/{styles → src/styles}/ionic.css +25 -0
- package/tsconfig.lib.json +17 -0
- package/tsconfig.lib.prod.json +9 -0
- package/tsconfig.spec.json +13 -0
- package/fesm2022/propbinder-mobile-design.mjs +0 -26168
- package/fesm2022/propbinder-mobile-design.mjs.map +0 -1
- package/index.d.ts +0 -8169
- /package/{assets → src/assets}/fonts/Brockmann-Bold.otf +0 -0
- /package/{assets → src/assets}/fonts/Brockmann-BoldItalic.otf +0 -0
- /package/{assets → src/assets}/fonts/Brockmann-Medium.otf +0 -0
- /package/{assets → src/assets}/fonts/Brockmann-MediumItalic.otf +0 -0
- /package/{assets → src/assets}/fonts/Brockmann-Regular.otf +0 -0
- /package/{assets → src/assets}/fonts/Brockmann-RegularItalic.otf +0 -0
- /package/{assets → src/assets}/fonts/Brockmann-SemiBold.otf +0 -0
- /package/{assets → src/assets}/fonts/Brockmann-SemiBoldItalic.otf +0 -0
- /package/{assets → src/assets}/fonts/Brockmann_desktop_license.pdf +0 -0
- /package/{assets → src/assets}/fonts/brockmann-medium-webfont.woff2 +0 -0
- /package/{assets → src/assets}/fonts/brockmann-regular-webfont.woff2 +0 -0
- /package/{assets → src/assets}/fonts/brockmann-semibold-webfont.woff2 +0 -0
- /package/{styles → src/components/shared}/mobile-common.css +0 -0
- /package/{styles → src/components/shared}/mobile-page-base.css +0 -0
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
import { Component, Input, Output, EventEmitter, signal, OnInit, AfterViewInit, OnDestroy, ElementRef, computed, inject, effect } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { Router, NavigationEnd } from '@angular/router';
|
|
4
|
+
import { filter } from 'rxjs/operators';
|
|
5
|
+
import { IonTabBar, IonTabButton, IonLabel, ModalController } from '@ionic/angular/standalone';
|
|
6
|
+
import { DsIconComponent } from '@propbinder/design-system';
|
|
7
|
+
import { DsAvatarComponent } from '@propbinder/design-system';
|
|
8
|
+
import { DsLogoComponent } from '../logo/ds-logo';
|
|
9
|
+
import { DsMobileProfileActionsSheetComponent, ActionResult, ActionGroup, Language } from '../bottom-sheet';
|
|
10
|
+
import { disableModalShadowPointerEvents } from '../bottom-sheet/modal-shadow-fix';
|
|
11
|
+
import { UserService } from '../../services/user.service';
|
|
12
|
+
|
|
13
|
+
export interface TabConfig {
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
route: string;
|
|
17
|
+
icon: string;
|
|
18
|
+
iconActive: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* DsMobileTabBarComponent
|
|
23
|
+
*
|
|
24
|
+
* Responsive navigation tab bar that adapts from mobile to desktop:
|
|
25
|
+
* - Mobile (< 768px): Bottom tab bar with icons + labels
|
|
26
|
+
* - Desktop (≥ 768px): Top navigation bar with logo, tabs, and avatar
|
|
27
|
+
*
|
|
28
|
+
* Use this component INSIDE your own `ion-tabs` when Angular routing
|
|
29
|
+
* requires `ion-tabs` to be a direct child in your component.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```html
|
|
33
|
+
* <!-- In your component with child routes -->
|
|
34
|
+
* <!-- IMPORTANT: Add class="ds-tabs-wrapper" to ion-tabs for proper styling -->
|
|
35
|
+
* <ion-tabs class="ds-tabs-wrapper">
|
|
36
|
+
* <ds-mobile-tab-bar
|
|
37
|
+
* [tabs]="tabs"
|
|
38
|
+
* [avatarInitials]="'JD'"
|
|
39
|
+
* [profileMenuItems]="profileMenuItems"
|
|
40
|
+
* (profileActionSelected)="handleProfileAction($event)"
|
|
41
|
+
* />
|
|
42
|
+
* </ion-tabs>
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @example With profile menu configuration
|
|
46
|
+
* ```typescript
|
|
47
|
+
* profileMenuItems: ActionGroup[] = [
|
|
48
|
+
* {
|
|
49
|
+
* actions: [
|
|
50
|
+
* { action: 'profile', title: 'My Profile', icon: 'remixUser3Line' },
|
|
51
|
+
* { action: 'settings', title: 'Settings', icon: 'remixSettings3Line' }
|
|
52
|
+
* ]
|
|
53
|
+
* },
|
|
54
|
+
* {
|
|
55
|
+
* actions: [
|
|
56
|
+
* { action: 'logout', title: 'Log Out', icon: 'remixLogoutBoxLine', destructive: true }
|
|
57
|
+
* ]
|
|
58
|
+
* }
|
|
59
|
+
* ];
|
|
60
|
+
*
|
|
61
|
+
* handleProfileAction(result: ActionResult): void {
|
|
62
|
+
* switch (result.action) {
|
|
63
|
+
* case 'profile': // Navigate to profile
|
|
64
|
+
* case 'settings': // Navigate to settings
|
|
65
|
+
* case 'logout': // Handle logout
|
|
66
|
+
* }
|
|
67
|
+
* }
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* @note When using this component, you must add the class "ds-tabs-wrapper"
|
|
71
|
+
* to your `ion-tabs` element, or manually apply these styles:
|
|
72
|
+
* ```css
|
|
73
|
+
* ion-tabs {
|
|
74
|
+
* height: 100%;
|
|
75
|
+
* background: var(--color-header-surface);
|
|
76
|
+
* }
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
@Component({
|
|
80
|
+
selector: 'ds-mobile-tab-bar',
|
|
81
|
+
standalone: true,
|
|
82
|
+
imports: [CommonModule, IonTabBar, IonTabButton, IonLabel, DsIconComponent, DsAvatarComponent, DsLogoComponent],
|
|
83
|
+
styleUrls: ['./ds-mobile-tab-bar.css'],
|
|
84
|
+
template: `
|
|
85
|
+
<ion-tab-bar [attr.slot]="isDesktop() ? 'top' : 'bottom'" class="ds-tab-bar" [class.ds-tab-bar--desktop]="isDesktop()">
|
|
86
|
+
<!-- Logo (desktop only, full logo in header) -->
|
|
87
|
+
<div class="ds-tab-bar__logo">
|
|
88
|
+
<ds-logo variant="full" size="lg" />
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<!-- Tab buttons container -->
|
|
92
|
+
<div class="ds-tab-bar__tabs" *ngIf="tabs">
|
|
93
|
+
<ion-tab-button
|
|
94
|
+
*ngFor="let tab of tabs; trackBy: trackByTabId"
|
|
95
|
+
[tab]="tab.route"
|
|
96
|
+
[attr.data-icon]="tab.icon"
|
|
97
|
+
[attr.data-icon-active]="tab.iconActive"
|
|
98
|
+
[attr.aria-label]="tab.label"
|
|
99
|
+
class="ds-tab-button ion-activatable"
|
|
100
|
+
[class.tab-selected]="isTabActive(tab.route)"
|
|
101
|
+
>
|
|
102
|
+
<div class="tab-icon-ripple"></div>
|
|
103
|
+
<div class="tab-icon-wrapper">
|
|
104
|
+
<ds-icon [name]="tab.icon" [size]="isDesktop() ? '20px' : '24px'" class="tab-icon-inactive" />
|
|
105
|
+
<ds-icon [name]="tab.iconActive" [size]="isDesktop() ? '20px' : '24px'" class="tab-icon-active" />
|
|
106
|
+
</div>
|
|
107
|
+
<ion-label [attr.aria-hidden]="true">{{ tab.label }}</ion-label>
|
|
108
|
+
</ion-tab-button>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<!-- Avatar (desktop only, positioned via CSS) -->
|
|
112
|
+
<div class="ds-tab-bar__actions">
|
|
113
|
+
<ds-avatar [size]="'md'" [type]="avatarType" [initials]="avatarInitials" [src]="avatarSrc" [iconName]="avatarIconName" (click)="handleAvatarClick()" />
|
|
114
|
+
</div>
|
|
115
|
+
</ion-tab-bar>
|
|
116
|
+
`,
|
|
117
|
+
})
|
|
118
|
+
export class DsMobileTabBarComponent implements OnInit, AfterViewInit, OnDestroy {
|
|
119
|
+
// Inputs
|
|
120
|
+
@Input() tabs: TabConfig[] = [];
|
|
121
|
+
|
|
122
|
+
// Avatar inputs
|
|
123
|
+
@Input() avatarType: 'initials' | 'photo' | 'icon' = 'initials';
|
|
124
|
+
@Input() avatarInitials: string = 'U';
|
|
125
|
+
@Input() avatarSrc: string = '';
|
|
126
|
+
@Input() avatarIconName: string = 'remixUser3Line';
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Profile menu action groups to display when avatar is clicked.
|
|
130
|
+
* If not provided, only the avatarClick event will be emitted.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* profileMenuItems: ActionGroup[] = [
|
|
135
|
+
* {
|
|
136
|
+
* actions: [
|
|
137
|
+
* { action: 'profile', title: 'My Profile', icon: 'remixUser3Line' },
|
|
138
|
+
* { action: 'settings', title: 'Settings', icon: 'remixSettings3Line' }
|
|
139
|
+
* ]
|
|
140
|
+
* }
|
|
141
|
+
* ];
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
@Input() profileMenuItems?: ActionGroup[];
|
|
145
|
+
|
|
146
|
+
// Outputs
|
|
147
|
+
@Output() avatarClick = new EventEmitter<void>();
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Emitted when a profile menu action is selected.
|
|
151
|
+
* Parent component should handle the action logic (navigation, logout, etc.).
|
|
152
|
+
*/
|
|
153
|
+
@Output() profileActionSelected = new EventEmitter<ActionResult>();
|
|
154
|
+
|
|
155
|
+
// Internal state - exposed for template binding
|
|
156
|
+
activeTab = signal<string>('');
|
|
157
|
+
isDesktop = signal<boolean>(false);
|
|
158
|
+
|
|
159
|
+
private mutationObserver?: MutationObserver;
|
|
160
|
+
private slotEnforcementObserver?: MutationObserver;
|
|
161
|
+
private resizeObserver?: ResizeObserver;
|
|
162
|
+
private mediaQuery?: MediaQueryList;
|
|
163
|
+
private routerSubscription?: any;
|
|
164
|
+
|
|
165
|
+
private router?: Router | null;
|
|
166
|
+
private modalController = inject(ModalController);
|
|
167
|
+
private userService = inject(UserService);
|
|
168
|
+
|
|
169
|
+
constructor(private elementRef: ElementRef) {
|
|
170
|
+
// Inject Router optionally
|
|
171
|
+
this.router = inject(Router, { optional: true }) || undefined;
|
|
172
|
+
|
|
173
|
+
// Initialize breakpoint detection EARLY (before effect)
|
|
174
|
+
// This ensures isDesktop() is set before the effect runs
|
|
175
|
+
this.setupBreakpointDetection();
|
|
176
|
+
|
|
177
|
+
// Debug: Log initial state
|
|
178
|
+
setTimeout(() => {
|
|
179
|
+
// console.log('[ds-mobile-tab-bar] Initial state:', {
|
|
180
|
+
// isDesktop: this.isDesktop(),
|
|
181
|
+
// windowWidth: window.innerWidth,
|
|
182
|
+
// mediaQuery: this.mediaQuery?.matches,
|
|
183
|
+
// userAgent: navigator.userAgent
|
|
184
|
+
// });
|
|
185
|
+
}, 100);
|
|
186
|
+
|
|
187
|
+
// Watch for isDesktop changes and update slot reactively
|
|
188
|
+
// effect() must be called in constructor (injection context)
|
|
189
|
+
effect(() => {
|
|
190
|
+
// This effect runs whenever isDesktop() changes
|
|
191
|
+
const _ = this.isDesktop(); // Read the signal to create dependency
|
|
192
|
+
// console.log('[ds-mobile-tab-bar] effect() triggered, isDesktop:', this.isDesktop());
|
|
193
|
+
if (this.elementRef.nativeElement) {
|
|
194
|
+
// Use setTimeout to ensure DOM is ready
|
|
195
|
+
setTimeout(() => this.updateSlot(), 0);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
ngOnInit(): void {
|
|
201
|
+
// Listen to router events to detect active tab from URL
|
|
202
|
+
if (this.router) {
|
|
203
|
+
this.routerSubscription = this.router.events.pipe(filter((event) => event instanceof NavigationEnd)).subscribe((event: any) => {
|
|
204
|
+
const url = event.urlAfterRedirects || event.url;
|
|
205
|
+
// Extract the route segment (e.g., /tab-bar-test/home -> home)
|
|
206
|
+
const segments = url.split('/').filter((s: string) => s);
|
|
207
|
+
const lastSegment = segments[segments.length - 1];
|
|
208
|
+
|
|
209
|
+
// Find matching tab by route
|
|
210
|
+
if (this.tabs && lastSegment) {
|
|
211
|
+
const matchingTab = this.tabs.find((tab) => tab.route === lastSegment);
|
|
212
|
+
if (matchingTab) {
|
|
213
|
+
this.activeTab.set(matchingTab.route);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
ngAfterViewInit(): void {
|
|
221
|
+
// Initial removal
|
|
222
|
+
this.removeTitleAttributes();
|
|
223
|
+
|
|
224
|
+
// Set up mutation observer to continuously remove title attributes
|
|
225
|
+
this.setupTitleRemovalObserver();
|
|
226
|
+
|
|
227
|
+
// Set up active tab detection
|
|
228
|
+
this.setupActiveTabDetection();
|
|
229
|
+
|
|
230
|
+
// Ensure slot is set correctly on initial render (with retries)
|
|
231
|
+
this.updateSlot();
|
|
232
|
+
|
|
233
|
+
// Set up slot enforcement to prevent Ionic from overriding
|
|
234
|
+
setTimeout(() => {
|
|
235
|
+
this.setupSlotEnforcement();
|
|
236
|
+
// Also retry updateSlot a few times to ensure it sticks
|
|
237
|
+
setTimeout(() => this.updateSlot(), 100);
|
|
238
|
+
setTimeout(() => this.updateSlot(), 300);
|
|
239
|
+
}, 0);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private updateSlot(): void {
|
|
243
|
+
// CRITICAL: Set slot on the HOST element (ds-mobile-tab-bar) first
|
|
244
|
+
// Ionic positions children based on the wrapper's slot, not the inner element's slot
|
|
245
|
+
const hostElement = this.elementRef.nativeElement;
|
|
246
|
+
const hostSlotValue = this.isDesktop() ? 'top' : 'bottom';
|
|
247
|
+
const currentHostSlot = hostElement.getAttribute('slot');
|
|
248
|
+
|
|
249
|
+
if (currentHostSlot !== hostSlotValue) {
|
|
250
|
+
// console.log('[ds-mobile-tab-bar] updateSlot: Setting HOST slot from', currentHostSlot, 'to', hostSlotValue);
|
|
251
|
+
hostElement.setAttribute('slot', hostSlotValue);
|
|
252
|
+
(hostElement as any).slot = hostSlotValue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Get the ion-tab-bar element
|
|
256
|
+
const tabBar = this.elementRef.nativeElement.querySelector('ion-tab-bar');
|
|
257
|
+
if (!tabBar) {
|
|
258
|
+
// console.log('[ds-mobile-tab-bar] updateSlot: tabBar not found, retrying...');
|
|
259
|
+
// Retry if element not found yet
|
|
260
|
+
setTimeout(() => this.updateSlot(), 50);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const slotValue = this.isDesktop() ? 'top' : 'bottom';
|
|
265
|
+
const currentSlot = tabBar.getAttribute('slot');
|
|
266
|
+
const currentSlotProperty = (tabBar as any).slot;
|
|
267
|
+
|
|
268
|
+
// Debug logging
|
|
269
|
+
// console.log('[ds-mobile-tab-bar] updateSlot:', {
|
|
270
|
+
// isDesktop: this.isDesktop(),
|
|
271
|
+
// windowWidth: window.innerWidth,
|
|
272
|
+
// slotValue,
|
|
273
|
+
// currentSlotAttribute: currentSlot,
|
|
274
|
+
// currentSlotProperty: currentSlotProperty,
|
|
275
|
+
// tabBarElement: tabBar,
|
|
276
|
+
// tabBarParent: tabBar.parentElement?.tagName,
|
|
277
|
+
// tabBarInIonTabs: tabBar.closest('ion-tabs') !== null
|
|
278
|
+
// });
|
|
279
|
+
|
|
280
|
+
// Only update if different to avoid unnecessary DOM manipulation
|
|
281
|
+
if (currentSlot !== slotValue || currentSlotProperty !== slotValue) {
|
|
282
|
+
// console.log('[ds-mobile-tab-bar] updateSlot: Setting slot from', currentSlot, 'to', slotValue);
|
|
283
|
+
|
|
284
|
+
// Set both attribute and property to ensure it works
|
|
285
|
+
tabBar.setAttribute('slot', slotValue);
|
|
286
|
+
(tabBar as any).slot = slotValue;
|
|
287
|
+
|
|
288
|
+
// Also try setting it on the parent ion-tabs
|
|
289
|
+
const parentIonTabs = tabBar.closest('ion-tabs');
|
|
290
|
+
if (parentIonTabs) {
|
|
291
|
+
// console.log('[ds-mobile-tab-bar] updateSlot: Found parent ion-tabs');
|
|
292
|
+
// Force Ionic to recognize the slot change
|
|
293
|
+
(parentIonTabs as any).forceUpdate?.();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Force a reflow to ensure Ionic processes the change
|
|
297
|
+
void tabBar.offsetHeight;
|
|
298
|
+
|
|
299
|
+
// Verify it was set
|
|
300
|
+
const verifySlot = tabBar.getAttribute('slot');
|
|
301
|
+
const verifySlotProperty = (tabBar as any).slot;
|
|
302
|
+
// console.log('[ds-mobile-tab-bar] updateSlot: After update, slot attribute:', verifySlot, 'slot property:', verifySlotProperty);
|
|
303
|
+
|
|
304
|
+
// Check computed styles
|
|
305
|
+
const computedStyle = window.getComputedStyle(tabBar);
|
|
306
|
+
const parentComputedStyle = tabBar.parentElement ? window.getComputedStyle(tabBar.parentElement) : null;
|
|
307
|
+
const ionTabsForStyles = tabBar.closest('ion-tabs');
|
|
308
|
+
const ionTabsComputedStyle = ionTabsForStyles ? window.getComputedStyle(ionTabsForStyles) : null;
|
|
309
|
+
|
|
310
|
+
// console.log('[ds-mobile-tab-bar] updateSlot: Computed styles:', {
|
|
311
|
+
// tabBar: {
|
|
312
|
+
// position: computedStyle.position,
|
|
313
|
+
// top: computedStyle.top,
|
|
314
|
+
// bottom: computedStyle.bottom,
|
|
315
|
+
// order: computedStyle.order,
|
|
316
|
+
// display: computedStyle.display,
|
|
317
|
+
// zIndex: computedStyle.zIndex,
|
|
318
|
+
// transform: computedStyle.transform
|
|
319
|
+
// },
|
|
320
|
+
// parent: parentComputedStyle ? {
|
|
321
|
+
// display: parentComputedStyle.display,
|
|
322
|
+
// flexDirection: parentComputedStyle.flexDirection,
|
|
323
|
+
// gridTemplateRows: parentComputedStyle.gridTemplateRows
|
|
324
|
+
// } : null,
|
|
325
|
+
// ionTabs: ionTabsComputedStyle ? {
|
|
326
|
+
// display: ionTabsComputedStyle.display,
|
|
327
|
+
// flexDirection: ionTabsComputedStyle.flexDirection,
|
|
328
|
+
// gridTemplateRows: ionTabsComputedStyle.gridTemplateRows,
|
|
329
|
+
// position: ionTabsComputedStyle.position
|
|
330
|
+
// } : null,
|
|
331
|
+
// tabBarRect: tabBar.getBoundingClientRect(),
|
|
332
|
+
// windowHeight: window.innerHeight
|
|
333
|
+
// });
|
|
334
|
+
} else {
|
|
335
|
+
// console.log('[ds-mobile-tab-bar] updateSlot: Slot already correct, no update needed');
|
|
336
|
+
|
|
337
|
+
// Even if slot is correct, check computed styles to see why it's not at top
|
|
338
|
+
const computedStyle = window.getComputedStyle(tabBar);
|
|
339
|
+
const ionTabsForStyles = tabBar.closest('ion-tabs');
|
|
340
|
+
const ionTabsComputedStyle = ionTabsForStyles ? window.getComputedStyle(ionTabsForStyles) : null;
|
|
341
|
+
const tabBarRect = tabBar.getBoundingClientRect();
|
|
342
|
+
|
|
343
|
+
// Log key values directly so they're always visible
|
|
344
|
+
// console.log('[ds-mobile-tab-bar] KEY VALUES:');
|
|
345
|
+
// console.log(' tabBar.position:', computedStyle.position);
|
|
346
|
+
// console.log(' tabBar.top:', computedStyle.top);
|
|
347
|
+
// console.log(' tabBar.bottom:', computedStyle.bottom);
|
|
348
|
+
// console.log(' tabBar.order:', computedStyle.order);
|
|
349
|
+
// console.log(' tabBar.display:', computedStyle.display);
|
|
350
|
+
// console.log(' tabBarRect.top:', tabBarRect.top, 'px from top');
|
|
351
|
+
// console.log(' tabBarRect.bottom:', tabBarRect.bottom, 'px from top');
|
|
352
|
+
// console.log(' window.innerHeight:', window.innerHeight);
|
|
353
|
+
if (ionTabsComputedStyle) {
|
|
354
|
+
// console.log(' ionTabs.display:', ionTabsComputedStyle.display);
|
|
355
|
+
// console.log(' ionTabs.flexDirection:', ionTabsComputedStyle.flexDirection);
|
|
356
|
+
// console.log(' ionTabs.gridTemplateRows:', ionTabsComputedStyle.gridTemplateRows);
|
|
357
|
+
}
|
|
358
|
+
if (ionTabsForStyles) {
|
|
359
|
+
const children = Array.from(ionTabsForStyles.children);
|
|
360
|
+
// console.log(' ionTabs children count:', children.length);
|
|
361
|
+
children.forEach((child: any, index) => {
|
|
362
|
+
// console.log(` [${index}] ${child.tagName} slot="${child.getAttribute('slot')}" order="${window.getComputedStyle(child).order}"`);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// console.log('[ds-mobile-tab-bar] updateSlot: Computed styles (slot correct but visually wrong):', {
|
|
367
|
+
// tabBar: {
|
|
368
|
+
// position: computedStyle.position,
|
|
369
|
+
// top: computedStyle.top,
|
|
370
|
+
// bottom: computedStyle.bottom,
|
|
371
|
+
// order: computedStyle.order,
|
|
372
|
+
// display: computedStyle.display,
|
|
373
|
+
// zIndex: computedStyle.zIndex,
|
|
374
|
+
// transform: computedStyle.transform,
|
|
375
|
+
// marginTop: computedStyle.marginTop,
|
|
376
|
+
// marginBottom: computedStyle.marginBottom,
|
|
377
|
+
// width: computedStyle.width,
|
|
378
|
+
// height: computedStyle.height
|
|
379
|
+
// },
|
|
380
|
+
// ionTabs: ionTabsComputedStyle ? {
|
|
381
|
+
// display: ionTabsComputedStyle.display,
|
|
382
|
+
// flexDirection: ionTabsComputedStyle.flexDirection,
|
|
383
|
+
// gridTemplateRows: ionTabsComputedStyle.gridTemplateRows,
|
|
384
|
+
// gridTemplateColumns: ionTabsComputedStyle.gridTemplateColumns,
|
|
385
|
+
// position: ionTabsComputedStyle.position,
|
|
386
|
+
// alignItems: ionTabsComputedStyle.alignItems,
|
|
387
|
+
// justifyContent: ionTabsComputedStyle.justifyContent,
|
|
388
|
+
// height: ionTabsComputedStyle.height,
|
|
389
|
+
// minHeight: ionTabsComputedStyle.minHeight
|
|
390
|
+
// } : null,
|
|
391
|
+
// tabBarRect: {
|
|
392
|
+
// top: tabBarRect.top,
|
|
393
|
+
// bottom: tabBarRect.bottom,
|
|
394
|
+
// height: tabBarRect.height,
|
|
395
|
+
// y: tabBarRect.y,
|
|
396
|
+
// left: tabBarRect.left,
|
|
397
|
+
// right: tabBarRect.right,
|
|
398
|
+
// width: tabBarRect.width
|
|
399
|
+
// },
|
|
400
|
+
// windowHeight: window.innerHeight,
|
|
401
|
+
// distanceFromTop: tabBarRect.top,
|
|
402
|
+
// distanceFromBottom: window.innerHeight - tabBarRect.bottom,
|
|
403
|
+
// // Check if tab bar is actually in the DOM at the right position
|
|
404
|
+
// tabBarParent: tabBar.parentElement?.tagName,
|
|
405
|
+
// tabBarNextSibling: tabBar.nextElementSibling?.tagName,
|
|
406
|
+
// tabBarPreviousSibling: tabBar.previousElementSibling?.tagName,
|
|
407
|
+
// // Check all children of ion-tabs to see DOM order
|
|
408
|
+
// ionTabsChildren: ionTabsForStyles ? Array.from(ionTabsForStyles.children).map((child: any) => ({
|
|
409
|
+
// tagName: child.tagName,
|
|
410
|
+
// slot: child.getAttribute('slot'),
|
|
411
|
+
// order: window.getComputedStyle(child).order
|
|
412
|
+
// })) : null
|
|
413
|
+
// });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private setupSlotEnforcement(): void {
|
|
418
|
+
const hostElement = this.elementRef.nativeElement;
|
|
419
|
+
const tabBar = this.elementRef.nativeElement.querySelector('ion-tab-bar');
|
|
420
|
+
if (!tabBar) {
|
|
421
|
+
// console.log('[ds-mobile-tab-bar] setupSlotEnforcement: tabBar not found, retrying...');
|
|
422
|
+
// Retry if element not found yet
|
|
423
|
+
setTimeout(() => this.setupSlotEnforcement(), 50);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// console.log('[ds-mobile-tab-bar] setupSlotEnforcement: Setting up MutationObserver');
|
|
428
|
+
|
|
429
|
+
const observer = new MutationObserver((mutations) => {
|
|
430
|
+
mutations.forEach((mutation) => {
|
|
431
|
+
if (mutation.type === 'attributes' && mutation.attributeName === 'slot') {
|
|
432
|
+
const target = mutation.target as HTMLElement;
|
|
433
|
+
const expectedSlot = this.isDesktop() ? 'top' : 'bottom';
|
|
434
|
+
|
|
435
|
+
// Check both host element and tab bar
|
|
436
|
+
if (target === hostElement || target === tabBar) {
|
|
437
|
+
const currentSlot = target.getAttribute('slot');
|
|
438
|
+
|
|
439
|
+
// console.log('[ds-mobile-tab-bar] Slot changed by external source:', {
|
|
440
|
+
// target: target.tagName,
|
|
441
|
+
// currentSlot,
|
|
442
|
+
// expectedSlot,
|
|
443
|
+
// isDesktop: this.isDesktop()
|
|
444
|
+
// });
|
|
445
|
+
|
|
446
|
+
// If Ionic or something else changed it, force it back
|
|
447
|
+
if (currentSlot !== expectedSlot) {
|
|
448
|
+
// console.log('[ds-mobile-tab-bar] Enforcing slot back to:', expectedSlot);
|
|
449
|
+
// Use requestAnimationFrame to avoid infinite loops
|
|
450
|
+
requestAnimationFrame(() => {
|
|
451
|
+
target.setAttribute('slot', expectedSlot);
|
|
452
|
+
(target as any).slot = expectedSlot;
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Observe both host element and tab bar for slot changes
|
|
461
|
+
observer.observe(hostElement, {
|
|
462
|
+
attributes: true,
|
|
463
|
+
attributeFilter: ['slot'],
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
observer.observe(tabBar, {
|
|
467
|
+
attributes: true,
|
|
468
|
+
attributeFilter: ['slot'],
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// Store observer for cleanup
|
|
472
|
+
this.slotEnforcementObserver = observer;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
ngOnDestroy(): void {
|
|
476
|
+
if (this.mutationObserver) {
|
|
477
|
+
this.mutationObserver.disconnect();
|
|
478
|
+
}
|
|
479
|
+
if (this.slotEnforcementObserver) {
|
|
480
|
+
this.slotEnforcementObserver.disconnect();
|
|
481
|
+
}
|
|
482
|
+
if (this.mediaQuery) {
|
|
483
|
+
this.mediaQuery.removeEventListener('change', this.handleBreakpointChange);
|
|
484
|
+
}
|
|
485
|
+
if (this.routerSubscription) {
|
|
486
|
+
this.routerSubscription.unsubscribe();
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private setupBreakpointDetection(): void {
|
|
491
|
+
// Use matchMedia for responsive breakpoint detection
|
|
492
|
+
this.mediaQuery = window.matchMedia('(min-width: 768px)');
|
|
493
|
+
this.isDesktop.set(this.mediaQuery.matches);
|
|
494
|
+
|
|
495
|
+
this.handleBreakpointChange = this.handleBreakpointChange.bind(this);
|
|
496
|
+
this.mediaQuery.addEventListener('change', this.handleBreakpointChange);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
private handleBreakpointChange = (e: MediaQueryListEvent): void => {
|
|
500
|
+
// console.log('[ds-mobile-tab-bar] handleBreakpointChange:', {
|
|
501
|
+
// matches: e.matches,
|
|
502
|
+
// windowWidth: window.innerWidth,
|
|
503
|
+
// previousIsDesktop: this.isDesktop()
|
|
504
|
+
// });
|
|
505
|
+
this.isDesktop.set(e.matches);
|
|
506
|
+
// Force update the slot when breakpoint changes
|
|
507
|
+
this.updateSlot();
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
private setupTitleRemovalObserver(): void {
|
|
511
|
+
const config = {
|
|
512
|
+
attributes: true,
|
|
513
|
+
attributeFilter: ['title'],
|
|
514
|
+
subtree: true,
|
|
515
|
+
childList: true,
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
this.mutationObserver = new MutationObserver((mutations) => {
|
|
519
|
+
mutations.forEach((mutation) => {
|
|
520
|
+
if (mutation.type === 'attributes' && mutation.attributeName === 'title') {
|
|
521
|
+
const target = mutation.target as HTMLElement;
|
|
522
|
+
if (target.tagName === 'ION-TAB-BUTTON' && target.hasAttribute('title')) {
|
|
523
|
+
target.removeAttribute('title');
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
// Also do a sweep after any changes
|
|
528
|
+
this.removeTitleAttributes();
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
this.mutationObserver.observe(this.elementRef.nativeElement, config);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private removeTitleAttributes(): void {
|
|
535
|
+
const tabButtons = this.elementRef.nativeElement.querySelectorAll('ion-tab-button');
|
|
536
|
+
tabButtons.forEach((button: HTMLElement) => {
|
|
537
|
+
if (button.hasAttribute('title')) {
|
|
538
|
+
button.removeAttribute('title');
|
|
539
|
+
}
|
|
540
|
+
// Also remove from the native button inside shadow DOM
|
|
541
|
+
const nativeButton = button.shadowRoot?.querySelector('button');
|
|
542
|
+
if (nativeButton?.hasAttribute('title')) {
|
|
543
|
+
nativeButton.removeAttribute('title');
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private setupActiveTabDetection(): void {
|
|
549
|
+
// Find the parent ion-tabs element
|
|
550
|
+
const ionTabs = this.elementRef.nativeElement.closest('ion-tabs');
|
|
551
|
+
if (!ionTabs) {
|
|
552
|
+
console.warn('ds-mobile-tab-bar: Could not find parent ion-tabs element');
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Listen for tab changes using Ionic events
|
|
557
|
+
ionTabs.addEventListener('ionTabsDidChange', (event: any) => {
|
|
558
|
+
const tabRoute = event.detail?.tab;
|
|
559
|
+
if (tabRoute) {
|
|
560
|
+
this.activeTab.set(tabRoute);
|
|
561
|
+
} else {
|
|
562
|
+
// Fallback: check DOM immediately after event
|
|
563
|
+
setTimeout(() => this.updateActiveTabFromDOM(), 0);
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// Also listen for tab button clicks
|
|
568
|
+
this.elementRef.nativeElement.addEventListener('click', (event: any) => {
|
|
569
|
+
const button = event.target.closest('ion-tab-button');
|
|
570
|
+
if (button) {
|
|
571
|
+
setTimeout(() => this.updateActiveTabFromDOM(), 50);
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// Get initial selected tab
|
|
576
|
+
this.updateActiveTabFromDOM();
|
|
577
|
+
|
|
578
|
+
// Watch for selected attribute changes on tab buttons (more reliable)
|
|
579
|
+
const observer = new MutationObserver(() => {
|
|
580
|
+
this.updateActiveTabFromDOM();
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
observer.observe(this.elementRef.nativeElement, {
|
|
584
|
+
attributes: true,
|
|
585
|
+
attributeFilter: ['selected', 'tab'],
|
|
586
|
+
subtree: true,
|
|
587
|
+
childList: true,
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
// Also watch the parent ion-tabs for changes
|
|
591
|
+
observer.observe(ionTabs, {
|
|
592
|
+
attributes: true,
|
|
593
|
+
attributeFilter: ['selected', 'tab'],
|
|
594
|
+
subtree: true,
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// Periodic check as fallback (in case events don't fire)
|
|
598
|
+
setInterval(() => {
|
|
599
|
+
this.updateActiveTabFromDOM();
|
|
600
|
+
}, 100);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
private updateActiveTabFromDOM(): void {
|
|
604
|
+
// Check parent ion-tabs for selected tab (most reliable)
|
|
605
|
+
const ionTabs = this.elementRef.nativeElement.closest('ion-tabs');
|
|
606
|
+
if (ionTabs) {
|
|
607
|
+
// Method 1: Check for ion-tab with selected attribute
|
|
608
|
+
const selectedTab = ionTabs.querySelector('ion-tab[selected]');
|
|
609
|
+
if (selectedTab) {
|
|
610
|
+
const tabRoute = selectedTab.getAttribute('tab');
|
|
611
|
+
if (tabRoute) {
|
|
612
|
+
this.activeTab.set(tabRoute);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Method 2: Check for ion-tab without tab-hidden class (Ionic shows active tab)
|
|
618
|
+
const visibleTab = ionTabs.querySelector('ion-tab:not(.tab-hidden)');
|
|
619
|
+
if (visibleTab) {
|
|
620
|
+
const tabRoute = visibleTab.getAttribute('tab');
|
|
621
|
+
if (tabRoute) {
|
|
622
|
+
this.activeTab.set(tabRoute);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Method 3: Check tab buttons for selected state
|
|
629
|
+
const tabButtons = this.elementRef.nativeElement.querySelectorAll('ion-tab-button');
|
|
630
|
+
tabButtons.forEach((button: any) => {
|
|
631
|
+
// Check Ionic's native selected property
|
|
632
|
+
if (button.selected === true) {
|
|
633
|
+
const tabRoute = button.getAttribute('tab');
|
|
634
|
+
if (tabRoute) {
|
|
635
|
+
this.activeTab.set(tabRoute);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
trackByTabId(index: number, tab: TabConfig): string {
|
|
642
|
+
return tab.id;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
isTabActive(tabRoute: string): boolean {
|
|
646
|
+
const currentActive = this.activeTab();
|
|
647
|
+
// Match by route (primary) or by checking if the tab button is selected
|
|
648
|
+
if (currentActive === tabRoute) {
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Fallback: check if this button is actually selected in the DOM
|
|
653
|
+
const tabButtons = this.elementRef.nativeElement.querySelectorAll('ion-tab-button');
|
|
654
|
+
for (let i = 0; i < tabButtons.length; i++) {
|
|
655
|
+
const button = tabButtons[i] as any;
|
|
656
|
+
if (button.getAttribute('tab') === tabRoute && button.selected === true) {
|
|
657
|
+
return true;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Handle avatar click - opens profile menu if configured, otherwise just emits avatarClick
|
|
666
|
+
*/
|
|
667
|
+
async handleAvatarClick(): Promise<void> {
|
|
668
|
+
// Emit the basic click event (for backwards compatibility)
|
|
669
|
+
this.avatarClick.emit();
|
|
670
|
+
|
|
671
|
+
// Use input if provided, otherwise fall back to service
|
|
672
|
+
const menuItems = this.profileMenuItems || this.userService.profileMenuItems();
|
|
673
|
+
|
|
674
|
+
// If no menu items configured, just emit and return
|
|
675
|
+
if (!menuItems || menuItems.length === 0) {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Open the bottom sheet with configured menu items
|
|
680
|
+
const sheet = await this.modalController.create({
|
|
681
|
+
component: DsMobileProfileActionsSheetComponent,
|
|
682
|
+
componentProps: {
|
|
683
|
+
actionGroups: menuItems,
|
|
684
|
+
currentLanguage: 'da', // TODO: Get from language service
|
|
685
|
+
availableLanguages: [
|
|
686
|
+
{
|
|
687
|
+
code: 'da',
|
|
688
|
+
nativeName: 'Dansk',
|
|
689
|
+
englishName: 'Danish',
|
|
690
|
+
flagIcon: '/Assets/country-flags/denmark.svg',
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
code: 'en',
|
|
694
|
+
nativeName: 'English',
|
|
695
|
+
englishName: 'English',
|
|
696
|
+
flagIcon: '/Assets/country-flags/united kingdom.svg',
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
code: 'sv',
|
|
700
|
+
nativeName: 'Svenska',
|
|
701
|
+
englishName: 'Swedish',
|
|
702
|
+
flagIcon: '/Assets/country-flags/sweden.svg',
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
code: 'no',
|
|
706
|
+
nativeName: 'Norsk',
|
|
707
|
+
englishName: 'Norwegian',
|
|
708
|
+
flagIcon: '/Assets/country-flags/norway.svg',
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
code: 'de',
|
|
712
|
+
nativeName: 'Deutsch',
|
|
713
|
+
englishName: 'German',
|
|
714
|
+
flagIcon: '/Assets/country-flags/germany.svg',
|
|
715
|
+
},
|
|
716
|
+
],
|
|
717
|
+
},
|
|
718
|
+
breakpoints: [0, 1],
|
|
719
|
+
initialBreakpoint: 1,
|
|
720
|
+
handle: true,
|
|
721
|
+
cssClass: ['ds-bottom-sheet', 'auto-height'],
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
await sheet.present();
|
|
725
|
+
disableModalShadowPointerEvents(sheet);
|
|
726
|
+
|
|
727
|
+
const result = await sheet.onWillDismiss<ActionResult>();
|
|
728
|
+
if (result.data?.action) {
|
|
729
|
+
// Emit the selected action to parent
|
|
730
|
+
this.profileActionSelected.emit(result.data);
|
|
731
|
+
// Also notify globally via UserService
|
|
732
|
+
this.userService.notifyProfileAction(result.data);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|