juxscript 1.1.31 → 1.1.33
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/lib/components/tabs.d.ts +20 -8
- package/lib/components/tabs.d.ts.map +1 -1
- package/lib/components/tabs.js +145 -118
- package/lib/components/tabs.ts +179 -132
- package/package.json +1 -1
package/lib/components/tabs.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { BaseComponent } from './base/BaseComponent.js';
|
|
1
|
+
import { BaseComponent, BaseState } from './base/BaseComponent.js';
|
|
2
2
|
export interface Tab {
|
|
3
3
|
id: string;
|
|
4
4
|
label: string;
|
|
5
|
-
content: string
|
|
5
|
+
content: string;
|
|
6
6
|
icon?: string;
|
|
7
7
|
}
|
|
8
8
|
export interface TabsOptions {
|
|
@@ -12,25 +12,37 @@ export interface TabsOptions {
|
|
|
12
12
|
style?: string;
|
|
13
13
|
class?: string;
|
|
14
14
|
}
|
|
15
|
-
type TabsState = {
|
|
15
|
+
type TabsState = BaseState & {
|
|
16
16
|
tabs: Tab[];
|
|
17
17
|
activeTab: string;
|
|
18
18
|
variant: string;
|
|
19
|
-
style: string;
|
|
20
|
-
class: string;
|
|
21
19
|
};
|
|
22
20
|
export declare class Tabs extends BaseComponent<TabsState> {
|
|
21
|
+
private _tabsWrapper;
|
|
23
22
|
constructor(id: string, options?: TabsOptions);
|
|
24
23
|
protected getTriggerEvents(): readonly string[];
|
|
25
24
|
protected getCallbackEvents(): readonly string[];
|
|
25
|
+
update(prop: string, value: any): void;
|
|
26
|
+
private _updateActiveTab;
|
|
27
|
+
private _rebuildTabs;
|
|
26
28
|
tabs(value: Tab[]): this;
|
|
27
29
|
addTab(tab: Tab): this;
|
|
30
|
+
removeTab(tabId: string): this;
|
|
28
31
|
activeTab(value: string): this;
|
|
29
32
|
variant(value: 'default' | 'pills' | 'underline'): this;
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Update content of a specific tab by ID
|
|
35
|
+
*/
|
|
36
|
+
updateTabContent(tabId: string, content: string): this;
|
|
37
|
+
/**
|
|
38
|
+
* Get tab by ID
|
|
39
|
+
*/
|
|
40
|
+
getTab(tabId: string): Tab | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Get current active tab
|
|
43
|
+
*/
|
|
44
|
+
getCurrentTab(): Tab | undefined;
|
|
32
45
|
render(targetId?: string): this;
|
|
33
|
-
private _switchTab;
|
|
34
46
|
}
|
|
35
47
|
export declare function tabs(id: string, options?: TabsOptions): Tabs;
|
|
36
48
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tabs.d.ts","sourceRoot":"","sources":["tabs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"tabs.d.ts","sourceRoot":"","sources":["tabs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAMnE,MAAM,WAAW,GAAG;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,WAAW,CAAC;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,KAAK,SAAS,GAAG,SAAS,GAAG;IAC3B,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,qBAAa,IAAK,SAAQ,aAAa,CAAC,SAAS,CAAC;IAChD,OAAO,CAAC,YAAY,CAA4B;gBAEpC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB;IAUjD,SAAS,CAAC,gBAAgB,IAAI,SAAS,MAAM,EAAE;IAI/C,SAAS,CAAC,iBAAiB,IAAI,SAAS,MAAM,EAAE;IAQhD,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,IAAI;IAuBtC,OAAO,CAAC,gBAAgB;IAsBxB,OAAO,CAAC,YAAY;IA6DpB,IAAI,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,IAAI;IAKxB,MAAM,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI;IAKtB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAW9B,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAK9B,OAAO,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,GAAG,WAAW,GAAG,IAAI;IAKvD;;OAEG;IACH,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAetD;;OAEG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS;IAItC;;OAEG;IACH,aAAa,IAAI,GAAG,GAAG,SAAS;IAQhC,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI;CAuEhC;AAED,wBAAgB,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,IAAI,CAEhE"}
|
package/lib/components/tabs.js
CHANGED
|
@@ -11,6 +11,7 @@ export class Tabs extends BaseComponent {
|
|
|
11
11
|
style: options.style ?? '',
|
|
12
12
|
class: options.class ?? ''
|
|
13
13
|
});
|
|
14
|
+
this._tabsWrapper = null;
|
|
14
15
|
}
|
|
15
16
|
getTriggerEvents() {
|
|
16
17
|
return TRIGGER_EVENTS;
|
|
@@ -18,10 +19,98 @@ export class Tabs extends BaseComponent {
|
|
|
18
19
|
getCallbackEvents() {
|
|
19
20
|
return CALLBACK_EVENTS;
|
|
20
21
|
}
|
|
22
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
23
|
+
* REACTIVE UPDATE
|
|
24
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
25
|
+
update(prop, value) {
|
|
26
|
+
super.update(prop, value);
|
|
27
|
+
if (!this._tabsWrapper)
|
|
28
|
+
return;
|
|
29
|
+
switch (prop) {
|
|
30
|
+
case 'activeTab':
|
|
31
|
+
this._updateActiveTab(value);
|
|
32
|
+
break;
|
|
33
|
+
case 'tabs':
|
|
34
|
+
this._rebuildTabs();
|
|
35
|
+
break;
|
|
36
|
+
case 'variant':
|
|
37
|
+
this._tabsWrapper.className = `jux-tabs jux-tabs-${value}`;
|
|
38
|
+
if (this.state.class) {
|
|
39
|
+
this._tabsWrapper.className += ` ${this.state.class}`;
|
|
40
|
+
}
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
_updateActiveTab(tabId) {
|
|
45
|
+
if (!this._tabsWrapper)
|
|
46
|
+
return;
|
|
47
|
+
// Update button states
|
|
48
|
+
this.state.tabs.forEach(tab => {
|
|
49
|
+
const btn = document.getElementById(`${this._id}-${tab.id}-button`);
|
|
50
|
+
if (btn) {
|
|
51
|
+
btn.classList.toggle('jux-tabs-button-active', tab.id === tabId);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
// Update panel visibility
|
|
55
|
+
this.state.tabs.forEach(tab => {
|
|
56
|
+
const panel = document.getElementById(`${this._id}-${tab.id}-panel`);
|
|
57
|
+
if (panel) {
|
|
58
|
+
const isActive = tab.id === tabId;
|
|
59
|
+
panel.classList.toggle('jux-tabs-panel-active', isActive);
|
|
60
|
+
panel.style.display = isActive ? 'block' : 'none';
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
_rebuildTabs() {
|
|
65
|
+
if (!this._tabsWrapper)
|
|
66
|
+
return;
|
|
67
|
+
const tabList = this._tabsWrapper.querySelector('.jux-tabs-list');
|
|
68
|
+
const tabPanels = this._tabsWrapper.querySelector('.jux-tabs-panels');
|
|
69
|
+
if (!tabList || !tabPanels)
|
|
70
|
+
return;
|
|
71
|
+
// Clear existing
|
|
72
|
+
tabList.innerHTML = '';
|
|
73
|
+
tabPanels.innerHTML = '';
|
|
74
|
+
// Rebuild
|
|
75
|
+
this.state.tabs.forEach(tab => {
|
|
76
|
+
// Button with unique ID
|
|
77
|
+
const tabButton = document.createElement('button');
|
|
78
|
+
tabButton.id = `${this._id}-${tab.id}-button`;
|
|
79
|
+
tabButton.className = 'jux-tabs-button';
|
|
80
|
+
tabButton.setAttribute('data-tab-id', tab.id);
|
|
81
|
+
if (tab.id === this.state.activeTab) {
|
|
82
|
+
tabButton.classList.add('jux-tabs-button-active');
|
|
83
|
+
}
|
|
84
|
+
if (tab.icon) {
|
|
85
|
+
const icon = document.createElement('span');
|
|
86
|
+
icon.className = 'jux-tabs-icon';
|
|
87
|
+
icon.textContent = tab.icon;
|
|
88
|
+
tabButton.appendChild(icon);
|
|
89
|
+
}
|
|
90
|
+
tabButton.appendChild(document.createTextNode(tab.label));
|
|
91
|
+
tabButton.addEventListener('click', () => {
|
|
92
|
+
this.state.activeTab = tab.id;
|
|
93
|
+
this._triggerCallback('tabChange', tab.id);
|
|
94
|
+
});
|
|
95
|
+
tabList.appendChild(tabButton);
|
|
96
|
+
// Panel with unique ID
|
|
97
|
+
const tabPanel = document.createElement('div');
|
|
98
|
+
tabPanel.id = `${this._id}-${tab.id}-panel`;
|
|
99
|
+
tabPanel.className = 'jux-tabs-panel';
|
|
100
|
+
tabPanel.setAttribute('data-tab-id', tab.id);
|
|
101
|
+
if (tab.id === this.state.activeTab) {
|
|
102
|
+
tabPanel.classList.add('jux-tabs-panel-active');
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
tabPanel.style.display = 'none';
|
|
106
|
+
}
|
|
107
|
+
tabPanel.innerHTML = tab.content;
|
|
108
|
+
tabPanels.appendChild(tabPanel);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
21
111
|
/* ═════════════════════════════════════════════════════════════════
|
|
22
112
|
* FLUENT API
|
|
23
113
|
* ═════════════════════════════════════════════════════════════════ */
|
|
24
|
-
// ✅ Inherited from BaseComponent
|
|
25
114
|
tabs(value) {
|
|
26
115
|
this.state.tabs = value;
|
|
27
116
|
return this;
|
|
@@ -30,32 +119,48 @@ export class Tabs extends BaseComponent {
|
|
|
30
119
|
this.state.tabs = [...this.state.tabs, tab];
|
|
31
120
|
return this;
|
|
32
121
|
}
|
|
122
|
+
removeTab(tabId) {
|
|
123
|
+
this.state.tabs = this.state.tabs.filter(t => t.id !== tabId);
|
|
124
|
+
// If removing active tab, switch to first tab
|
|
125
|
+
if (this.state.activeTab === tabId && this.state.tabs.length > 0) {
|
|
126
|
+
this.state.activeTab = this.state.tabs[0].id;
|
|
127
|
+
}
|
|
128
|
+
return this;
|
|
129
|
+
}
|
|
33
130
|
activeTab(value) {
|
|
34
131
|
this.state.activeTab = value;
|
|
35
|
-
this._updateActiveTab();
|
|
36
132
|
return this;
|
|
37
133
|
}
|
|
38
134
|
variant(value) {
|
|
39
135
|
this.state.variant = value;
|
|
40
136
|
return this;
|
|
41
137
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
panel.
|
|
54
|
-
|
|
55
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Update content of a specific tab by ID
|
|
140
|
+
*/
|
|
141
|
+
updateTabContent(tabId, content) {
|
|
142
|
+
const tab = this.state.tabs.find(t => t.id === tabId);
|
|
143
|
+
if (!tab)
|
|
144
|
+
return this;
|
|
145
|
+
tab.content = content;
|
|
146
|
+
// Update DOM if rendered
|
|
147
|
+
const panel = document.getElementById(`${this._id}-${tabId}-panel`);
|
|
148
|
+
if (panel) {
|
|
149
|
+
panel.innerHTML = content;
|
|
150
|
+
}
|
|
151
|
+
return this;
|
|
56
152
|
}
|
|
57
|
-
|
|
58
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Get tab by ID
|
|
155
|
+
*/
|
|
156
|
+
getTab(tabId) {
|
|
157
|
+
return this.state.tabs.find(t => t.id === tabId);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get current active tab
|
|
161
|
+
*/
|
|
162
|
+
getCurrentTab() {
|
|
163
|
+
return this.state.tabs.find(t => t.id === this.state.activeTab);
|
|
59
164
|
}
|
|
60
165
|
/* ═════════════════════════════════════════════════════════════════
|
|
61
166
|
* RENDER
|
|
@@ -63,7 +168,6 @@ export class Tabs extends BaseComponent {
|
|
|
63
168
|
render(targetId) {
|
|
64
169
|
const container = this._setupContainer(targetId);
|
|
65
170
|
const { tabs, activeTab, variant, style, class: className } = this.state;
|
|
66
|
-
const hasActiveTabSync = this._syncBindings.some(b => b.property === 'activeTab');
|
|
67
171
|
const wrapper = document.createElement('div');
|
|
68
172
|
wrapper.className = `jux-tabs jux-tabs-${variant}`;
|
|
69
173
|
wrapper.id = this._id;
|
|
@@ -76,125 +180,48 @@ export class Tabs extends BaseComponent {
|
|
|
76
180
|
const tabPanels = document.createElement('div');
|
|
77
181
|
tabPanels.className = 'jux-tabs-panels';
|
|
78
182
|
tabs.forEach(tab => {
|
|
183
|
+
// Tab button with unique ID
|
|
79
184
|
const tabButton = document.createElement('button');
|
|
185
|
+
tabButton.id = `${this._id}-${tab.id}-button`;
|
|
80
186
|
tabButton.className = 'jux-tabs-button';
|
|
81
|
-
tabButton.setAttribute('data-tab', tab.id);
|
|
82
|
-
if (tab.id === activeTab)
|
|
187
|
+
tabButton.setAttribute('data-tab-id', tab.id);
|
|
188
|
+
if (tab.id === activeTab) {
|
|
83
189
|
tabButton.classList.add('jux-tabs-button-active');
|
|
84
|
-
|
|
190
|
+
}
|
|
191
|
+
if (tab.icon) {
|
|
192
|
+
const icon = document.createElement('span');
|
|
193
|
+
icon.className = 'jux-tabs-icon';
|
|
194
|
+
icon.textContent = tab.icon;
|
|
195
|
+
tabButton.appendChild(icon);
|
|
196
|
+
}
|
|
197
|
+
tabButton.appendChild(document.createTextNode(tab.label));
|
|
198
|
+
tabButton.addEventListener('click', () => {
|
|
199
|
+
this.state.activeTab = tab.id;
|
|
200
|
+
this._triggerCallback('tabChange', tab.id);
|
|
201
|
+
});
|
|
85
202
|
tabList.appendChild(tabButton);
|
|
203
|
+
// Tab panel with unique ID
|
|
86
204
|
const tabPanel = document.createElement('div');
|
|
205
|
+
tabPanel.id = `${this._id}-${tab.id}-panel`;
|
|
87
206
|
tabPanel.className = 'jux-tabs-panel';
|
|
88
|
-
tabPanel.setAttribute('data-tab', tab.id);
|
|
207
|
+
tabPanel.setAttribute('data-tab-id', tab.id);
|
|
89
208
|
if (tab.id === activeTab) {
|
|
90
209
|
tabPanel.classList.add('jux-tabs-panel-active');
|
|
91
210
|
}
|
|
92
211
|
else {
|
|
93
212
|
tabPanel.style.display = 'none';
|
|
94
213
|
}
|
|
95
|
-
|
|
96
|
-
tabPanel.innerHTML = tab.content;
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
tabPanel.appendChild(tab.content);
|
|
100
|
-
}
|
|
214
|
+
tabPanel.innerHTML = tab.content;
|
|
101
215
|
tabPanels.appendChild(tabPanel);
|
|
102
216
|
});
|
|
103
217
|
wrapper.appendChild(tabList);
|
|
104
218
|
wrapper.appendChild(tabPanels);
|
|
105
|
-
if (!hasActiveTabSync) {
|
|
106
|
-
tabList.querySelectorAll('.jux-tabs-button').forEach(button => {
|
|
107
|
-
button.addEventListener('click', () => {
|
|
108
|
-
const tabId = button.getAttribute('data-tab');
|
|
109
|
-
this.state.activeTab = tabId;
|
|
110
|
-
this._switchTab(tabId, wrapper);
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
219
|
this._wireStandardEvents(wrapper);
|
|
115
|
-
this.
|
|
116
|
-
if (property === 'activeTab') {
|
|
117
|
-
const transformToState = toState || ((v) => String(v));
|
|
118
|
-
const transformToComponent = toComponent || ((v) => String(v));
|
|
119
|
-
let isUpdating = false;
|
|
120
|
-
stateObj.subscribe((val) => {
|
|
121
|
-
if (isUpdating)
|
|
122
|
-
return;
|
|
123
|
-
const transformed = transformToComponent(val);
|
|
124
|
-
this.state.activeTab = transformed;
|
|
125
|
-
this._switchTab(transformed, wrapper);
|
|
126
|
-
});
|
|
127
|
-
tabList.querySelectorAll('.jux-tabs-button').forEach(button => {
|
|
128
|
-
button.addEventListener('click', () => {
|
|
129
|
-
if (isUpdating)
|
|
130
|
-
return;
|
|
131
|
-
isUpdating = true;
|
|
132
|
-
const tabId = button.getAttribute('data-tab');
|
|
133
|
-
this.state.activeTab = tabId;
|
|
134
|
-
this._switchTab(tabId, wrapper);
|
|
135
|
-
const transformed = transformToState(tabId);
|
|
136
|
-
stateObj.set(transformed);
|
|
137
|
-
setTimeout(() => { isUpdating = false; }, 0);
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
else if (property === 'tabs') {
|
|
142
|
-
const transform = toComponent || ((v) => v);
|
|
143
|
-
stateObj.subscribe((val) => {
|
|
144
|
-
const transformed = transform(val);
|
|
145
|
-
this.state.tabs = transformed;
|
|
146
|
-
tabList.innerHTML = '';
|
|
147
|
-
tabPanels.innerHTML = '';
|
|
148
|
-
transformed.forEach((tab) => {
|
|
149
|
-
const tabButton = document.createElement('button');
|
|
150
|
-
tabButton.className = 'jux-tabs-button';
|
|
151
|
-
tabButton.setAttribute('data-tab', tab.id);
|
|
152
|
-
if (tab.id === this.state.activeTab)
|
|
153
|
-
tabButton.classList.add('jux-tabs-button-active');
|
|
154
|
-
tabButton.textContent = tab.label;
|
|
155
|
-
tabList.appendChild(tabButton);
|
|
156
|
-
const tabPanel = document.createElement('div');
|
|
157
|
-
tabPanel.className = 'jux-tabs-panel';
|
|
158
|
-
tabPanel.setAttribute('data-tab', tab.id);
|
|
159
|
-
if (tab.id === this.state.activeTab) {
|
|
160
|
-
tabPanel.classList.add('jux-tabs-panel-active');
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
tabPanel.style.display = 'none';
|
|
164
|
-
}
|
|
165
|
-
if (typeof tab.content === 'string') {
|
|
166
|
-
tabPanel.innerHTML = tab.content;
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
tabPanel.appendChild(tab.content);
|
|
170
|
-
}
|
|
171
|
-
tabPanels.appendChild(tabPanel);
|
|
172
|
-
});
|
|
173
|
-
tabList.querySelectorAll('.jux-tabs-button').forEach(button => {
|
|
174
|
-
button.addEventListener('click', () => {
|
|
175
|
-
const tabId = button.getAttribute('data-tab');
|
|
176
|
-
this.state.activeTab = tabId;
|
|
177
|
-
this._switchTab(tabId, wrapper);
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
});
|
|
220
|
+
this._wireAllSyncs();
|
|
183
221
|
container.appendChild(wrapper);
|
|
222
|
+
this._tabsWrapper = wrapper;
|
|
184
223
|
return this;
|
|
185
224
|
}
|
|
186
|
-
_switchTab(tabId, wrapper) {
|
|
187
|
-
wrapper.querySelectorAll('.jux-tabs-button').forEach(btn => {
|
|
188
|
-
btn.classList.toggle('jux-tabs-button-active', btn.getAttribute('data-tab') === tabId);
|
|
189
|
-
});
|
|
190
|
-
wrapper.querySelectorAll('.jux-tabs-panel').forEach(panel => {
|
|
191
|
-
const isActive = panel.getAttribute('data-tab') === tabId;
|
|
192
|
-
panel.classList.toggle('jux-tabs-panel-active', isActive);
|
|
193
|
-
panel.style.display = isActive ? 'block' : 'none';
|
|
194
|
-
});
|
|
195
|
-
// 🎯 Fire the tabChange callback event
|
|
196
|
-
this._triggerCallback('tabChange', tabId);
|
|
197
|
-
}
|
|
198
225
|
}
|
|
199
226
|
export function tabs(id, options = {}) {
|
|
200
227
|
return new Tabs(id, options);
|
package/lib/components/tabs.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseComponent } from './base/BaseComponent.js';
|
|
1
|
+
import { BaseComponent, BaseState } from './base/BaseComponent.js';
|
|
2
2
|
|
|
3
3
|
// Event definitions
|
|
4
4
|
const TRIGGER_EVENTS = [] as const;
|
|
@@ -7,7 +7,7 @@ const CALLBACK_EVENTS = ['tabChange'] as const;
|
|
|
7
7
|
export interface Tab {
|
|
8
8
|
id: string;
|
|
9
9
|
label: string;
|
|
10
|
-
content: string
|
|
10
|
+
content: string; // ✅ Only static HTML strings for now
|
|
11
11
|
icon?: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -19,15 +19,15 @@ export interface TabsOptions {
|
|
|
19
19
|
class?: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
type TabsState = {
|
|
22
|
+
type TabsState = BaseState & {
|
|
23
23
|
tabs: Tab[];
|
|
24
24
|
activeTab: string;
|
|
25
25
|
variant: string;
|
|
26
|
-
style: string;
|
|
27
|
-
class: string;
|
|
28
26
|
};
|
|
29
27
|
|
|
30
28
|
export class Tabs extends BaseComponent<TabsState> {
|
|
29
|
+
private _tabsWrapper: HTMLElement | null = null;
|
|
30
|
+
|
|
31
31
|
constructor(id: string, options: TabsOptions = {}) {
|
|
32
32
|
super(id, {
|
|
33
33
|
tabs: options.tabs ?? [],
|
|
@@ -47,10 +47,114 @@ export class Tabs extends BaseComponent<TabsState> {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/* ═════════════════════════════════════════════════════════════════
|
|
50
|
-
*
|
|
50
|
+
* REACTIVE UPDATE
|
|
51
51
|
* ═════════════════════════════════════════════════════════════════ */
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
update(prop: string, value: any): void {
|
|
54
|
+
super.update(prop, value);
|
|
55
|
+
|
|
56
|
+
if (!this._tabsWrapper) return;
|
|
57
|
+
|
|
58
|
+
switch (prop) {
|
|
59
|
+
case 'activeTab':
|
|
60
|
+
this._updateActiveTab(value);
|
|
61
|
+
break;
|
|
62
|
+
|
|
63
|
+
case 'tabs':
|
|
64
|
+
this._rebuildTabs();
|
|
65
|
+
break;
|
|
66
|
+
|
|
67
|
+
case 'variant':
|
|
68
|
+
this._tabsWrapper.className = `jux-tabs jux-tabs-${value}`;
|
|
69
|
+
if (this.state.class) {
|
|
70
|
+
this._tabsWrapper.className += ` ${this.state.class}`;
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private _updateActiveTab(tabId: string): void {
|
|
77
|
+
if (!this._tabsWrapper) return;
|
|
78
|
+
|
|
79
|
+
// Update button states
|
|
80
|
+
this.state.tabs.forEach(tab => {
|
|
81
|
+
const btn = document.getElementById(`${this._id}-${tab.id}-button`);
|
|
82
|
+
if (btn) {
|
|
83
|
+
btn.classList.toggle('jux-tabs-button-active', tab.id === tabId);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Update panel visibility
|
|
88
|
+
this.state.tabs.forEach(tab => {
|
|
89
|
+
const panel = document.getElementById(`${this._id}-${tab.id}-panel`);
|
|
90
|
+
if (panel) {
|
|
91
|
+
const isActive = tab.id === tabId;
|
|
92
|
+
panel.classList.toggle('jux-tabs-panel-active', isActive);
|
|
93
|
+
panel.style.display = isActive ? 'block' : 'none';
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private _rebuildTabs(): void {
|
|
99
|
+
if (!this._tabsWrapper) return;
|
|
100
|
+
|
|
101
|
+
const tabList = this._tabsWrapper.querySelector('.jux-tabs-list');
|
|
102
|
+
const tabPanels = this._tabsWrapper.querySelector('.jux-tabs-panels');
|
|
103
|
+
|
|
104
|
+
if (!tabList || !tabPanels) return;
|
|
105
|
+
|
|
106
|
+
// Clear existing
|
|
107
|
+
tabList.innerHTML = '';
|
|
108
|
+
tabPanels.innerHTML = '';
|
|
109
|
+
|
|
110
|
+
// Rebuild
|
|
111
|
+
this.state.tabs.forEach(tab => {
|
|
112
|
+
// Button with unique ID
|
|
113
|
+
const tabButton = document.createElement('button');
|
|
114
|
+
tabButton.id = `${this._id}-${tab.id}-button`;
|
|
115
|
+
tabButton.className = 'jux-tabs-button';
|
|
116
|
+
tabButton.setAttribute('data-tab-id', tab.id);
|
|
117
|
+
|
|
118
|
+
if (tab.id === this.state.activeTab) {
|
|
119
|
+
tabButton.classList.add('jux-tabs-button-active');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (tab.icon) {
|
|
123
|
+
const icon = document.createElement('span');
|
|
124
|
+
icon.className = 'jux-tabs-icon';
|
|
125
|
+
icon.textContent = tab.icon;
|
|
126
|
+
tabButton.appendChild(icon);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
tabButton.appendChild(document.createTextNode(tab.label));
|
|
130
|
+
|
|
131
|
+
tabButton.addEventListener('click', () => {
|
|
132
|
+
this.state.activeTab = tab.id;
|
|
133
|
+
this._triggerCallback('tabChange', tab.id);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
tabList.appendChild(tabButton);
|
|
137
|
+
|
|
138
|
+
// Panel with unique ID
|
|
139
|
+
const tabPanel = document.createElement('div');
|
|
140
|
+
tabPanel.id = `${this._id}-${tab.id}-panel`;
|
|
141
|
+
tabPanel.className = 'jux-tabs-panel';
|
|
142
|
+
tabPanel.setAttribute('data-tab-id', tab.id);
|
|
143
|
+
|
|
144
|
+
if (tab.id === this.state.activeTab) {
|
|
145
|
+
tabPanel.classList.add('jux-tabs-panel-active');
|
|
146
|
+
} else {
|
|
147
|
+
tabPanel.style.display = 'none';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
tabPanel.innerHTML = tab.content;
|
|
151
|
+
tabPanels.appendChild(tabPanel);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
156
|
+
* FLUENT API
|
|
157
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
54
158
|
|
|
55
159
|
tabs(value: Tab[]): this {
|
|
56
160
|
this.state.tabs = value;
|
|
@@ -62,9 +166,19 @@ export class Tabs extends BaseComponent<TabsState> {
|
|
|
62
166
|
return this;
|
|
63
167
|
}
|
|
64
168
|
|
|
169
|
+
removeTab(tabId: string): this {
|
|
170
|
+
this.state.tabs = this.state.tabs.filter(t => t.id !== tabId);
|
|
171
|
+
|
|
172
|
+
// If removing active tab, switch to first tab
|
|
173
|
+
if (this.state.activeTab === tabId && this.state.tabs.length > 0) {
|
|
174
|
+
this.state.activeTab = this.state.tabs[0].id;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return this;
|
|
178
|
+
}
|
|
179
|
+
|
|
65
180
|
activeTab(value: string): this {
|
|
66
181
|
this.state.activeTab = value;
|
|
67
|
-
this._updateActiveTab();
|
|
68
182
|
return this;
|
|
69
183
|
}
|
|
70
184
|
|
|
@@ -73,24 +187,36 @@ export class Tabs extends BaseComponent<TabsState> {
|
|
|
73
187
|
return this;
|
|
74
188
|
}
|
|
75
189
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
190
|
+
/**
|
|
191
|
+
* Update content of a specific tab by ID
|
|
192
|
+
*/
|
|
193
|
+
updateTabContent(tabId: string, content: string): this {
|
|
194
|
+
const tab = this.state.tabs.find(t => t.id === tabId);
|
|
195
|
+
if (!tab) return this;
|
|
80
196
|
|
|
81
|
-
|
|
82
|
-
btn.classList.toggle('jux-tabs-button-active', btn.getAttribute('data-tab') === this.state.activeTab);
|
|
83
|
-
});
|
|
197
|
+
tab.content = content;
|
|
84
198
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
199
|
+
// Update DOM if rendered
|
|
200
|
+
const panel = document.getElementById(`${this._id}-${tabId}-panel`);
|
|
201
|
+
if (panel) {
|
|
202
|
+
panel.innerHTML = content;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return this;
|
|
90
206
|
}
|
|
91
207
|
|
|
92
|
-
|
|
93
|
-
|
|
208
|
+
/**
|
|
209
|
+
* Get tab by ID
|
|
210
|
+
*/
|
|
211
|
+
getTab(tabId: string): Tab | undefined {
|
|
212
|
+
return this.state.tabs.find(t => t.id === tabId);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get current active tab
|
|
217
|
+
*/
|
|
218
|
+
getCurrentTab(): Tab | undefined {
|
|
219
|
+
return this.state.tabs.find(t => t.id === this.state.activeTab);
|
|
94
220
|
}
|
|
95
221
|
|
|
96
222
|
/* ═════════════════════════════════════════════════════════════════
|
|
@@ -101,7 +227,6 @@ export class Tabs extends BaseComponent<TabsState> {
|
|
|
101
227
|
const container = this._setupContainer(targetId);
|
|
102
228
|
|
|
103
229
|
const { tabs, activeTab, variant, style, class: className } = this.state;
|
|
104
|
-
const hasActiveTabSync = this._syncBindings.some(b => b.property === 'activeTab');
|
|
105
230
|
|
|
106
231
|
const wrapper = document.createElement('div');
|
|
107
232
|
wrapper.className = `jux-tabs jux-tabs-${variant}`;
|
|
@@ -116,136 +241,58 @@ export class Tabs extends BaseComponent<TabsState> {
|
|
|
116
241
|
tabPanels.className = 'jux-tabs-panels';
|
|
117
242
|
|
|
118
243
|
tabs.forEach(tab => {
|
|
244
|
+
// Tab button with unique ID
|
|
119
245
|
const tabButton = document.createElement('button');
|
|
246
|
+
tabButton.id = `${this._id}-${tab.id}-button`;
|
|
120
247
|
tabButton.className = 'jux-tabs-button';
|
|
121
|
-
tabButton.setAttribute('data-tab', tab.id);
|
|
122
|
-
|
|
123
|
-
|
|
248
|
+
tabButton.setAttribute('data-tab-id', tab.id);
|
|
249
|
+
|
|
250
|
+
if (tab.id === activeTab) {
|
|
251
|
+
tabButton.classList.add('jux-tabs-button-active');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (tab.icon) {
|
|
255
|
+
const icon = document.createElement('span');
|
|
256
|
+
icon.className = 'jux-tabs-icon';
|
|
257
|
+
icon.textContent = tab.icon;
|
|
258
|
+
tabButton.appendChild(icon);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
tabButton.appendChild(document.createTextNode(tab.label));
|
|
262
|
+
|
|
263
|
+
tabButton.addEventListener('click', () => {
|
|
264
|
+
this.state.activeTab = tab.id;
|
|
265
|
+
this._triggerCallback('tabChange', tab.id);
|
|
266
|
+
});
|
|
267
|
+
|
|
124
268
|
tabList.appendChild(tabButton);
|
|
125
269
|
|
|
270
|
+
// Tab panel with unique ID
|
|
126
271
|
const tabPanel = document.createElement('div');
|
|
272
|
+
tabPanel.id = `${this._id}-${tab.id}-panel`;
|
|
127
273
|
tabPanel.className = 'jux-tabs-panel';
|
|
128
|
-
tabPanel.setAttribute('data-tab', tab.id);
|
|
274
|
+
tabPanel.setAttribute('data-tab-id', tab.id);
|
|
275
|
+
|
|
129
276
|
if (tab.id === activeTab) {
|
|
130
277
|
tabPanel.classList.add('jux-tabs-panel-active');
|
|
131
278
|
} else {
|
|
132
279
|
tabPanel.style.display = 'none';
|
|
133
280
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
} else {
|
|
137
|
-
tabPanel.appendChild(tab.content);
|
|
138
|
-
}
|
|
281
|
+
|
|
282
|
+
tabPanel.innerHTML = tab.content;
|
|
139
283
|
tabPanels.appendChild(tabPanel);
|
|
140
284
|
});
|
|
141
285
|
|
|
142
286
|
wrapper.appendChild(tabList);
|
|
143
287
|
wrapper.appendChild(tabPanels);
|
|
144
288
|
|
|
145
|
-
if (!hasActiveTabSync) {
|
|
146
|
-
tabList.querySelectorAll('.jux-tabs-button').forEach(button => {
|
|
147
|
-
button.addEventListener('click', () => {
|
|
148
|
-
const tabId = button.getAttribute('data-tab')!;
|
|
149
|
-
this.state.activeTab = tabId;
|
|
150
|
-
this._switchTab(tabId, wrapper);
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
|
|
155
289
|
this._wireStandardEvents(wrapper);
|
|
156
|
-
|
|
157
|
-
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
158
|
-
if (property === 'activeTab') {
|
|
159
|
-
const transformToState = toState || ((v: any) => String(v));
|
|
160
|
-
const transformToComponent = toComponent || ((v: any) => String(v));
|
|
161
|
-
|
|
162
|
-
let isUpdating = false;
|
|
163
|
-
|
|
164
|
-
stateObj.subscribe((val: any) => {
|
|
165
|
-
if (isUpdating) return;
|
|
166
|
-
const transformed = transformToComponent(val);
|
|
167
|
-
this.state.activeTab = transformed;
|
|
168
|
-
this._switchTab(transformed, wrapper);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
tabList.querySelectorAll('.jux-tabs-button').forEach(button => {
|
|
172
|
-
button.addEventListener('click', () => {
|
|
173
|
-
if (isUpdating) return;
|
|
174
|
-
isUpdating = true;
|
|
175
|
-
|
|
176
|
-
const tabId = button.getAttribute('data-tab')!;
|
|
177
|
-
this.state.activeTab = tabId;
|
|
178
|
-
this._switchTab(tabId, wrapper);
|
|
179
|
-
|
|
180
|
-
const transformed = transformToState(tabId);
|
|
181
|
-
stateObj.set(transformed);
|
|
182
|
-
|
|
183
|
-
setTimeout(() => { isUpdating = false; }, 0);
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
else if (property === 'tabs') {
|
|
188
|
-
const transform = toComponent || ((v: any) => v);
|
|
189
|
-
|
|
190
|
-
stateObj.subscribe((val: any) => {
|
|
191
|
-
const transformed = transform(val);
|
|
192
|
-
this.state.tabs = transformed;
|
|
193
|
-
|
|
194
|
-
tabList.innerHTML = '';
|
|
195
|
-
tabPanels.innerHTML = '';
|
|
196
|
-
|
|
197
|
-
transformed.forEach((tab: any) => {
|
|
198
|
-
const tabButton = document.createElement('button');
|
|
199
|
-
tabButton.className = 'jux-tabs-button';
|
|
200
|
-
tabButton.setAttribute('data-tab', tab.id);
|
|
201
|
-
if (tab.id === this.state.activeTab) tabButton.classList.add('jux-tabs-button-active');
|
|
202
|
-
tabButton.textContent = tab.label;
|
|
203
|
-
tabList.appendChild(tabButton);
|
|
204
|
-
|
|
205
|
-
const tabPanel = document.createElement('div');
|
|
206
|
-
tabPanel.className = 'jux-tabs-panel';
|
|
207
|
-
tabPanel.setAttribute('data-tab', tab.id);
|
|
208
|
-
if (tab.id === this.state.activeTab) {
|
|
209
|
-
tabPanel.classList.add('jux-tabs-panel-active');
|
|
210
|
-
} else {
|
|
211
|
-
tabPanel.style.display = 'none';
|
|
212
|
-
}
|
|
213
|
-
if (typeof tab.content === 'string') {
|
|
214
|
-
tabPanel.innerHTML = tab.content;
|
|
215
|
-
} else {
|
|
216
|
-
tabPanel.appendChild(tab.content);
|
|
217
|
-
}
|
|
218
|
-
tabPanels.appendChild(tabPanel);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
tabList.querySelectorAll('.jux-tabs-button').forEach(button => {
|
|
222
|
-
button.addEventListener('click', () => {
|
|
223
|
-
const tabId = button.getAttribute('data-tab')!;
|
|
224
|
-
this.state.activeTab = tabId;
|
|
225
|
-
this._switchTab(tabId, wrapper);
|
|
226
|
-
});
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
});
|
|
290
|
+
this._wireAllSyncs();
|
|
231
291
|
|
|
232
292
|
container.appendChild(wrapper);
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
private _switchTab(tabId: string, wrapper: HTMLElement): void {
|
|
237
|
-
wrapper.querySelectorAll('.jux-tabs-button').forEach(btn => {
|
|
238
|
-
btn.classList.toggle('jux-tabs-button-active', btn.getAttribute('data-tab') === tabId);
|
|
239
|
-
});
|
|
293
|
+
this._tabsWrapper = wrapper;
|
|
240
294
|
|
|
241
|
-
|
|
242
|
-
const isActive = panel.getAttribute('data-tab') === tabId;
|
|
243
|
-
panel.classList.toggle('jux-tabs-panel-active', isActive);
|
|
244
|
-
(panel as HTMLElement).style.display = isActive ? 'block' : 'none';
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
// 🎯 Fire the tabChange callback event
|
|
248
|
-
this._triggerCallback('tabChange', tabId);
|
|
295
|
+
return this;
|
|
249
296
|
}
|
|
250
297
|
}
|
|
251
298
|
|