panelset 0.5.0 → 0.5.2
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/dist/index.d.ts +4 -1
- package/dist/panelset.js +25 -12
- package/dist/panelset.js.map +1 -1
- package/package.json +5 -6
- package/src/docs/assets/scripts/copybutton.js +0 -44
- package/src/docs/assets/scripts/example-async.js +0 -161
- package/src/docs/assets/scripts/example-closable.js +0 -27
- package/src/docs/assets/scripts/example-megamenu.js +0 -84
- package/src/docs/assets/scripts/example.js +0 -29
- package/src/docs/assets/scripts/main.js +0 -7
- package/src/docs/assets/styles/_base.scss +0 -13
- package/src/docs/assets/styles/_code.scss +0 -121
- package/src/docs/assets/styles/_demos.scss +0 -180
- package/src/docs/assets/styles/_landingpage.scss +0 -41
- package/src/docs/assets/styles/_layout.scss +0 -80
- package/src/docs/assets/styles/_sidebar.scss +0 -67
- package/src/docs/assets/styles/_typography.scss +0 -116
- package/src/docs/assets/styles/_variables.scss +0 -32
- package/src/docs/assets/styles/docs.scss +0 -64
- package/src/docs/views/api-reference.pug +0 -474
- package/src/docs/views/configuration.pug +0 -173
- package/src/docs/views/events.pug +0 -222
- package/src/docs/views/examples/async.pug +0 -268
- package/src/docs/views/examples/basic.pug +0 -155
- package/src/docs/views/examples/closable.pug +0 -97
- package/src/docs/views/getting-started.pug +0 -99
- package/src/docs/views/index.pug +0 -38
- package/src/docs/views/templates/includes/_head.pug +0 -11
- package/src/docs/views/templates/includes/_mixins.pug +0 -100
- package/src/docs/views/templates/includes/_scripts.pug +0 -14
- package/src/docs/views/templates/includes/_sidebar.pug +0 -18
- package/src/docs/views/templates/layouts/_base.pug +0 -36
- package/src/docs/views/transitions.pug +0 -141
- package/src/lib/index.ts +0 -685
- package/src/lib/styles/_base.scss +0 -99
- package/src/lib/styles/_loading.scss +0 -47
- package/src/lib/styles/_variables.scss +0 -19
- package/src/lib/styles/panelset.scss +0 -3
package/src/lib/index.ts
DELETED
|
@@ -1,685 +0,0 @@
|
|
|
1
|
-
import './styles/panelset.scss';
|
|
2
|
-
|
|
3
|
-
// Configuration types
|
|
4
|
-
export interface PanelSetConfig {
|
|
5
|
-
transitions?: boolean | {
|
|
6
|
-
panels?: boolean;
|
|
7
|
-
height?: boolean;
|
|
8
|
-
};
|
|
9
|
-
closable?: boolean;
|
|
10
|
-
emptyPanelHeight?: number;
|
|
11
|
-
loadingDelay?: number;
|
|
12
|
-
debug?: boolean;
|
|
13
|
-
selector?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// Event detail types
|
|
17
|
-
export interface ReadyEventDetail {
|
|
18
|
-
container: HTMLElement;
|
|
19
|
-
instance: PanelSet;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface BeforeActivateEventDetail {
|
|
23
|
-
panelId: string;
|
|
24
|
-
targetPanel: HTMLElement;
|
|
25
|
-
outgoingPanel: HTMLElement | null;
|
|
26
|
-
signal: AbortSignal;
|
|
27
|
-
promise: Promise<void> | null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface ActivationEventDetail {
|
|
31
|
-
panelId: string;
|
|
32
|
-
trigger: string | null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface ActivationAbortedEventDetail {
|
|
36
|
-
panelId: string;
|
|
37
|
-
trigger: string | null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Handler options
|
|
41
|
-
export interface HandlerOptions {
|
|
42
|
-
once?: boolean;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Show options
|
|
46
|
-
export interface ShowOptions {
|
|
47
|
-
trigger?: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Async content handler type
|
|
51
|
-
export type AsyncContentHandler = (
|
|
52
|
-
targetPanel: HTMLElement,
|
|
53
|
-
signal: AbortSignal
|
|
54
|
-
) => Promise<void> | void;
|
|
55
|
-
|
|
56
|
-
// Extend HTMLElement to include panelSet property
|
|
57
|
-
declare global {
|
|
58
|
-
interface HTMLElement {
|
|
59
|
-
panelSet?: PanelSet;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export class PanelSet {
|
|
64
|
-
// Default configuration
|
|
65
|
-
static defaults: Required<Omit<PanelSetConfig, 'selector'>> = {
|
|
66
|
-
transitions: true,
|
|
67
|
-
closable: false,
|
|
68
|
-
emptyPanelHeight: 200,
|
|
69
|
-
loadingDelay: 300,
|
|
70
|
-
debug: false
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
// Instance properties
|
|
74
|
-
element!: HTMLElement;
|
|
75
|
-
config!: Required<Omit<PanelSetConfig, 'selector'>>;
|
|
76
|
-
panels!: HTMLElement[];
|
|
77
|
-
activePanel!: HTMLElement;
|
|
78
|
-
panelWrapper!: HTMLElement;
|
|
79
|
-
pendingPanel!: HTMLElement;
|
|
80
|
-
|
|
81
|
-
private _openCloseGeneration: number = 0;
|
|
82
|
-
private _isLoadingAsync: boolean = false;
|
|
83
|
-
// private _isTransitioning: boolean = false;
|
|
84
|
-
private _currentAbortController?: AbortController;
|
|
85
|
-
|
|
86
|
-
// Parse data attributes from element
|
|
87
|
-
static _getDataConfig(element: HTMLElement): Partial<PanelSetConfig> {
|
|
88
|
-
const data = element.dataset;
|
|
89
|
-
const config: Partial<PanelSetConfig> = {};
|
|
90
|
-
|
|
91
|
-
// Define attribute types
|
|
92
|
-
const attrs: Record<string, 'json' | 'boolean' | 'number'> = {
|
|
93
|
-
transitions: 'json',
|
|
94
|
-
closable: 'boolean',
|
|
95
|
-
emptyPanelHeight: 'number',
|
|
96
|
-
loadingDelay: 'number',
|
|
97
|
-
debug: 'boolean'
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
// Parse each attribute
|
|
101
|
-
for (const [key, type] of Object.entries(attrs)) {
|
|
102
|
-
if (!(key in data)) continue;
|
|
103
|
-
|
|
104
|
-
const value = data[key];
|
|
105
|
-
if (value === undefined) continue;
|
|
106
|
-
|
|
107
|
-
switch (type) {
|
|
108
|
-
case 'boolean':
|
|
109
|
-
(config as any)[key] = value !== 'false';
|
|
110
|
-
break;
|
|
111
|
-
case 'number':
|
|
112
|
-
(config as any)[key] = parseInt(value, 10);
|
|
113
|
-
break;
|
|
114
|
-
case 'json':
|
|
115
|
-
try {
|
|
116
|
-
(config as any)[key] = JSON.parse(value);
|
|
117
|
-
} catch {
|
|
118
|
-
// Fall back to boolean parsing
|
|
119
|
-
(config as any)[key] = value !== 'false';
|
|
120
|
-
}
|
|
121
|
-
break;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return config;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Merge configurations
|
|
129
|
-
static _mergeConfig(
|
|
130
|
-
defaults: Required<Omit<PanelSetConfig, 'selector'>>,
|
|
131
|
-
dataConfig: Partial<PanelSetConfig>,
|
|
132
|
-
options: Partial<PanelSetConfig>
|
|
133
|
-
): Required<Omit<PanelSetConfig, 'selector'>> {
|
|
134
|
-
return {
|
|
135
|
-
...defaults,
|
|
136
|
-
...dataConfig,
|
|
137
|
-
...options
|
|
138
|
-
} as Required<Omit<PanelSetConfig, 'selector'>>;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Initialize PanelSet instances
|
|
143
|
-
* @param selectorOrOptions - CSS selector string or config object
|
|
144
|
-
* @param options - Additional config options (when first param is selector)
|
|
145
|
-
* @returns Array of PanelSet instances
|
|
146
|
-
*/
|
|
147
|
-
static init(selectorOrOptions: string | PanelSetConfig = {}, options: PanelSetConfig = {}): PanelSet[] {
|
|
148
|
-
// Handle different call signatures
|
|
149
|
-
let selector: string;
|
|
150
|
-
let config: PanelSetConfig;
|
|
151
|
-
|
|
152
|
-
if (typeof selectorOrOptions === 'string') {
|
|
153
|
-
// init('#demo') or init('#demo', {debug: true})
|
|
154
|
-
selector = selectorOrOptions;
|
|
155
|
-
config = options;
|
|
156
|
-
} else {
|
|
157
|
-
// init() or init({selector: '#demo', debug: true})
|
|
158
|
-
config = selectorOrOptions;
|
|
159
|
-
selector = config.selector || '[data-panelset]';
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const elements = document.querySelectorAll<HTMLElement>(selector);
|
|
163
|
-
const instances: PanelSet[] = [];
|
|
164
|
-
|
|
165
|
-
elements.forEach(el => {
|
|
166
|
-
// Skip if already initialized
|
|
167
|
-
if (el.panelSet || el.dataset.panelset === 'true') {
|
|
168
|
-
if (el.panelSet) instances.push(el.panelSet);
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const instance = new PanelSet(el, config);
|
|
173
|
-
instances.push(instance);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
return instances;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
constructor(elementOrSelector: HTMLElement | string, options: PanelSetConfig = {}) {
|
|
180
|
-
// Handle both element and selector
|
|
181
|
-
let element: HTMLElement | null;
|
|
182
|
-
if (typeof elementOrSelector === 'string') {
|
|
183
|
-
element = document.querySelector<HTMLElement>(elementOrSelector);
|
|
184
|
-
if (!element) {
|
|
185
|
-
throw new Error(`PanelSet: No element found for selector "${elementOrSelector}"`);
|
|
186
|
-
}
|
|
187
|
-
} else {
|
|
188
|
-
element = elementOrSelector;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
this.element = element;
|
|
192
|
-
|
|
193
|
-
// Check if already initialized
|
|
194
|
-
if (element.panelSet || element.dataset.panelset === 'true') {
|
|
195
|
-
console.warn('PanelSet: Element already initialized, returning existing instance');
|
|
196
|
-
return element.panelSet!;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Store instance on element
|
|
200
|
-
element.panelSet = this;
|
|
201
|
-
|
|
202
|
-
const dataConfig = PanelSet._getDataConfig(element);
|
|
203
|
-
this.config = PanelSet._mergeConfig(
|
|
204
|
-
PanelSet.defaults,
|
|
205
|
-
dataConfig,
|
|
206
|
-
options
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
this.panels = Array.from(element.querySelectorAll<HTMLElement>('[role="tabpanel"]'));
|
|
210
|
-
this.activePanel =
|
|
211
|
-
this.panels.find(p => p.classList.contains('active')) || this.panels[0];
|
|
212
|
-
this.panelWrapper =
|
|
213
|
-
this.element.querySelector<HTMLElement>('.panel-wrapper') || this._autoWrapPanels();
|
|
214
|
-
|
|
215
|
-
this.pendingPanel = this.activePanel;
|
|
216
|
-
|
|
217
|
-
this._internalInit();
|
|
218
|
-
|
|
219
|
-
// Mark as initialized, can be used in CSS selectors for visual confirmation
|
|
220
|
-
this.element.dataset.panelset = 'true';
|
|
221
|
-
|
|
222
|
-
this._log(`Initialized (${this.panels.length} panels)`);
|
|
223
|
-
this._dispatch<ReadyEventDetail>('ps:ready', { container: this.element, instance: this });
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Debug logging helper
|
|
227
|
-
private _log(message: string): void {
|
|
228
|
-
if (!this.config.debug) return;
|
|
229
|
-
const id = this.element.id || 'no id';
|
|
230
|
-
console.log(`[PanelSet] - "${id}" -`, message);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
private _autoWrapPanels(): HTMLElement {
|
|
234
|
-
const wrapper = document.createElement('div');
|
|
235
|
-
wrapper.className = 'panel-wrapper';
|
|
236
|
-
this.panels.forEach(panel => wrapper.appendChild(panel));
|
|
237
|
-
this.element.appendChild(wrapper);
|
|
238
|
-
return wrapper;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
private _internalInit(): void {
|
|
242
|
-
this.panels.forEach(panel => {
|
|
243
|
-
panel.classList.remove('fade', 'incoming');
|
|
244
|
-
if (panel !== this.activePanel) {
|
|
245
|
-
panel.hidden = true;
|
|
246
|
-
panel.classList.remove('active');
|
|
247
|
-
} else {
|
|
248
|
-
panel.hidden = false;
|
|
249
|
-
panel.classList.add('active');
|
|
250
|
-
}
|
|
251
|
-
});
|
|
252
|
-
this.element.style.height = '';
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Dispatch custom event helper
|
|
256
|
-
private _dispatch<T = unknown>(eventName: string, detail: T): void {
|
|
257
|
-
this.element.dispatchEvent(
|
|
258
|
-
new CustomEvent(eventName, {
|
|
259
|
-
detail,
|
|
260
|
-
bubbles: true,
|
|
261
|
-
cancelable: false
|
|
262
|
-
})
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/* --- Modular helpers --- */
|
|
267
|
-
|
|
268
|
-
private _getVerticalMetrics(el: HTMLElement | null): number {
|
|
269
|
-
if (!el) return 0;
|
|
270
|
-
const s = getComputedStyle(el);
|
|
271
|
-
return ['paddingTop', 'paddingBottom', 'borderTopWidth', 'borderBottomWidth']
|
|
272
|
-
.reduce((sum, prop) => sum + (parseFloat(s[prop as keyof CSSStyleDeclaration] as string) || 0), 0);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
private _measureHeight(panel: HTMLElement): number {
|
|
276
|
-
let total = panel.offsetHeight;
|
|
277
|
-
total += this._getVerticalMetrics(this.panelWrapper);
|
|
278
|
-
total += this._getVerticalMetrics(this.element);
|
|
279
|
-
return total;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
private _cleanupPanels(newPanel: HTMLElement): void {
|
|
283
|
-
this.panels.forEach(panel => {
|
|
284
|
-
panel.classList.remove('fade', 'incoming');
|
|
285
|
-
if (panel !== newPanel) {
|
|
286
|
-
panel.classList.remove('active');
|
|
287
|
-
panel.hidden = true;
|
|
288
|
-
} else {
|
|
289
|
-
panel.classList.add('active');
|
|
290
|
-
panel.hidden = false;
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
this.element.style.height = '';
|
|
294
|
-
this.element.classList.remove('is-transitioning');
|
|
295
|
-
this.activePanel = newPanel;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
private _waitForTransition(element: HTMLElement): Promise<void> {
|
|
299
|
-
return new Promise(resolve => {
|
|
300
|
-
const styles = getComputedStyle(element);
|
|
301
|
-
const duration = parseFloat(styles.transitionDuration) || 0;
|
|
302
|
-
const delay = parseFloat(styles.transitionDelay) || 0;
|
|
303
|
-
if (duration + delay === 0) {
|
|
304
|
-
resolve();
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
const handler = (e: TransitionEvent) => {
|
|
308
|
-
if (e.target !== element) return;
|
|
309
|
-
element.removeEventListener('transitionend', handler);
|
|
310
|
-
resolve();
|
|
311
|
-
};
|
|
312
|
-
element.addEventListener('transitionend', handler);
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// Shared helper for open/close
|
|
317
|
-
private _animateOpenClose(isOpening: boolean, withTransition: boolean): void {
|
|
318
|
-
const action = isOpening ? 'opening' : 'closing';
|
|
319
|
-
const oppositeAction = isOpening ? 'closing' : 'opening';
|
|
320
|
-
const oppositeClass = `is-${oppositeAction}`;
|
|
321
|
-
const actionClass = `is-${action}`;
|
|
322
|
-
|
|
323
|
-
this._log(isOpening ? 'Opening' : 'Closing');
|
|
324
|
-
|
|
325
|
-
this._openCloseGeneration++;
|
|
326
|
-
const myGeneration = this._openCloseGeneration;
|
|
327
|
-
|
|
328
|
-
// Remove opposite state if interrupting
|
|
329
|
-
if (this.element.classList.contains(oppositeClass)) {
|
|
330
|
-
this.element.classList.remove(oppositeClass);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const targetHeight = isOpening ? this._measureHeight(this.pendingPanel) : 0;
|
|
334
|
-
|
|
335
|
-
if (withTransition && this.config.transitions) {
|
|
336
|
-
this.element.classList.add(actionClass);
|
|
337
|
-
|
|
338
|
-
const currentHeight = this.element.offsetHeight;
|
|
339
|
-
this.element.style.height = `${currentHeight}px`;
|
|
340
|
-
|
|
341
|
-
if (isOpening) this.element.classList.remove('is-closed');
|
|
342
|
-
|
|
343
|
-
requestAnimationFrame(() => {
|
|
344
|
-
this.element.style.height = `${targetHeight}px`;
|
|
345
|
-
|
|
346
|
-
this._waitForTransition(this.element).then(() => {
|
|
347
|
-
if (this._openCloseGeneration === myGeneration) {
|
|
348
|
-
this.element.style.height = '';
|
|
349
|
-
this.element.classList.remove(actionClass);
|
|
350
|
-
if (!isOpening) this.element.classList.add('is-closed');
|
|
351
|
-
}
|
|
352
|
-
});
|
|
353
|
-
});
|
|
354
|
-
} else {
|
|
355
|
-
if (isOpening) {
|
|
356
|
-
this.element.classList.remove('is-closed');
|
|
357
|
-
} else {
|
|
358
|
-
this.element.classList.add('is-closed');
|
|
359
|
-
}
|
|
360
|
-
this.element.style.height = '';
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Get the ID of the currently active panel
|
|
366
|
-
* @returns Panel ID or null if no panel is active
|
|
367
|
-
*/
|
|
368
|
-
getActive(): string | null {
|
|
369
|
-
return this.pendingPanel?.id || null;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Open a closable panelset
|
|
374
|
-
* @param withTransition - Whether to animate
|
|
375
|
-
*/
|
|
376
|
-
open(withTransition: boolean = true): void {
|
|
377
|
-
if (!this.config.closable) {
|
|
378
|
-
this._log('Cannot open: closable is false');
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
const isClosed = this.element.classList.contains('is-closed');
|
|
383
|
-
const isClosing = this.element.classList.contains('is-closing');
|
|
384
|
-
|
|
385
|
-
if (!isClosed && !isClosing) return;
|
|
386
|
-
|
|
387
|
-
this._animateOpenClose(true, withTransition);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Close a closable panelset
|
|
392
|
-
* @param withTransition - Whether to animate
|
|
393
|
-
*/
|
|
394
|
-
close(withTransition: boolean = true): void {
|
|
395
|
-
if (!this.config.closable) {
|
|
396
|
-
this._log('Cannot close: closable is false');
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const isClosed = this.element.classList.contains('is-closed');
|
|
401
|
-
const isOpening = this.element.classList.contains('is-opening');
|
|
402
|
-
|
|
403
|
-
if (isClosed && !isOpening) return;
|
|
404
|
-
|
|
405
|
-
this._animateOpenClose(false, withTransition);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Toggle a closable panelset between open and closed
|
|
410
|
-
* @param withTransition - Whether to animate
|
|
411
|
-
*/
|
|
412
|
-
toggle(withTransition: boolean = true): void {
|
|
413
|
-
const isClosed = this.element.classList.contains('is-closed');
|
|
414
|
-
const isClosing = this.element.classList.contains('is-closing');
|
|
415
|
-
|
|
416
|
-
// If closed or closing, open it
|
|
417
|
-
if (isClosed || isClosing) {
|
|
418
|
-
this.open(withTransition);
|
|
419
|
-
} else {
|
|
420
|
-
this.close(withTransition);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Register a handler for async content loading
|
|
426
|
-
* @param handler - Async content handler function
|
|
427
|
-
* @param options - Handler options (once: whether to load only once)
|
|
428
|
-
*/
|
|
429
|
-
onBeforeActivate(handler: AsyncContentHandler, options: HandlerOptions = {}): void {
|
|
430
|
-
const once = options.once === true; // Default: false (always reload)
|
|
431
|
-
|
|
432
|
-
this.element.addEventListener('ps:beforeactivate', (e) => {
|
|
433
|
-
const event = e as CustomEvent<BeforeActivateEventDetail>;
|
|
434
|
-
const { targetPanel, signal } = event.detail;
|
|
435
|
-
|
|
436
|
-
// Skip if already loaded and once=true
|
|
437
|
-
if (once && targetPanel.dataset.loaded === 'true') {
|
|
438
|
-
if (this.config.debug) {
|
|
439
|
-
this._log(`Skipping ${targetPanel.id} (already loaded)`);
|
|
440
|
-
}
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// Call user handler
|
|
445
|
-
const result = handler(targetPanel, signal);
|
|
446
|
-
|
|
447
|
-
// If handler returns a promise, attach it to the event
|
|
448
|
-
if (result && typeof result.then === 'function') {
|
|
449
|
-
event.detail.promise = result
|
|
450
|
-
.then(() => {
|
|
451
|
-
// Auto-mark as loaded on success if once=true
|
|
452
|
-
if (once) {
|
|
453
|
-
targetPanel.dataset.loaded = 'true';
|
|
454
|
-
}
|
|
455
|
-
})
|
|
456
|
-
.catch(error => {
|
|
457
|
-
if (error.name === 'AbortError') {
|
|
458
|
-
if (this.config.debug) {
|
|
459
|
-
this._log(`Load aborted: ${targetPanel.id}`);
|
|
460
|
-
}
|
|
461
|
-
throw error;
|
|
462
|
-
} else {
|
|
463
|
-
this._log(`Load failed: ${error.message}`);
|
|
464
|
-
throw error;
|
|
465
|
-
}
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
/* --- Main logic --- */
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* Show a panel by ID
|
|
475
|
-
* @param panelId - ID of the panel to show
|
|
476
|
-
* @param withTransition - Whether to animate the transition
|
|
477
|
-
* @param options - Additional options (trigger name)
|
|
478
|
-
*/
|
|
479
|
-
async show(panelId: string, withTransition: boolean = true, options: ShowOptions = {}): Promise<void> {
|
|
480
|
-
const newPanel = this.panels.find(p => p.id === panelId);
|
|
481
|
-
|
|
482
|
-
if (!newPanel) {
|
|
483
|
-
this._log(`Panel not found: ${panelId}`);
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
if (newPanel === this.pendingPanel) return;
|
|
488
|
-
|
|
489
|
-
const prevPanel = this.pendingPanel;
|
|
490
|
-
const prevPanelId = prevPanel?.id;
|
|
491
|
-
this.pendingPanel = newPanel;
|
|
492
|
-
|
|
493
|
-
this.element.classList.remove('is-loading');
|
|
494
|
-
|
|
495
|
-
if (prevPanel && prevPanel !== this.activePanel && prevPanel !== newPanel) {
|
|
496
|
-
prevPanel.classList.remove('incoming');
|
|
497
|
-
if (prevPanel.hidden) {
|
|
498
|
-
// Was never visible, keep hidden
|
|
499
|
-
} else {
|
|
500
|
-
prevPanel.classList.remove('active');
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
const wasLoadingAsync = this._isLoadingAsync;
|
|
505
|
-
|
|
506
|
-
if (this._currentAbortController) {
|
|
507
|
-
this._currentAbortController.abort();
|
|
508
|
-
|
|
509
|
-
// Only fire abort if we're cancelling an async operation
|
|
510
|
-
if (wasLoadingAsync && prevPanelId && prevPanelId !== panelId) {
|
|
511
|
-
this._dispatch<ActivationAbortedEventDetail>('ps:activationaborted', {
|
|
512
|
-
panelId: prevPanelId,
|
|
513
|
-
trigger: null
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
const abortController = new AbortController();
|
|
519
|
-
this._currentAbortController = abortController;
|
|
520
|
-
this._isLoadingAsync = false;
|
|
521
|
-
|
|
522
|
-
this._log(`${prevPanel?.id || 'none'} → ${panelId}`);
|
|
523
|
-
|
|
524
|
-
const beforeActivateDetail: BeforeActivateEventDetail = {
|
|
525
|
-
panelId,
|
|
526
|
-
targetPanel: newPanel,
|
|
527
|
-
outgoingPanel: prevPanel,
|
|
528
|
-
signal: abortController.signal,
|
|
529
|
-
promise: null
|
|
530
|
-
};
|
|
531
|
-
|
|
532
|
-
const beforeActivateEvent = new CustomEvent('ps:beforeactivate', {
|
|
533
|
-
detail: beforeActivateDetail,
|
|
534
|
-
bubbles: true,
|
|
535
|
-
cancelable: false
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
this.element.dispatchEvent(beforeActivateEvent);
|
|
539
|
-
|
|
540
|
-
const userPromise = beforeActivateDetail.promise;
|
|
541
|
-
|
|
542
|
-
if (userPromise) {
|
|
543
|
-
this._isLoadingAsync = true;
|
|
544
|
-
this._log('Waiting for content...');
|
|
545
|
-
|
|
546
|
-
let spinnerTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
547
|
-
let loadingShown = false;
|
|
548
|
-
|
|
549
|
-
if (this.config.loadingDelay > 0) {
|
|
550
|
-
spinnerTimeout = setTimeout(() => {
|
|
551
|
-
this.element.classList.add('is-loading');
|
|
552
|
-
loadingShown = true;
|
|
553
|
-
}, this.config.loadingDelay);
|
|
554
|
-
} else {
|
|
555
|
-
this.element.classList.add('is-loading');
|
|
556
|
-
loadingShown = true;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
const hasPreviousPanel = this.activePanel && this.activePanel !== newPanel;
|
|
560
|
-
|
|
561
|
-
if (!hasPreviousPanel) {
|
|
562
|
-
const shouldTransition = withTransition !== false && this.config.transitions !== false;
|
|
563
|
-
let heightTransition = shouldTransition;
|
|
564
|
-
if (typeof this.config.transitions === 'object') {
|
|
565
|
-
heightTransition = shouldTransition && this.config.transitions.height !== false;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
if (heightTransition) {
|
|
569
|
-
const currentHeight = this.element.offsetHeight;
|
|
570
|
-
this.element.style.height = `${currentHeight}px`;
|
|
571
|
-
|
|
572
|
-
requestAnimationFrame(() => {
|
|
573
|
-
this.element.style.height = `${this.config.emptyPanelHeight}px`;
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
try {
|
|
579
|
-
await userPromise;
|
|
580
|
-
|
|
581
|
-
if (spinnerTimeout) clearTimeout(spinnerTimeout);
|
|
582
|
-
|
|
583
|
-
if (abortController.signal.aborted) {
|
|
584
|
-
this._log(`Aborted during load: ${panelId}`);
|
|
585
|
-
if (loadingShown) this.element.classList.remove('is-loading');
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
this._log('Content loaded');
|
|
590
|
-
} catch (error) {
|
|
591
|
-
if (spinnerTimeout) clearTimeout(spinnerTimeout);
|
|
592
|
-
|
|
593
|
-
const err = error as Error;
|
|
594
|
-
this._log(`Load failed: ${err.message}`);
|
|
595
|
-
if (loadingShown) this.element.classList.remove('is-loading');
|
|
596
|
-
|
|
597
|
-
if (err.name !== 'AbortError') {
|
|
598
|
-
console.error('Panel load error:', error);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
if (loadingShown) this.element.classList.remove('is-loading');
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
if (abortController.signal.aborted) {
|
|
608
|
-
this._log(`Aborted: ${panelId}`);
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
this._dispatch<ActivationEventDetail>('ps:activationstart', {
|
|
613
|
-
panelId,
|
|
614
|
-
trigger: options.trigger || null
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
const shouldTransition = withTransition !== false && this.config.transitions !== false;
|
|
618
|
-
|
|
619
|
-
let panelTransition = shouldTransition;
|
|
620
|
-
let heightTransition = shouldTransition;
|
|
621
|
-
|
|
622
|
-
if (typeof this.config.transitions === 'object') {
|
|
623
|
-
panelTransition = shouldTransition && this.config.transitions.panels !== false;
|
|
624
|
-
heightTransition = shouldTransition && this.config.transitions.height !== false;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
this.panels.forEach(panel => panel.classList.toggle('fade', panelTransition));
|
|
628
|
-
|
|
629
|
-
const startHeight = this.element.offsetHeight;
|
|
630
|
-
if (heightTransition) {
|
|
631
|
-
this.element.style.height = `${startHeight}px`;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
const outgoingPanel = this.activePanel;
|
|
635
|
-
|
|
636
|
-
newPanel.hidden = false;
|
|
637
|
-
newPanel.classList.add('incoming');
|
|
638
|
-
if (panelTransition) {
|
|
639
|
-
// Apply general transitioning class for overflow: hidden
|
|
640
|
-
this.element.classList.add('is-transitioning');
|
|
641
|
-
}
|
|
642
|
-
if (outgoingPanel && outgoingPanel !== newPanel) {
|
|
643
|
-
outgoingPanel.classList.remove('active', 'incoming');
|
|
644
|
-
outgoingPanel.hidden = false;
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
requestAnimationFrame(() => {
|
|
648
|
-
newPanel.classList.add('active');
|
|
649
|
-
if (outgoingPanel && outgoingPanel !== newPanel) {
|
|
650
|
-
outgoingPanel.classList.remove('incoming');
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
const targetHeight = this._measureHeight(newPanel);
|
|
654
|
-
const heightChanged = startHeight !== targetHeight;
|
|
655
|
-
|
|
656
|
-
if (heightTransition) {
|
|
657
|
-
this.element.style.height = `${targetHeight}px`;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
const promises: Promise<void>[] = [];
|
|
661
|
-
if (panelTransition) {
|
|
662
|
-
promises.push(this._waitForTransition(newPanel));
|
|
663
|
-
}
|
|
664
|
-
if (heightTransition && heightChanged) {
|
|
665
|
-
promises.push(this._waitForTransition(this.element));
|
|
666
|
-
}
|
|
667
|
-
if (!promises.length) promises.push(Promise.resolve());
|
|
668
|
-
|
|
669
|
-
Promise.all(promises).then(() => {
|
|
670
|
-
// Check if interrupted by another activation
|
|
671
|
-
if (this.pendingPanel !== newPanel) {
|
|
672
|
-
this._log(`Interrupted: ${panelId}`);
|
|
673
|
-
return;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
this._cleanupPanels(newPanel);
|
|
677
|
-
this._log(`✓ ${panelId}`);
|
|
678
|
-
this._dispatch<ActivationEventDetail>('ps:activationcomplete', {
|
|
679
|
-
panelId,
|
|
680
|
-
trigger: options.trigger || null
|
|
681
|
-
});
|
|
682
|
-
});
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
}
|