juxscript 1.1.31 → 1.1.32
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 +21 -8
- package/lib/components/tabs.d.ts.map +1 -1
- package/lib/components/tabs.js +151 -116
- package/lib/components/tabs.ts +182 -131
- 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 | HTMLElement
|
|
5
|
+
content: string | HTMLElement | BaseComponent<any>;
|
|
6
6
|
icon?: string;
|
|
7
7
|
}
|
|
8
8
|
export interface TabsOptions {
|
|
@@ -12,25 +12,38 @@ 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;
|
|
28
|
+
/**
|
|
29
|
+
* ✅ NEW: Smart content rendering
|
|
30
|
+
* Handles strings, DOM elements, and component instances
|
|
31
|
+
*/
|
|
32
|
+
private _renderTabContent;
|
|
26
33
|
tabs(value: Tab[]): this;
|
|
27
34
|
addTab(tab: Tab): this;
|
|
35
|
+
removeTab(tabId: string): this;
|
|
28
36
|
activeTab(value: string): this;
|
|
29
37
|
variant(value: 'default' | 'pills' | 'underline'): this;
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
/**
|
|
39
|
+
* ✅ NEW: Update content of a specific tab
|
|
40
|
+
*/
|
|
41
|
+
updateTabContent(tabId: string, content: string | HTMLElement | BaseComponent<any>): this;
|
|
42
|
+
/**
|
|
43
|
+
* ✅ NEW: Get current tab
|
|
44
|
+
*/
|
|
45
|
+
getCurrentTab(): Tab | undefined;
|
|
32
46
|
render(targetId?: string): this;
|
|
33
|
-
private _switchTab;
|
|
34
47
|
}
|
|
35
48
|
export declare function tabs(id: string, options?: TabsOptions): Tabs;
|
|
36
49
|
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,GAAG,WAAW,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;IACnD,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;IAgBxB,OAAO,CAAC,YAAY;IAuDpB;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAkBzB,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,WAAW,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,IAAI;IAkBzF;;OAEG;IACH,aAAa,IAAI,GAAG,GAAG,SAAS;IAQhC,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI;CAqEhC;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,110 @@ 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); // ✅ Handle base properties
|
|
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._tabsWrapper.querySelectorAll('.jux-tabs-button').forEach(btn => {
|
|
49
|
+
btn.classList.toggle('jux-tabs-button-active', btn.getAttribute('data-tab') === tabId);
|
|
50
|
+
});
|
|
51
|
+
// Update panel visibility
|
|
52
|
+
this._tabsWrapper.querySelectorAll('.jux-tabs-panel').forEach(panel => {
|
|
53
|
+
const isActive = panel.getAttribute('data-tab') === tabId;
|
|
54
|
+
panel.classList.toggle('jux-tabs-panel-active', isActive);
|
|
55
|
+
panel.style.display = isActive ? 'block' : 'none';
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
_rebuildTabs() {
|
|
59
|
+
if (!this._tabsWrapper)
|
|
60
|
+
return;
|
|
61
|
+
const tabList = this._tabsWrapper.querySelector('.jux-tabs-list');
|
|
62
|
+
const tabPanels = this._tabsWrapper.querySelector('.jux-tabs-panels');
|
|
63
|
+
if (!tabList || !tabPanels)
|
|
64
|
+
return;
|
|
65
|
+
// Clear existing
|
|
66
|
+
tabList.innerHTML = '';
|
|
67
|
+
tabPanels.innerHTML = '';
|
|
68
|
+
// Rebuild
|
|
69
|
+
this.state.tabs.forEach(tab => {
|
|
70
|
+
// Button
|
|
71
|
+
const tabButton = document.createElement('button');
|
|
72
|
+
tabButton.className = 'jux-tabs-button';
|
|
73
|
+
tabButton.setAttribute('data-tab', tab.id);
|
|
74
|
+
if (tab.id === this.state.activeTab) {
|
|
75
|
+
tabButton.classList.add('jux-tabs-button-active');
|
|
76
|
+
}
|
|
77
|
+
if (tab.icon) {
|
|
78
|
+
const icon = document.createElement('span');
|
|
79
|
+
icon.className = 'jux-tabs-icon';
|
|
80
|
+
icon.textContent = tab.icon;
|
|
81
|
+
tabButton.appendChild(icon);
|
|
82
|
+
}
|
|
83
|
+
tabButton.appendChild(document.createTextNode(tab.label));
|
|
84
|
+
tabButton.addEventListener('click', () => {
|
|
85
|
+
this.state.activeTab = tab.id;
|
|
86
|
+
this._triggerCallback('tabChange', tab.id);
|
|
87
|
+
});
|
|
88
|
+
tabList.appendChild(tabButton);
|
|
89
|
+
// Panel
|
|
90
|
+
const tabPanel = document.createElement('div');
|
|
91
|
+
tabPanel.className = 'jux-tabs-panel';
|
|
92
|
+
tabPanel.setAttribute('data-tab', tab.id);
|
|
93
|
+
if (tab.id === this.state.activeTab) {
|
|
94
|
+
tabPanel.classList.add('jux-tabs-panel-active');
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
tabPanel.style.display = 'none';
|
|
98
|
+
}
|
|
99
|
+
// ✅ Render content (supports strings, DOM, and components)
|
|
100
|
+
this._renderTabContent(tabPanel, tab.content);
|
|
101
|
+
tabPanels.appendChild(tabPanel);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* ✅ NEW: Smart content rendering
|
|
106
|
+
* Handles strings, DOM elements, and component instances
|
|
107
|
+
*/
|
|
108
|
+
_renderTabContent(panel, content) {
|
|
109
|
+
if (typeof content === 'string') {
|
|
110
|
+
panel.innerHTML = content;
|
|
111
|
+
}
|
|
112
|
+
else if (content instanceof HTMLElement) {
|
|
113
|
+
panel.appendChild(content);
|
|
114
|
+
}
|
|
115
|
+
else if (content && typeof content.render === 'function') {
|
|
116
|
+
// It's a component instance - create a container and render into it
|
|
117
|
+
const componentContainer = document.createElement('div');
|
|
118
|
+
componentContainer.id = `${panel.getAttribute('data-tab')}-content`;
|
|
119
|
+
panel.appendChild(componentContainer);
|
|
120
|
+
content.render(`#${componentContainer.id}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
21
123
|
/* ═════════════════════════════════════════════════════════════════
|
|
22
124
|
* FLUENT API
|
|
23
125
|
* ═════════════════════════════════════════════════════════════════ */
|
|
24
|
-
// ✅ Inherited from BaseComponent
|
|
25
126
|
tabs(value) {
|
|
26
127
|
this.state.tabs = value;
|
|
27
128
|
return this;
|
|
@@ -30,32 +131,45 @@ export class Tabs extends BaseComponent {
|
|
|
30
131
|
this.state.tabs = [...this.state.tabs, tab];
|
|
31
132
|
return this;
|
|
32
133
|
}
|
|
134
|
+
removeTab(tabId) {
|
|
135
|
+
this.state.tabs = this.state.tabs.filter(t => t.id !== tabId);
|
|
136
|
+
// If removing active tab, switch to first tab
|
|
137
|
+
if (this.state.activeTab === tabId && this.state.tabs.length > 0) {
|
|
138
|
+
this.state.activeTab = this.state.tabs[0].id;
|
|
139
|
+
}
|
|
140
|
+
return this;
|
|
141
|
+
}
|
|
33
142
|
activeTab(value) {
|
|
34
143
|
this.state.activeTab = value;
|
|
35
|
-
this._updateActiveTab();
|
|
36
144
|
return this;
|
|
37
145
|
}
|
|
38
146
|
variant(value) {
|
|
39
147
|
this.state.variant = value;
|
|
40
148
|
return this;
|
|
41
149
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
150
|
+
/**
|
|
151
|
+
* ✅ NEW: Update content of a specific tab
|
|
152
|
+
*/
|
|
153
|
+
updateTabContent(tabId, content) {
|
|
154
|
+
const tab = this.state.tabs.find(t => t.id === tabId);
|
|
155
|
+
if (!tab)
|
|
156
|
+
return this;
|
|
157
|
+
tab.content = content;
|
|
158
|
+
// Update DOM if tab is rendered
|
|
159
|
+
if (this._tabsWrapper) {
|
|
160
|
+
const panel = this._tabsWrapper.querySelector(`.jux-tabs-panel[data-tab="${tabId}"]`);
|
|
161
|
+
if (panel) {
|
|
162
|
+
panel.innerHTML = ''; // Clear existing content
|
|
163
|
+
this._renderTabContent(panel, content);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return this;
|
|
56
167
|
}
|
|
57
|
-
|
|
58
|
-
|
|
168
|
+
/**
|
|
169
|
+
* ✅ NEW: Get current tab
|
|
170
|
+
*/
|
|
171
|
+
getCurrentTab() {
|
|
172
|
+
return this.state.tabs.find(t => t.id === this.state.activeTab);
|
|
59
173
|
}
|
|
60
174
|
/* ═════════════════════════════════════════════════════════════════
|
|
61
175
|
* RENDER
|
|
@@ -63,7 +177,6 @@ export class Tabs extends BaseComponent {
|
|
|
63
177
|
render(targetId) {
|
|
64
178
|
const container = this._setupContainer(targetId);
|
|
65
179
|
const { tabs, activeTab, variant, style, class: className } = this.state;
|
|
66
|
-
const hasActiveTabSync = this._syncBindings.some(b => b.property === 'activeTab');
|
|
67
180
|
const wrapper = document.createElement('div');
|
|
68
181
|
wrapper.className = `jux-tabs jux-tabs-${variant}`;
|
|
69
182
|
wrapper.id = this._id;
|
|
@@ -76,13 +189,26 @@ export class Tabs extends BaseComponent {
|
|
|
76
189
|
const tabPanels = document.createElement('div');
|
|
77
190
|
tabPanels.className = 'jux-tabs-panels';
|
|
78
191
|
tabs.forEach(tab => {
|
|
192
|
+
// Tab button
|
|
79
193
|
const tabButton = document.createElement('button');
|
|
80
194
|
tabButton.className = 'jux-tabs-button';
|
|
81
195
|
tabButton.setAttribute('data-tab', tab.id);
|
|
82
|
-
if (tab.id === activeTab)
|
|
196
|
+
if (tab.id === activeTab) {
|
|
83
197
|
tabButton.classList.add('jux-tabs-button-active');
|
|
84
|
-
|
|
198
|
+
}
|
|
199
|
+
if (tab.icon) {
|
|
200
|
+
const icon = document.createElement('span');
|
|
201
|
+
icon.className = 'jux-tabs-icon';
|
|
202
|
+
icon.textContent = tab.icon;
|
|
203
|
+
tabButton.appendChild(icon);
|
|
204
|
+
}
|
|
205
|
+
tabButton.appendChild(document.createTextNode(tab.label));
|
|
206
|
+
tabButton.addEventListener('click', () => {
|
|
207
|
+
this.state.activeTab = tab.id;
|
|
208
|
+
this._triggerCallback('tabChange', tab.id);
|
|
209
|
+
});
|
|
85
210
|
tabList.appendChild(tabButton);
|
|
211
|
+
// Tab panel
|
|
86
212
|
const tabPanel = document.createElement('div');
|
|
87
213
|
tabPanel.className = 'jux-tabs-panel';
|
|
88
214
|
tabPanel.setAttribute('data-tab', tab.id);
|
|
@@ -92,109 +218,18 @@ export class Tabs extends BaseComponent {
|
|
|
92
218
|
else {
|
|
93
219
|
tabPanel.style.display = 'none';
|
|
94
220
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
tabPanel.appendChild(tab.content);
|
|
100
|
-
}
|
|
221
|
+
// ✅ Render content (supports strings, DOM, and components)
|
|
222
|
+
this._renderTabContent(tabPanel, tab.content);
|
|
101
223
|
tabPanels.appendChild(tabPanel);
|
|
102
224
|
});
|
|
103
225
|
wrapper.appendChild(tabList);
|
|
104
226
|
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
227
|
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
|
-
});
|
|
228
|
+
this._wireAllSyncs();
|
|
183
229
|
container.appendChild(wrapper);
|
|
230
|
+
this._tabsWrapper = wrapper;
|
|
184
231
|
return this;
|
|
185
232
|
}
|
|
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
233
|
}
|
|
199
234
|
export function tabs(id, options = {}) {
|
|
200
235
|
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 | HTMLElement
|
|
10
|
+
content: string | HTMLElement | BaseComponent<any>; // ✅ Support component instances
|
|
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,124 @@ 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); // ✅ Handle base properties
|
|
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._tabsWrapper.querySelectorAll('.jux-tabs-button').forEach(btn => {
|
|
81
|
+
btn.classList.toggle('jux-tabs-button-active', btn.getAttribute('data-tab') === tabId);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Update panel visibility
|
|
85
|
+
this._tabsWrapper.querySelectorAll('.jux-tabs-panel').forEach(panel => {
|
|
86
|
+
const isActive = panel.getAttribute('data-tab') === tabId;
|
|
87
|
+
panel.classList.toggle('jux-tabs-panel-active', isActive);
|
|
88
|
+
(panel as HTMLElement).style.display = isActive ? 'block' : 'none';
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private _rebuildTabs(): void {
|
|
93
|
+
if (!this._tabsWrapper) return;
|
|
94
|
+
|
|
95
|
+
const tabList = this._tabsWrapper.querySelector('.jux-tabs-list');
|
|
96
|
+
const tabPanels = this._tabsWrapper.querySelector('.jux-tabs-panels');
|
|
97
|
+
|
|
98
|
+
if (!tabList || !tabPanels) return;
|
|
99
|
+
|
|
100
|
+
// Clear existing
|
|
101
|
+
tabList.innerHTML = '';
|
|
102
|
+
tabPanels.innerHTML = '';
|
|
103
|
+
|
|
104
|
+
// Rebuild
|
|
105
|
+
this.state.tabs.forEach(tab => {
|
|
106
|
+
// Button
|
|
107
|
+
const tabButton = document.createElement('button');
|
|
108
|
+
tabButton.className = 'jux-tabs-button';
|
|
109
|
+
tabButton.setAttribute('data-tab', tab.id);
|
|
110
|
+
if (tab.id === this.state.activeTab) {
|
|
111
|
+
tabButton.classList.add('jux-tabs-button-active');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (tab.icon) {
|
|
115
|
+
const icon = document.createElement('span');
|
|
116
|
+
icon.className = 'jux-tabs-icon';
|
|
117
|
+
icon.textContent = tab.icon;
|
|
118
|
+
tabButton.appendChild(icon);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
tabButton.appendChild(document.createTextNode(tab.label));
|
|
122
|
+
tabButton.addEventListener('click', () => {
|
|
123
|
+
this.state.activeTab = tab.id;
|
|
124
|
+
this._triggerCallback('tabChange', tab.id);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
tabList.appendChild(tabButton);
|
|
128
|
+
|
|
129
|
+
// Panel
|
|
130
|
+
const tabPanel = document.createElement('div');
|
|
131
|
+
tabPanel.className = 'jux-tabs-panel';
|
|
132
|
+
tabPanel.setAttribute('data-tab', tab.id);
|
|
133
|
+
|
|
134
|
+
if (tab.id === this.state.activeTab) {
|
|
135
|
+
tabPanel.classList.add('jux-tabs-panel-active');
|
|
136
|
+
} else {
|
|
137
|
+
tabPanel.style.display = 'none';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ✅ Render content (supports strings, DOM, and components)
|
|
141
|
+
this._renderTabContent(tabPanel, tab.content);
|
|
142
|
+
|
|
143
|
+
tabPanels.appendChild(tabPanel);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* ✅ NEW: Smart content rendering
|
|
149
|
+
* Handles strings, DOM elements, and component instances
|
|
150
|
+
*/
|
|
151
|
+
private _renderTabContent(panel: HTMLElement, content: string | HTMLElement | BaseComponent<any>): void {
|
|
152
|
+
if (typeof content === 'string') {
|
|
153
|
+
panel.innerHTML = content;
|
|
154
|
+
} else if (content instanceof HTMLElement) {
|
|
155
|
+
panel.appendChild(content);
|
|
156
|
+
} else if (content && typeof (content as any).render === 'function') {
|
|
157
|
+
// It's a component instance - create a container and render into it
|
|
158
|
+
const componentContainer = document.createElement('div');
|
|
159
|
+
componentContainer.id = `${panel.getAttribute('data-tab')}-content`;
|
|
160
|
+
panel.appendChild(componentContainer);
|
|
161
|
+
(content as BaseComponent<any>).render(`#${componentContainer.id}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
166
|
+
* FLUENT API
|
|
167
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
54
168
|
|
|
55
169
|
tabs(value: Tab[]): this {
|
|
56
170
|
this.state.tabs = value;
|
|
@@ -62,9 +176,19 @@ export class Tabs extends BaseComponent<TabsState> {
|
|
|
62
176
|
return this;
|
|
63
177
|
}
|
|
64
178
|
|
|
179
|
+
removeTab(tabId: string): this {
|
|
180
|
+
this.state.tabs = this.state.tabs.filter(t => t.id !== tabId);
|
|
181
|
+
|
|
182
|
+
// If removing active tab, switch to first tab
|
|
183
|
+
if (this.state.activeTab === tabId && this.state.tabs.length > 0) {
|
|
184
|
+
this.state.activeTab = this.state.tabs[0].id;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return this;
|
|
188
|
+
}
|
|
189
|
+
|
|
65
190
|
activeTab(value: string): this {
|
|
66
191
|
this.state.activeTab = value;
|
|
67
|
-
this._updateActiveTab();
|
|
68
192
|
return this;
|
|
69
193
|
}
|
|
70
194
|
|
|
@@ -73,24 +197,32 @@ export class Tabs extends BaseComponent<TabsState> {
|
|
|
73
197
|
return this;
|
|
74
198
|
}
|
|
75
199
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
200
|
+
/**
|
|
201
|
+
* ✅ NEW: Update content of a specific tab
|
|
202
|
+
*/
|
|
203
|
+
updateTabContent(tabId: string, content: string | HTMLElement | BaseComponent<any>): this {
|
|
204
|
+
const tab = this.state.tabs.find(t => t.id === tabId);
|
|
205
|
+
if (!tab) return this;
|
|
206
|
+
|
|
207
|
+
tab.content = content;
|
|
208
|
+
|
|
209
|
+
// Update DOM if tab is rendered
|
|
210
|
+
if (this._tabsWrapper) {
|
|
211
|
+
const panel = this._tabsWrapper.querySelector(`.jux-tabs-panel[data-tab="${tabId}"]`);
|
|
212
|
+
if (panel) {
|
|
213
|
+
panel.innerHTML = ''; // Clear existing content
|
|
214
|
+
this._renderTabContent(panel as HTMLElement, content);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
84
217
|
|
|
85
|
-
|
|
86
|
-
const isActive = panel.getAttribute('data-tab') === this.state.activeTab;
|
|
87
|
-
panel.classList.toggle('jux-tabs-panel-active', isActive);
|
|
88
|
-
(panel as HTMLElement).style.display = isActive ? 'block' : 'none';
|
|
89
|
-
});
|
|
218
|
+
return this;
|
|
90
219
|
}
|
|
91
220
|
|
|
92
|
-
|
|
93
|
-
|
|
221
|
+
/**
|
|
222
|
+
* ✅ NEW: Get current tab
|
|
223
|
+
*/
|
|
224
|
+
getCurrentTab(): Tab | undefined {
|
|
225
|
+
return this.state.tabs.find(t => t.id === this.state.activeTab);
|
|
94
226
|
}
|
|
95
227
|
|
|
96
228
|
/* ═════════════════════════════════════════════════════════════════
|
|
@@ -101,7 +233,6 @@ export class Tabs extends BaseComponent<TabsState> {
|
|
|
101
233
|
const container = this._setupContainer(targetId);
|
|
102
234
|
|
|
103
235
|
const { tabs, activeTab, variant, style, class: className } = this.state;
|
|
104
|
-
const hasActiveTabSync = this._syncBindings.some(b => b.property === 'activeTab');
|
|
105
236
|
|
|
106
237
|
const wrapper = document.createElement('div');
|
|
107
238
|
wrapper.className = `jux-tabs jux-tabs-${variant}`;
|
|
@@ -116,136 +247,56 @@ export class Tabs extends BaseComponent<TabsState> {
|
|
|
116
247
|
tabPanels.className = 'jux-tabs-panels';
|
|
117
248
|
|
|
118
249
|
tabs.forEach(tab => {
|
|
250
|
+
// Tab button
|
|
119
251
|
const tabButton = document.createElement('button');
|
|
120
252
|
tabButton.className = 'jux-tabs-button';
|
|
121
253
|
tabButton.setAttribute('data-tab', tab.id);
|
|
122
|
-
if (tab.id === activeTab)
|
|
123
|
-
|
|
254
|
+
if (tab.id === activeTab) {
|
|
255
|
+
tabButton.classList.add('jux-tabs-button-active');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (tab.icon) {
|
|
259
|
+
const icon = document.createElement('span');
|
|
260
|
+
icon.className = 'jux-tabs-icon';
|
|
261
|
+
icon.textContent = tab.icon;
|
|
262
|
+
tabButton.appendChild(icon);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
tabButton.appendChild(document.createTextNode(tab.label));
|
|
266
|
+
tabButton.addEventListener('click', () => {
|
|
267
|
+
this.state.activeTab = tab.id;
|
|
268
|
+
this._triggerCallback('tabChange', tab.id);
|
|
269
|
+
});
|
|
270
|
+
|
|
124
271
|
tabList.appendChild(tabButton);
|
|
125
272
|
|
|
273
|
+
// Tab panel
|
|
126
274
|
const tabPanel = document.createElement('div');
|
|
127
275
|
tabPanel.className = 'jux-tabs-panel';
|
|
128
276
|
tabPanel.setAttribute('data-tab', tab.id);
|
|
277
|
+
|
|
129
278
|
if (tab.id === activeTab) {
|
|
130
279
|
tabPanel.classList.add('jux-tabs-panel-active');
|
|
131
280
|
} else {
|
|
132
281
|
tabPanel.style.display = 'none';
|
|
133
282
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
283
|
+
|
|
284
|
+
// ✅ Render content (supports strings, DOM, and components)
|
|
285
|
+
this._renderTabContent(tabPanel, tab.content);
|
|
286
|
+
|
|
139
287
|
tabPanels.appendChild(tabPanel);
|
|
140
288
|
});
|
|
141
289
|
|
|
142
290
|
wrapper.appendChild(tabList);
|
|
143
291
|
wrapper.appendChild(tabPanels);
|
|
144
292
|
|
|
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
293
|
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
|
-
});
|
|
294
|
+
this._wireAllSyncs();
|
|
231
295
|
|
|
232
296
|
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
|
-
});
|
|
297
|
+
this._tabsWrapper = wrapper;
|
|
240
298
|
|
|
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);
|
|
299
|
+
return this;
|
|
249
300
|
}
|
|
250
301
|
}
|
|
251
302
|
|