@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.
- package/README.md +7 -0
- package/eslint.config.js +44 -0
- package/jest.config.ts +21 -0
- package/ng-package.json +7 -0
- package/package.json +9 -0
- package/project.json +36 -0
- package/src/index.ts +8 -0
- package/src/lib/components/tab/rdt-tab.component.html +3 -0
- package/src/lib/components/tab/rdt-tab.component.ts +158 -0
- package/src/lib/components/tab-container/rdt-tab-container.component.html +29 -0
- package/src/lib/components/tab-container/rdt-tab-container.component.scss +155 -0
- package/src/lib/components/tab-container/rdt-tab-container.component.ts +315 -0
- package/src/lib/directives/rdt-destroy-inactive.directive.ts +8 -0
- package/src/lib/directives/rdt-tab-controller.directive.ts +440 -0
- package/src/lib/rdt-tabs-models.ts +12 -0
- package/src/lib/rdt-tabs.module.ts +24 -0
- package/src/lib/strategies/auto-rdt-tabs-shortcut-strategy.ts +81 -0
- package/src/lib/strategies/rdt-tabs-shortcut-strategy.ts +20 -0
- package/src/lib/strategies/static-rdt-tabs-shortcut-strategy.ts +30 -0
- package/src/test-setup.ts +8 -0
- package/tsconfig.json +28 -0
- package/tsconfig.lib.json +17 -0
- package/tsconfig.lib.prod.json +9 -0
- package/tsconfig.spec.json +16 -0
|
@@ -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
|
+
}
|