@ngrdt/tabs 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,315 @@
1
+ import {
2
+ AfterContentInit,
3
+ ChangeDetectionStrategy,
4
+ ChangeDetectorRef,
5
+ Component,
6
+ ContentChildren,
7
+ DestroyRef,
8
+ ElementRef,
9
+ EventEmitter,
10
+ HostBinding,
11
+ HostListener,
12
+ inject,
13
+ Input,
14
+ Output,
15
+ QueryList,
16
+ Renderer2,
17
+ TemplateRef,
18
+ ViewChild,
19
+ ViewChildren,
20
+ ViewEncapsulation,
21
+ } from '@angular/core';
22
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
23
+ import {
24
+ canTransition$,
25
+ RDT_GUARDED_COMPONENT,
26
+ RdtChildDirective,
27
+ } from '@ngrdt/core';
28
+ import { RdtHTMLUtils } from '@ngrdt/utils';
29
+ import { timer } from 'rxjs';
30
+ import { RdtTabControllerDirective } from '../../directives/rdt-tab-controller.directive';
31
+ import { RdtTabContainerState, RdtTabState } from '../../rdt-tabs-models';
32
+ import { RdtTabComponent } from '../tab/rdt-tab.component';
33
+
34
+ @Component({
35
+ selector: 'rdt-tab-container',
36
+ templateUrl: './rdt-tab-container.component.html',
37
+ styleUrls: ['./rdt-tab-container.component.scss'],
38
+ changeDetection: ChangeDetectionStrategy.OnPush,
39
+ providers: [
40
+ {
41
+ provide: RDT_GUARDED_COMPONENT,
42
+ useExisting: RdtTabContainerComponent,
43
+ },
44
+ ],
45
+ hostDirectives: [RdtChildDirective],
46
+ encapsulation: ViewEncapsulation.None,
47
+ })
48
+ export class RdtTabContainerComponent implements AfterContentInit {
49
+ readonly destroyRef = inject(DestroyRef);
50
+
51
+ @Input()
52
+ stateId: any = null;
53
+
54
+ @Input()
55
+ set value(val: string) {
56
+ if (this.activeTab && this.activeTab.stateId !== val) {
57
+ // TODO
58
+ //this.activeTab.onViewWillLeave();
59
+ }
60
+ this._value = val;
61
+ this.setCurrent();
62
+ }
63
+ get value() {
64
+ return this._value;
65
+ }
66
+ private _value!: string;
67
+
68
+ @Input()
69
+ disabled = false;
70
+
71
+ @Input()
72
+ formAutofocus = true;
73
+
74
+ @Input()
75
+ panelPosition: 'left' | 'right' | 'top' | 'bottom' = 'top';
76
+
77
+ @Output()
78
+ valueChange = new EventEmitter();
79
+
80
+ @ContentChildren(RdtTabComponent)
81
+ tabsList!: QueryList<RdtTabComponent>;
82
+
83
+ @ViewChildren('headerButton')
84
+ tabHeaderButtons?: QueryList<ElementRef<HTMLElement>>;
85
+
86
+ @ViewChild('tabContent', { static: true })
87
+ tabContent!: ElementRef<HTMLElement>;
88
+
89
+ @HostBinding('class.active')
90
+ private _active = false;
91
+ get active() {
92
+ return this._active;
93
+ }
94
+ set active(value: boolean) {
95
+ this._active = value;
96
+ this.cd.markForCheck();
97
+ }
98
+
99
+ get isTopLevel(): boolean {
100
+ return !this.parentTab;
101
+ }
102
+
103
+ readonly parentTab = inject(RdtTabComponent, { optional: true });
104
+ readonly controller = inject(RdtTabControllerDirective, {
105
+ optional: true,
106
+ });
107
+ readonly cd = inject(ChangeDetectorRef);
108
+ readonly elRef = inject(ElementRef);
109
+ protected readonly renderer = inject(Renderer2);
110
+
111
+ protected currentTemplate: TemplateRef<any> | null = null;
112
+ protected currentTab: RdtTabComponent | null = null;
113
+
114
+ @HostBinding('class')
115
+ get classes() {
116
+ return `dp3-tab-container panel-${this.panelPosition}`;
117
+ }
118
+
119
+ @HostBinding('class.panel-horizontal')
120
+ get isHorizontal() {
121
+ return this.panelPosition === 'top' || this.panelPosition === 'bottom';
122
+ }
123
+
124
+ @HostBinding('class.panel-vertical')
125
+ get isVertical() {
126
+ return !this.isHorizontal;
127
+ }
128
+
129
+ @HostBinding('attr.role')
130
+ private attrRole = 'tablist';
131
+
132
+ get hasNonDisabledTabs() {
133
+ return this.tabsList.some((tab) => !tab.disabled);
134
+ }
135
+
136
+ get activeTab() {
137
+ return this.getTabByValue(this.value);
138
+ }
139
+
140
+ getTabByValue(value: string) {
141
+ return this.tabsList?.find((tab) => tab.stateId === value);
142
+ }
143
+
144
+ ngAfterContentInit() {
145
+ this.controller?.onTabContainerChanges();
146
+ timer(0)
147
+ .pipe(takeUntilDestroyed(this.destroyRef))
148
+ .subscribe(() => this.setCurrent());
149
+
150
+ this.tabsList.changes
151
+ .pipe(takeUntilDestroyed(this.destroyRef))
152
+ .subscribe(() => {
153
+ this.controller?.onTabChanges();
154
+ this.setCurrent();
155
+ this.cd.markForCheck();
156
+ });
157
+
158
+ if (this.stateId === null && this.controller) {
159
+ const id = this.controller.getTabContainerIndex(this);
160
+ this.stateId = `tab-container-${id}`;
161
+ }
162
+ }
163
+
164
+ getState(): RdtTabContainerState {
165
+ return {
166
+ id: this.stateId,
167
+ value: this.value,
168
+ tabStates: this.tabsList.map((tab) => tab.getState()),
169
+ };
170
+ }
171
+
172
+ applyState(state: RdtTabContainerState) {
173
+ this.value = state.value;
174
+ this.scrollCurrentButtonIntoView();
175
+
176
+ const states: (RdtTabState | null)[] = [...state.tabStates];
177
+ const tabs: (RdtTabComponent | null)[] = this.tabsList.toArray();
178
+ const minLen = Math.min(states.length, tabs.length);
179
+
180
+ for (let i = 0; i < minLen; i++) {
181
+ if (tabs[i]!.stateId === states[i]!.id) {
182
+ tabs[i]!.applyState(states[i]!);
183
+ tabs[i] = states[i] = null;
184
+ }
185
+ }
186
+ tabs.forEach((tab, tabI) => {
187
+ if (!tab) {
188
+ return;
189
+ }
190
+ for (let offset = 0; offset < states.length; offset++) {
191
+ const prev = tabI - offset;
192
+ const next = tabI + offset;
193
+ if (prev > 0 && states[prev]?.id === tab.stateId) {
194
+ tab.applyState(states[prev]!);
195
+ states[prev] = null;
196
+ return;
197
+ } else if (next < states.length && states[next]?.id === tab.stateId) {
198
+ tab.applyState(states[next]!);
199
+ states[next] = null;
200
+ return;
201
+ }
202
+ }
203
+ });
204
+ }
205
+
206
+ scrollIntoView() {
207
+ this.elRef?.nativeElement?.scrollIntoView({ behavior: 'smooth' });
208
+ }
209
+
210
+ activateTab(value: string) {
211
+ const nextTab = this.getTabByValue(value);
212
+
213
+ canTransition$(this.activeTab?.contDir, nextTab?.contDir).subscribe(
214
+ (res) => {
215
+ if (res) {
216
+ this.value = value;
217
+ this.valueChange.emit(value);
218
+ }
219
+ }
220
+ );
221
+ }
222
+
223
+ scrollCurrentButtonIntoView() {
224
+ if (!this.tabsList || !this.tabHeaderButtons) {
225
+ return;
226
+ }
227
+ const tabIndex = this.tabsList
228
+ .toArray()
229
+ .findIndex((tab) => tab.stateId === this.value);
230
+ const buttonRef = this.tabHeaderButtons.get(tabIndex);
231
+ if (buttonRef) {
232
+ RdtHTMLUtils.scrollIntoViewHorizontallyWithinParent(
233
+ buttonRef.nativeElement
234
+ );
235
+ }
236
+ }
237
+
238
+ @HostListener('click', ['$event'])
239
+ private onClick(event: PointerEvent) {
240
+ this.controller?.activateTabContainerFromClick(this, event.timeStamp);
241
+ }
242
+
243
+ private setCurrent() {
244
+ if (!this.tabsList) {
245
+ return;
246
+ }
247
+ if (this.value == undefined) {
248
+ const firstEnabled = this.getFirstEnabledTab();
249
+ if (firstEnabled) {
250
+ this.value = firstEnabled.stateId;
251
+ } else {
252
+ return;
253
+ }
254
+ }
255
+
256
+ const found = this.tabsList.find((tab) => tab.stateId === this.value);
257
+ if (!found) {
258
+ const first = this.getFirstEnabledTab();
259
+ if (first) {
260
+ this.activateTab(first.stateId);
261
+ return;
262
+ } else {
263
+ console.error(
264
+ 'Active tab not found.',
265
+ this.tabsList.toArray(),
266
+ this.value
267
+ );
268
+ return;
269
+ }
270
+ }
271
+ this.setTab(found);
272
+ }
273
+
274
+ private setTab(tab: RdtTabComponent) {
275
+ if (this.currentTab !== tab) {
276
+ this.solidifyTabContentSize();
277
+ this.currentTab = tab;
278
+ this.currentTemplate = tab.templateRef;
279
+ this.cd.markForCheck();
280
+ //tab.onViewWillEnter();
281
+ }
282
+
283
+ if (this.formAutofocus && tab.formAutofocus) {
284
+ // Wait until template is attached to DOM
285
+ timer(0)
286
+ .pipe(
287
+ takeUntilDestroyed(this.destroyRef),
288
+ takeUntilDestroyed(tab.destroyRef)
289
+ )
290
+ .subscribe(() => tab.focusForm());
291
+ }
292
+
293
+ setTimeout(() => this.releaseTabContentSize());
294
+ }
295
+
296
+ // Swapping template in template outlet will cause large css recalc
297
+ // fix size to previous tab and in case they are the same size
298
+ // the recalc isn't noticable
299
+ private solidifyTabContentSize() {
300
+ const el = this.tabContent.nativeElement;
301
+ const bb = el.getBoundingClientRect();
302
+ this.renderer.setStyle(el, 'width', bb.width + 'px');
303
+ this.renderer.setStyle(el, 'height', bb.height + 'px');
304
+ }
305
+
306
+ private releaseTabContentSize() {
307
+ const el = this.tabContent.nativeElement;
308
+ this.renderer.removeStyle(el, 'width');
309
+ this.renderer.removeStyle(el, 'height');
310
+ }
311
+
312
+ private getFirstEnabledTab() {
313
+ return this.tabsList.find((t) => !t.disabled);
314
+ }
315
+ }
@@ -0,0 +1,8 @@
1
+ import { Directive, inject, TemplateRef } from '@angular/core';
2
+
3
+ @Directive({
4
+ selector: '[rdtDestroyInactive]',
5
+ })
6
+ export class RdtDestroyInactiveDirective {
7
+ public readonly template = inject(TemplateRef);
8
+ }