diff --git a/apps/doc/src/app/components/tabs/examples/tabs-example-closable/tabs-example-closable.component.html b/apps/doc/src/app/components/tabs/examples/tabs-example-closable/tabs-example-closable.component.html index 82bf5e3e9c..8ba43c5a9e 100644 --- a/apps/doc/src/app/components/tabs/examples/tabs-example-closable/tabs-example-closable.component.html +++ b/apps/doc/src/app/components/tabs/examples/tabs-example-closable/tabs-example-closable.component.html @@ -1,5 +1,5 @@
- +
+ +
activeTabIndex: [{{ activeTabIndex }}]
diff --git a/apps/doc/src/app/components/tabs/examples/tabs-example-closable/tabs-example-closable.component.ts b/apps/doc/src/app/components/tabs/examples/tabs-example-closable/tabs-example-closable.component.ts index 1297b713f8..35a63b2199 100644 --- a/apps/doc/src/app/components/tabs/examples/tabs-example-closable/tabs-example-closable.component.ts +++ b/apps/doc/src/app/components/tabs/examples/tabs-example-closable/tabs-example-closable.component.ts @@ -8,6 +8,7 @@ import { PrizmTabItem } from '@prizm-ui/components'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class TabsExampleClosableComponent { + activeTabIndex = 0; public tabs: PrizmTabItem[] = [ { title: 'Вкладка 1', diff --git a/apps/doc/src/app/components/tabs/tabs-example.component.html b/apps/doc/src/app/components/tabs/tabs-example.component.html index f7492f5335..4d4a3ff501 100644 --- a/apps/doc/src/app/components/tabs/tabs-example.component.html +++ b/apps/doc/src/app/components/tabs/tabs-example.component.html @@ -37,11 +37,11 @@
(); + private currentDomIdx!: number; override readonly testId_ = 'ui_tab'; readonly isActiveTab$: Observable = combineLatest([ this.idx$, @@ -72,33 +76,42 @@ export class PrizmTabComponent extends PrizmAbstractTestId implements OnInit, On tap(tab => { if (tab === this) this.tabsService.removeTab(tab); }), - takeUntil(this.destroy) + timeout(25) ) .subscribe(); } private isFromMenuTab(): boolean { - return !!this.inMenuContextService?.context?.inMenuIdx; + return Compare.isNotNullish(this.inMenuContextService?.context?.inMenuIdx); } private isMainProjectedTab(): boolean { return !this.isFromMenuTab(); } - public ngOnInit(): void { - this.tabsService.tabs$ + private initUpdateIndexOnDomUpdateListener(): void { + this.tabsService.removed$$ .pipe( + switchMap(() => this.tabsService.changeParent$), + startWith(void 0), filter(() => this.isMainProjectedTab()), tap(() => { const currentDomIdx = Array.from(this.el.nativeElement.parentElement?.children ?? []).indexOf( this.el.nativeElement ); - this.tabsService.updateTab(this, currentDomIdx); + if (Compare.isNotNullish(this.currentDomIdx) && currentDomIdx !== this.currentDomIdx) { + this.tabsService.moveTab(this.currentDomIdx, currentDomIdx, this); + } else { + this.tabsService.updateTab(this, currentDomIdx); + } + this.currentDomIdx = currentDomIdx; }), takeUntil(this.destroy) ) .subscribe(); + } + private initClickListenerToSelectTab(): void { fromEvent(this.el.nativeElement, 'click') .pipe( switchMap(() => { @@ -110,6 +123,11 @@ export class PrizmTabComponent extends PrizmAbstractTestId implements OnInit, On .subscribe(); } + public ngOnInit(): void { + this.initUpdateIndexOnDomUpdateListener(); + this.initClickListenerToSelectTab(); + } + public selectTab$(): Observable { return this.tab$.pipe( first(), diff --git a/libs/components/src/lib/components/tabs/tabs.component.ts b/libs/components/src/lib/components/tabs/tabs.component.ts index becf1c0e4a..3e4e8f25d4 100644 --- a/libs/components/src/lib/components/tabs/tabs.component.ts +++ b/libs/components/src/lib/components/tabs/tabs.component.ts @@ -78,6 +78,7 @@ export class PrizmTabsComponent extends PrizmAbstractTestId implements OnInit, O constructor( private readonly cdRef: ChangeDetectorRef, + private readonly elRef: ElementRef, private readonly destroy$: PrizmDestroyService, private readonly tabsService: PrizmTabsService ) { @@ -85,6 +86,7 @@ export class PrizmTabsComponent extends PrizmAbstractTestId implements OnInit, O } public ngOnInit(): void { + this.tabsService.initObservingTabsParent(this.tabsContainer.nativeElement); this.mutationObserver = new MutationObserver(() => this.mutationDetector$.next()); this.resizeObserver = new ResizeObserver(() => this.mutationDetector$.next()); this.mutationObserver.observe(this.tabsContainer.nativeElement, { diff --git a/libs/components/src/lib/components/tabs/tabs.service.ts b/libs/components/src/lib/components/tabs/tabs.service.ts index 2c75686cb7..51bf283e3a 100644 --- a/libs/components/src/lib/components/tabs/tabs.service.ts +++ b/libs/components/src/lib/components/tabs/tabs.service.ts @@ -2,16 +2,22 @@ import { Injectable, OnDestroy } from '@angular/core'; import { BehaviorSubject, combineLatest, concat, Observable, of, Subject } from 'rxjs'; import { distinctUntilChanged, filter, map, startWith, take, takeUntil, tap } from 'rxjs/operators'; import { PrizmTabComponent } from './components/tab.component'; -import { filterTruthy, PrizmDestroyService } from '@prizm-ui/helpers'; +import { filterTruthy, PrizmDestroyService, prizmFromMutationObserver$ } from '@prizm-ui/helpers'; import { PrizmTabCanOpen } from './tabs.model'; @Injectable() export class PrizmTabsService implements OnDestroy { readonly tabs = new Map(); readonly changes$$ = new Subject>(); + readonly removed$$ = new Subject(); + private changeParent$_!: Observable; + get changeParent$() { + return this.changeParent$_; + } readonly closeTab$$ = new Subject>(); private readonly activeTabIdx$$ = new BehaviorSubject(0); readonly activeTabIdx$ = this.activeTabIdx$$.pipe(distinctUntilChanged()); + get activeTabIdx() { return this.activeTabIdx$$.value; } @@ -22,6 +28,12 @@ export class PrizmTabsService implements OnDestroy { constructor(private readonly destroy: PrizmDestroyService) {} + public initObservingTabsParent(el: HTMLElement) { + this.changeParent$_ = prizmFromMutationObserver$(el, { + subtree: true, + childList: true, + }); + } public isActiveTab(tab: PrizmTabComponent): Observable { return combineLatest([this.activeTabIdx$$, this.tabs$]).pipe( map(([activeTabIdx]) => { @@ -36,6 +48,15 @@ export class PrizmTabsService implements OnDestroy { return this.tabs.get(idx) as PrizmTabComponent; } + public moveTab(idx: number, toIndex: number, tab: PrizmTabComponent): void { + if (tab !== this.getTabByIdx(idx)) return; + this.tabs.delete(idx); + this.updateTab(tab, toIndex); + if (this.activeTabIdx$$.value === idx) { + this.activeTabIdx$$.next(toIndex); + } + } + public updateTab(tab: PrizmTabComponent, idx?: number): void { const tabIdx = typeof idx !== 'number' ? this.tabs.size : idx; if (this.tabs.get(tabIdx) === tab) return; @@ -46,24 +67,26 @@ export class PrizmTabsService implements OnDestroy { public removeTab(tab: PrizmTabComponent): void { const idx = this.findTabIdx(tab); this.tabs.delete(idx); - const currentTabIdx = this.findTabIdx(tab); - if (currentTabIdx === -1) return; - this.correctActiveTabIdx(currentTabIdx); - this.changes$$.next(this.tabs); + this.removed$$.next(tab); + const newIdx = this.correctActiveTabIdx(idx); + if (idx !== newIdx) this.changes$$.next(this.tabs); } - private correctActiveTabIdx(idx: number = this.activeTabIdx$$.value): void { - const isActiveTab = this.activeTabIdx$$.value === idx; - let newIdx = idx - 1; - if (isActiveTab) newIdx++; - if (!this.tabs.size) newIdx = 0; - if (isActiveTab && this.activeTabIdx$$.value !== newIdx) this.activeTabIdx$$.next(newIdx); + private correctActiveTabIdx(idx: number = this.activeTabIdx$$.value): number { + if (this.tabs.has(this.activeTabIdx$$.value)) return this.activeTabIdx$$.value; + if (!this.tabs.size) return -1; + const indexes = Array.from(this.tabs.keys()).sort(); + const nextIdx = indexes.find(i => i > idx); + const newIdx = nextIdx ?? (indexes.pop() as number); + this.activeTabIdx$$.next(newIdx); + return newIdx; } public findTabIdx(tab: PrizmTabComponent): number { return Array.from(this.tabs.entries()).find(([, t]) => t === tab)?.[0] ?? -1; } public selectTab(tab: PrizmTabComponent): void { const idx = this.findTabIdx(tab); + if (idx === -1) { return; } @@ -74,7 +97,6 @@ export class PrizmTabsService implements OnDestroy { if (idx === this.activeTabIdx) { return; } - (typeof this.canOpenTab === 'function' ? this.canOpenTab(tab) : of(true)) .pipe( take(1),