@haiyue/ui 0.1.0
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/LICENSE +21 -0
- package/README.md +9 -0
- package/dist/checkbox.d.ts +30 -0
- package/dist/checkbox.js +204 -0
- package/dist/context-menu.d.ts +41 -0
- package/dist/context-menu.js +225 -0
- package/dist/dialog.d.ts +30 -0
- package/dist/dialog.js +183 -0
- package/dist/dropdown.d.ts +45 -0
- package/dist/dropdown.js +228 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +110 -0
- package/dist/input.d.ts +30 -0
- package/dist/input.js +131 -0
- package/dist/menu-shared.d.ts +14 -0
- package/dist/menu-shared.js +54 -0
- package/dist/radio.d.ts +30 -0
- package/dist/radio.js +205 -0
- package/dist/select.d.ts +40 -0
- package/dist/select.js +253 -0
- package/dist/split.d.ts +46 -0
- package/dist/split.js +277 -0
- package/dist/tabs.d.ts +28 -0
- package/dist/tabs.js +161 -0
- package/dist/tooltip.d.ts +37 -0
- package/dist/tooltip.js +286 -0
- package/dist/tree-model.d.ts +35 -0
- package/dist/tree-model.js +281 -0
- package/dist/tree-node.d.ts +11 -0
- package/dist/tree-node.js +63 -0
- package/dist/tree-types.d.ts +41 -0
- package/dist/tree-types.js +2 -0
- package/dist/tree.d.ts +102 -0
- package/dist/tree.js +942 -0
- package/package.json +46 -0
- package/src/checkbox.ts +215 -0
- package/src/context-menu.ts +248 -0
- package/src/dialog.ts +195 -0
- package/src/dropdown.ts +249 -0
- package/src/index.ts +192 -0
- package/src/input.ts +150 -0
- package/src/menu-shared.ts +65 -0
- package/src/radio.ts +216 -0
- package/src/select.ts +271 -0
- package/src/split.ts +306 -0
- package/src/tabs.ts +182 -0
- package/src/tooltip.ts +321 -0
- package/src/tree-model.ts +305 -0
- package/src/tree-node.ts +67 -0
- package/src/tree-types.ts +47 -0
- package/src/tree.ts +1002 -0
package/src/split.ts
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
export type GESplitDirection = 'horizontal' | 'vertical';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_BAR_SIZE = 6;
|
|
4
|
+
|
|
5
|
+
export interface GESplitRatioChangeDetail {
|
|
6
|
+
ratio: number;
|
|
7
|
+
direction: GESplitDirection;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class GESplit extends HTMLElement {
|
|
11
|
+
private readonly _style = document.createElement('style');
|
|
12
|
+
private readonly _first = document.createElement('div');
|
|
13
|
+
private readonly _second = document.createElement('div');
|
|
14
|
+
private readonly _bar = document.createElement('div');
|
|
15
|
+
private readonly _resizeObserver = new ResizeObserver(() => this._scheduleLayout());
|
|
16
|
+
private _dragging = false;
|
|
17
|
+
private _pointerId = -1;
|
|
18
|
+
private _layoutFrame = 0;
|
|
19
|
+
|
|
20
|
+
static get observedAttributes(): string[] {
|
|
21
|
+
return ['direction', 'ratio', 'min-first', 'min-second', 'bar-size'];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
constructor() {
|
|
25
|
+
super();
|
|
26
|
+
const root = this.attachShadow({ mode: 'open' });
|
|
27
|
+
this._style.textContent = `
|
|
28
|
+
:host {
|
|
29
|
+
box-sizing: border-box;
|
|
30
|
+
display: grid;
|
|
31
|
+
width: 100%;
|
|
32
|
+
height: 100%;
|
|
33
|
+
min-width: 0;
|
|
34
|
+
min-height: 0;
|
|
35
|
+
overflow: hidden;
|
|
36
|
+
color: var(--ge-split-text-color, var(--ge-text-color, #d8e2f2));
|
|
37
|
+
background: var(--ge-split-bg-color, var(--ge-surface-color, #121822));
|
|
38
|
+
}
|
|
39
|
+
.pane {
|
|
40
|
+
min-width: 0;
|
|
41
|
+
min-height: 0;
|
|
42
|
+
overflow: auto;
|
|
43
|
+
background: var(--ge-split-pane-bg-color, transparent);
|
|
44
|
+
}
|
|
45
|
+
.bar {
|
|
46
|
+
background: var(--ge-split-bar-color, var(--ge-border-strong-color, #273142));
|
|
47
|
+
position: relative;
|
|
48
|
+
z-index: 1;
|
|
49
|
+
transition: background var(--ge-split-bar-transition-duration, 0.12s) ease;
|
|
50
|
+
}
|
|
51
|
+
.bar:focus {
|
|
52
|
+
outline: var(--ge-split-focus-ring-width, 1px) solid var(--ge-split-focus-ring-color, var(--ge-focus-border-color, var(--ge-accent-color, #3d6fa8)));
|
|
53
|
+
outline-offset: -1px;
|
|
54
|
+
}
|
|
55
|
+
.bar:hover,
|
|
56
|
+
.bar.dragging {
|
|
57
|
+
background: var(--ge-split-bar-active-color, var(--ge-accent-color, #3d6fa8));
|
|
58
|
+
}
|
|
59
|
+
:host([direction="vertical"]) .bar {
|
|
60
|
+
cursor: row-resize;
|
|
61
|
+
}
|
|
62
|
+
:host(:not([direction="vertical"])) .bar {
|
|
63
|
+
cursor: col-resize;
|
|
64
|
+
}
|
|
65
|
+
:host([direction="vertical"]) .bar::before {
|
|
66
|
+
content: "";
|
|
67
|
+
position: absolute;
|
|
68
|
+
left: 0;
|
|
69
|
+
right: 0;
|
|
70
|
+
top: calc(var(--ge-split-bar-hit-outset, 3px) * -1);
|
|
71
|
+
bottom: calc(var(--ge-split-bar-hit-outset, 3px) * -1);
|
|
72
|
+
}
|
|
73
|
+
:host(:not([direction="vertical"])) .bar::before {
|
|
74
|
+
content: "";
|
|
75
|
+
position: absolute;
|
|
76
|
+
top: 0;
|
|
77
|
+
bottom: 0;
|
|
78
|
+
left: calc(var(--ge-split-bar-hit-outset, 3px) * -1);
|
|
79
|
+
right: calc(var(--ge-split-bar-hit-outset, 3px) * -1);
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
this._first.className = 'pane first';
|
|
84
|
+
this._second.className = 'pane second';
|
|
85
|
+
this._bar.className = 'bar';
|
|
86
|
+
this._bar.tabIndex = 0;
|
|
87
|
+
this._bar.setAttribute('role', 'separator');
|
|
88
|
+
this._bar.setAttribute('aria-valuemin', '0');
|
|
89
|
+
this._bar.setAttribute('aria-valuemax', '100');
|
|
90
|
+
this._first.append(this._createSlot('first'));
|
|
91
|
+
this._second.append(this._createSlot('second'));
|
|
92
|
+
root.append(this._style, this._first, this._bar, this._second);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
connectedCallback(): void {
|
|
96
|
+
if (!this.hasAttribute('direction')) this.setAttribute('direction', 'horizontal');
|
|
97
|
+
if (!this.hasAttribute('ratio')) this.setAttribute('ratio', '0.5');
|
|
98
|
+
this._bar.addEventListener('pointerdown', this._onPointerDown);
|
|
99
|
+
this._bar.addEventListener('keydown', this._onKeyDown);
|
|
100
|
+
this._resizeObserver.observe(this);
|
|
101
|
+
this._syncAria();
|
|
102
|
+
this._scheduleLayout();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
disconnectedCallback(): void {
|
|
106
|
+
this._resizeObserver.disconnect();
|
|
107
|
+
this._bar.removeEventListener('pointerdown', this._onPointerDown);
|
|
108
|
+
this._bar.removeEventListener('keydown', this._onKeyDown);
|
|
109
|
+
window.removeEventListener('pointermove', this._onPointerMove);
|
|
110
|
+
window.removeEventListener('pointerup', this._onPointerUp);
|
|
111
|
+
window.removeEventListener('pointercancel', this._onPointerUp);
|
|
112
|
+
if (this._layoutFrame) {
|
|
113
|
+
cancelAnimationFrame(this._layoutFrame);
|
|
114
|
+
this._layoutFrame = 0;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
attributeChangedCallback(): void {
|
|
119
|
+
this._syncAria();
|
|
120
|
+
this._scheduleLayout();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
get direction(): GESplitDirection {
|
|
124
|
+
return this.getAttribute('direction') === 'vertical' ? 'vertical' : 'horizontal';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
set direction(value: GESplitDirection) {
|
|
128
|
+
this.setAttribute('direction', value);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
get ratio(): number {
|
|
132
|
+
return this._clampRatio(Number(this.getAttribute('ratio') ?? 0.5));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
set ratio(value: number) {
|
|
136
|
+
this.setAttribute('ratio', String(this._clampRatio(value)));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
get minFirst(): number {
|
|
140
|
+
return Math.max(0, Number(this.getAttribute('min-first') ?? 80));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
set minFirst(value: number) {
|
|
144
|
+
this.setAttribute('min-first', String(Math.max(0, value)));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
get minSecond(): number {
|
|
148
|
+
return Math.max(0, Number(this.getAttribute('min-second') ?? 80));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
set minSecond(value: number) {
|
|
152
|
+
this.setAttribute('min-second', String(Math.max(0, value)));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
get barSize(): number {
|
|
156
|
+
const value = Number(this.getAttribute('bar-size') ?? DEFAULT_BAR_SIZE);
|
|
157
|
+
return Number.isFinite(value) ? Math.max(1, value) : DEFAULT_BAR_SIZE;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
set barSize(value: number) {
|
|
161
|
+
this.setAttribute('bar-size', String(Math.max(1, value)));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private _createSlot(name: string): HTMLSlotElement {
|
|
165
|
+
const slot = document.createElement('slot');
|
|
166
|
+
slot.name = name;
|
|
167
|
+
return slot;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private _onPointerDown = (event: PointerEvent): void => {
|
|
171
|
+
this._dragging = true;
|
|
172
|
+
this._pointerId = event.pointerId;
|
|
173
|
+
this._bar.classList.add('dragging');
|
|
174
|
+
this._bar.setPointerCapture(event.pointerId);
|
|
175
|
+
window.addEventListener('pointermove', this._onPointerMove);
|
|
176
|
+
window.addEventListener('pointerup', this._onPointerUp);
|
|
177
|
+
window.addEventListener('pointercancel', this._onPointerUp);
|
|
178
|
+
event.preventDefault();
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
private _onPointerMove = (event: PointerEvent): void => {
|
|
182
|
+
if (!this._dragging || event.pointerId !== this._pointerId) return;
|
|
183
|
+
const rect = this.getBoundingClientRect();
|
|
184
|
+
const size = this._getLayoutSize();
|
|
185
|
+
const available = this._getAvailablePaneSize(size);
|
|
186
|
+
if (available <= 0) return;
|
|
187
|
+
|
|
188
|
+
// Pointer coordinates and DOMRect are visual (and may be transformed or
|
|
189
|
+
// fractional), while CSS layout consumers usually observe integer
|
|
190
|
+
// clientWidth/clientHeight. Map between the two coordinate spaces before
|
|
191
|
+
// snapping the divider to the layout pixel grid.
|
|
192
|
+
const visualSize = this.direction === 'vertical' ? rect.height : rect.width;
|
|
193
|
+
const visualOffset = this.direction === 'vertical'
|
|
194
|
+
? event.clientY - rect.top
|
|
195
|
+
: event.clientX - rect.left;
|
|
196
|
+
const offset = visualSize > 0 ? visualOffset * size / visualSize : 0;
|
|
197
|
+
const firstSize = this._snapFirstSize(
|
|
198
|
+
offset - this._getLayoutBarSize() / 2,
|
|
199
|
+
available,
|
|
200
|
+
);
|
|
201
|
+
this.ratio = firstSize / available;
|
|
202
|
+
this._emitRatioChange();
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
private _onKeyDown = (event: KeyboardEvent): void => {
|
|
206
|
+
const direction = this.direction;
|
|
207
|
+
const backward = direction === 'vertical' ? event.key === 'ArrowUp' : event.key === 'ArrowLeft';
|
|
208
|
+
const forward = direction === 'vertical' ? event.key === 'ArrowDown' : event.key === 'ArrowRight';
|
|
209
|
+
if (!backward && !forward && event.key !== 'Home' && event.key !== 'End') return;
|
|
210
|
+
|
|
211
|
+
event.preventDefault();
|
|
212
|
+
const step = event.shiftKey ? 0.1 : 0.02;
|
|
213
|
+
if (event.key === 'Home') this.ratio = 0;
|
|
214
|
+
else if (event.key === 'End') this.ratio = 1;
|
|
215
|
+
else this.ratio = this.ratio + (forward ? step : -step);
|
|
216
|
+
this._emitRatioChange();
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
private _onPointerUp = (event: PointerEvent): void => {
|
|
220
|
+
if (!this._dragging || event.pointerId !== this._pointerId) return;
|
|
221
|
+
this._dragging = false;
|
|
222
|
+
this._pointerId = -1;
|
|
223
|
+
this._bar.classList.remove('dragging');
|
|
224
|
+
window.removeEventListener('pointermove', this._onPointerMove);
|
|
225
|
+
window.removeEventListener('pointerup', this._onPointerUp);
|
|
226
|
+
window.removeEventListener('pointercancel', this._onPointerUp);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
private _applyLayout(): void {
|
|
230
|
+
this._layoutFrame = 0;
|
|
231
|
+
const size = this._getLayoutSize();
|
|
232
|
+
const available = this._getAvailablePaneSize(size);
|
|
233
|
+
const firstSize = available > 0 ? this._snapFirstSize(this.ratio * available, available) : 0;
|
|
234
|
+
const secondSize = Math.max(0, available - firstSize);
|
|
235
|
+
const barSize = `${this._getLayoutBarSize()}px`;
|
|
236
|
+
if (this.direction === 'vertical') {
|
|
237
|
+
this.style.gridTemplateColumns = 'minmax(0, 1fr)';
|
|
238
|
+
this.style.gridTemplateRows = `${firstSize}px ${barSize} minmax(0, ${secondSize}px)`;
|
|
239
|
+
} else {
|
|
240
|
+
this.style.gridTemplateRows = 'minmax(0, 1fr)';
|
|
241
|
+
this.style.gridTemplateColumns = `${firstSize}px ${barSize} minmax(0, ${secondSize}px)`;
|
|
242
|
+
}
|
|
243
|
+
this._syncAria();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private _clampRatio(value: number): number {
|
|
247
|
+
if (!Number.isFinite(value)) return 0.5;
|
|
248
|
+
return Math.max(0, Math.min(1, value));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private _scheduleLayout(): void {
|
|
252
|
+
if (!this.isConnected) return;
|
|
253
|
+
if (this._layoutFrame) return;
|
|
254
|
+
this._layoutFrame = requestAnimationFrame(() => this._applyLayout());
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private _syncAria(): void {
|
|
258
|
+
const direction = this.direction;
|
|
259
|
+
this._bar.setAttribute('aria-orientation', direction === 'vertical' ? 'horizontal' : 'vertical');
|
|
260
|
+
this._bar.setAttribute('aria-valuenow', String(Math.round(this.ratio * 100)));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private _emitRatioChange(): void {
|
|
264
|
+
this._syncAria();
|
|
265
|
+
this.dispatchEvent(new CustomEvent<GESplitRatioChangeDetail>('ratio-change', {
|
|
266
|
+
detail: { ratio: this.ratio, direction: this.direction },
|
|
267
|
+
bubbles: true,
|
|
268
|
+
composed: true,
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private _getAvailablePaneSize(size: number): number {
|
|
273
|
+
return Math.max(0, size - this._getLayoutBarSize());
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private _getLayoutSize(): number {
|
|
277
|
+
return this.direction === 'vertical' ? this.clientHeight : this.clientWidth;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private _getLayoutBarSize(): number {
|
|
281
|
+
return Math.max(1, Math.round(this.barSize));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private _snapFirstSize(value: number, available: number): number {
|
|
285
|
+
if (available <= 0) return 0;
|
|
286
|
+
const clamped = this._clampFirstSize(value, available);
|
|
287
|
+
const minFirst = Math.ceil(Math.min(this.minFirst, available));
|
|
288
|
+
const minSecond = Math.ceil(Math.min(this.minSecond, Math.max(0, available - minFirst)));
|
|
289
|
+
const maxFirst = Math.max(minFirst, available - minSecond);
|
|
290
|
+
return Math.max(minFirst, Math.min(maxFirst, Math.round(clamped)));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private _clampFirstSize(value: number, available: number): number {
|
|
294
|
+
if (available <= 0) return 0;
|
|
295
|
+
const minFirst = Math.min(this.minFirst, available);
|
|
296
|
+
const minSecond = Math.min(this.minSecond, Math.max(0, available - minFirst));
|
|
297
|
+
const maxFirst = Math.max(minFirst, available - minSecond);
|
|
298
|
+
return Math.max(minFirst, Math.min(maxFirst, value));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function defineSplitComponents(): void {
|
|
303
|
+
if (!customElements.get('ge-split')) {
|
|
304
|
+
customElements.define('ge-split', GESplit);
|
|
305
|
+
}
|
|
306
|
+
}
|
package/src/tabs.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
export interface GETabOption {
|
|
2
|
+
label: string;
|
|
3
|
+
value: string;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface GETabChangeDetail {
|
|
8
|
+
value: string;
|
|
9
|
+
option: GETabOption | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class GETabs extends HTMLElement {
|
|
13
|
+
private readonly _style = document.createElement('style');
|
|
14
|
+
private readonly _tabs = document.createElement('div');
|
|
15
|
+
private readonly _panels = document.createElement('div');
|
|
16
|
+
private _options: GETabOption[] = [];
|
|
17
|
+
|
|
18
|
+
static get observedAttributes(): string[] {
|
|
19
|
+
return ['options', 'value'];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
super();
|
|
24
|
+
const root = this.attachShadow({ mode: 'open' });
|
|
25
|
+
this._style.textContent = `
|
|
26
|
+
:host {
|
|
27
|
+
box-sizing: border-box;
|
|
28
|
+
display: grid;
|
|
29
|
+
grid-template-rows: 32px minmax(0, 1fr);
|
|
30
|
+
width: 100%;
|
|
31
|
+
height: 100%;
|
|
32
|
+
min-width: 0;
|
|
33
|
+
min-height: 0;
|
|
34
|
+
color: var(--ge-text-color, #d8e2f2);
|
|
35
|
+
background: var(--ge-panel-bg-color, #171d28);
|
|
36
|
+
font: 12px system-ui, sans-serif;
|
|
37
|
+
}
|
|
38
|
+
.tabs {
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: end;
|
|
41
|
+
gap: 2px;
|
|
42
|
+
min-width: 0;
|
|
43
|
+
padding: 4px 6px 0;
|
|
44
|
+
border-bottom: 1px solid var(--ge-border-color, #303746);
|
|
45
|
+
background: var(--ge-tabs-bar-bg-color, var(--ge-menu-bg-color, var(--ge-surface-elevated-color, #1b2230)));
|
|
46
|
+
}
|
|
47
|
+
button {
|
|
48
|
+
height: 28px;
|
|
49
|
+
min-width: 0;
|
|
50
|
+
padding: 0 10px;
|
|
51
|
+
border: 1px solid transparent;
|
|
52
|
+
border-bottom: 0;
|
|
53
|
+
border-radius: 4px 4px 0 0;
|
|
54
|
+
color: var(--ge-secondary-text-color, #8fa7c8);
|
|
55
|
+
background: transparent;
|
|
56
|
+
font: inherit;
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
}
|
|
59
|
+
button:hover {
|
|
60
|
+
color: var(--ge-text-color, #d8e2f2);
|
|
61
|
+
background: var(--ge-hover-bg-color, #222b3a);
|
|
62
|
+
}
|
|
63
|
+
button.active {
|
|
64
|
+
color: var(--ge-active-text-color, #eef3ff);
|
|
65
|
+
border-color: var(--ge-border-color, #303746);
|
|
66
|
+
background: var(--ge-panel-bg-color, #171d28);
|
|
67
|
+
}
|
|
68
|
+
button:disabled {
|
|
69
|
+
opacity: 0.45;
|
|
70
|
+
cursor: default;
|
|
71
|
+
}
|
|
72
|
+
.panels {
|
|
73
|
+
min-width: 0;
|
|
74
|
+
min-height: 0;
|
|
75
|
+
overflow: auto;
|
|
76
|
+
}
|
|
77
|
+
::slotted(*) {
|
|
78
|
+
box-sizing: border-box;
|
|
79
|
+
}
|
|
80
|
+
`;
|
|
81
|
+
this._tabs.className = 'tabs';
|
|
82
|
+
this._panels.className = 'panels';
|
|
83
|
+
root.append(this._style, this._tabs, this._panels);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
connectedCallback(): void {
|
|
87
|
+
this._syncFromAttributes();
|
|
88
|
+
this._render();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null): void {
|
|
92
|
+
if (name === 'options') this._readOptionsAttribute();
|
|
93
|
+
if (name === 'value' && newValue === null && this._options[0]) {
|
|
94
|
+
this.setAttribute('value', this._options[0].value);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this._render();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get options(): GETabOption[] {
|
|
101
|
+
return this._options;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
set options(value: GETabOption[]) {
|
|
105
|
+
this._options = Array.isArray(value) ? value : [];
|
|
106
|
+
if (!this._options.some(item => item.value === this.value)) {
|
|
107
|
+
this.value = this._options[0]?.value ?? '';
|
|
108
|
+
}
|
|
109
|
+
this._render();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
get value(): string {
|
|
113
|
+
return this.getAttribute('value') ?? '';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
set value(value: string) {
|
|
117
|
+
this.setAttribute('value', value);
|
|
118
|
+
this._render();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private _syncFromAttributes(): void {
|
|
122
|
+
this._readOptionsAttribute();
|
|
123
|
+
if (!this.value && this._options[0]) this.setAttribute('value', this._options[0].value);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private _readOptionsAttribute(): void {
|
|
127
|
+
const raw = this.getAttribute('options');
|
|
128
|
+
if (!raw) {
|
|
129
|
+
this._options = [];
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const parsed = JSON.parse(raw);
|
|
134
|
+
this._options = Array.isArray(parsed)
|
|
135
|
+
? parsed.map(item => ({
|
|
136
|
+
label: String(item.label ?? item.value ?? ''),
|
|
137
|
+
value: String(item.value ?? item.label ?? ''),
|
|
138
|
+
disabled: Boolean(item.disabled),
|
|
139
|
+
}))
|
|
140
|
+
: [];
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.warn('Invalid ge-tabs options attribute.', error);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private _render(): void {
|
|
147
|
+
this._tabs.replaceChildren();
|
|
148
|
+
this._panels.replaceChildren();
|
|
149
|
+
|
|
150
|
+
const value = this.value || this._options[0]?.value || '';
|
|
151
|
+
for (const option of this._options) {
|
|
152
|
+
const button = document.createElement('button');
|
|
153
|
+
button.type = 'button';
|
|
154
|
+
button.textContent = option.label;
|
|
155
|
+
button.disabled = Boolean(option.disabled);
|
|
156
|
+
button.classList.toggle('active', option.value === value);
|
|
157
|
+
button.addEventListener('click', () => this._select(option.value));
|
|
158
|
+
this._tabs.append(button);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const slot = document.createElement('slot');
|
|
162
|
+
slot.name = value;
|
|
163
|
+
this._panels.append(slot);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private _select(value: string): void {
|
|
167
|
+
if (value === this.value) return;
|
|
168
|
+
this.setAttribute('value', value);
|
|
169
|
+
const option = this._options.find(item => item.value === value) ?? null;
|
|
170
|
+
this.dispatchEvent(new CustomEvent<GETabChangeDetail>('tab-change', {
|
|
171
|
+
detail: { value, option },
|
|
172
|
+
bubbles: true,
|
|
173
|
+
composed: true,
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function defineTabsComponents(): void {
|
|
179
|
+
if (!customElements.get('ge-tabs')) {
|
|
180
|
+
customElements.define('ge-tabs', GETabs);
|
|
181
|
+
}
|
|
182
|
+
}
|