@customviews-js/customviews 1.0.2 → 1.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/README.md +164 -30
- package/dist/{custom-views.cjs.js → custom-views.core.cjs.js} +814 -99
- package/dist/custom-views.core.cjs.js.map +1 -0
- package/dist/custom-views.core.esm.js +2296 -0
- package/dist/custom-views.core.esm.js.map +1 -0
- package/dist/custom-views.esm.js +814 -98
- package/dist/custom-views.esm.js.map +1 -1
- package/dist/{custom-views.umd.js → custom-views.js} +814 -99
- package/dist/custom-views.js.map +1 -0
- package/dist/custom-views.min.js +7 -0
- package/dist/custom-views.min.js.map +1 -0
- package/dist/types/{models/AssetsManager.d.ts → core/assets-manager.d.ts} +1 -1
- package/dist/types/{models/AssetsManager.d.ts.map → core/assets-manager.d.ts.map} +1 -1
- package/dist/types/core/core.d.ts +25 -9
- package/dist/types/core/core.d.ts.map +1 -1
- package/dist/types/core/custom-elements.d.ts +8 -0
- package/dist/types/core/custom-elements.d.ts.map +1 -0
- package/dist/types/core/render.d.ts +1 -1
- package/dist/types/core/render.d.ts.map +1 -1
- package/dist/types/core/tab-manager.d.ts +35 -0
- package/dist/types/core/tab-manager.d.ts.map +1 -0
- package/dist/types/core/url-state-manager.d.ts.map +1 -1
- package/dist/types/core/visibility-manager.d.ts +1 -1
- package/dist/types/core/widget.d.ts +2 -0
- package/dist/types/core/widget.d.ts.map +1 -1
- package/dist/types/entry/browser-entry.d.ts +13 -0
- package/dist/types/entry/browser-entry.d.ts.map +1 -0
- package/dist/types/index.d.ts +11 -20
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/lib/custom-views.d.ts +29 -0
- package/dist/types/lib/custom-views.d.ts.map +1 -0
- package/dist/types/styles/styles.d.ts +2 -0
- package/dist/types/styles/styles.d.ts.map +1 -1
- package/dist/types/styles/tab-styles.d.ts +5 -0
- package/dist/types/styles/tab-styles.d.ts.map +1 -0
- package/dist/types/styles/toggle-styles.d.ts +5 -0
- package/dist/types/styles/toggle-styles.d.ts.map +1 -0
- package/dist/types/styles/widget-styles.d.ts +1 -1
- package/dist/types/styles/widget-styles.d.ts.map +1 -1
- package/dist/types/types/types.d.ts +85 -0
- package/dist/types/types/types.d.ts.map +1 -1
- package/dist/types/utils/url-utils.d.ts +8 -0
- package/dist/types/utils/url-utils.d.ts.map +1 -0
- package/package.json +13 -9
- package/dist/custom-views.cjs.js.map +0 -1
- package/dist/custom-views.umd.js.map +0 -1
- package/dist/custom-views.umd.min.js +0 -7
- package/dist/custom-views.umd.min.js.map +0 -1
- package/dist/types/models/Config.d.ts +0 -10
- package/dist/types/models/Config.d.ts.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* @customviews-js/customviews v1.0
|
|
2
|
+
* @customviews-js/customviews v1.1.0
|
|
3
3
|
* (c) 2025 Chan Ger Teck
|
|
4
4
|
* Released under the MIT License.
|
|
5
5
|
*/
|
|
@@ -86,18 +86,6 @@
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
/**
|
|
90
|
-
* Configuration for the site, has default state and list of toggles
|
|
91
|
-
*/
|
|
92
|
-
class Config {
|
|
93
|
-
defaultState;
|
|
94
|
-
allToggles;
|
|
95
|
-
constructor(defaultState, allToggles) {
|
|
96
|
-
this.defaultState = defaultState;
|
|
97
|
-
this.allToggles = allToggles;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
89
|
/**
|
|
102
90
|
* Manages persistence of custom views state using browser localStorage
|
|
103
91
|
*/
|
|
@@ -227,6 +215,10 @@
|
|
|
227
215
|
const compact = {
|
|
228
216
|
t: state.toggles
|
|
229
217
|
};
|
|
218
|
+
// Add tab groups if present
|
|
219
|
+
if (state.tabs && Object.keys(state.tabs).length > 0) {
|
|
220
|
+
compact.g = Object.entries(state.tabs);
|
|
221
|
+
}
|
|
230
222
|
// Convert to JSON and encode
|
|
231
223
|
const json = JSON.stringify(compact);
|
|
232
224
|
let encoded;
|
|
@@ -273,9 +265,19 @@
|
|
|
273
265
|
if (!compact || typeof compact !== 'object') {
|
|
274
266
|
throw new Error('Invalid compact state structure');
|
|
275
267
|
}
|
|
276
|
-
|
|
268
|
+
const state = {
|
|
277
269
|
toggles: Array.isArray(compact.t) ? compact.t : []
|
|
278
270
|
};
|
|
271
|
+
// Reconstruct tabs from compact format
|
|
272
|
+
if (Array.isArray(compact.g)) {
|
|
273
|
+
state.tabs = {};
|
|
274
|
+
for (const [groupId, tabId] of compact.g) {
|
|
275
|
+
if (typeof groupId === 'string' && typeof tabId === 'string') {
|
|
276
|
+
state.tabs[groupId] = tabId;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return state;
|
|
279
281
|
}
|
|
280
282
|
catch (error) {
|
|
281
283
|
console.warn('Failed to decode view state:', error);
|
|
@@ -330,7 +332,7 @@
|
|
|
330
332
|
}
|
|
331
333
|
/**
|
|
332
334
|
* Apply simple class-based visibility to a toggle element.
|
|
333
|
-
* The element is assumed to have data-customviews-toggle.
|
|
335
|
+
* The element is assumed to have data-cv-toggle or data-customviews-toggle.
|
|
334
336
|
*/
|
|
335
337
|
applyElementVisibility(el, visible) {
|
|
336
338
|
if (visible) {
|
|
@@ -344,8 +346,211 @@
|
|
|
344
346
|
}
|
|
345
347
|
}
|
|
346
348
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
+
// Constants for selectors
|
|
350
|
+
const TABGROUP_SELECTOR = 'cv-tabgroup';
|
|
351
|
+
const TAB_SELECTOR = 'cv-tab';
|
|
352
|
+
const NAV_AUTO_SELECTOR = 'cv-tabgroup[nav="auto"], cv-tabgroup:not([nav])';
|
|
353
|
+
const NAV_CONTAINER_CLASS = 'cv-tabs-nav';
|
|
354
|
+
/**
|
|
355
|
+
* TabManager handles discovery, visibility, and navigation for tab groups and tabs
|
|
356
|
+
*/
|
|
357
|
+
class TabManager {
|
|
358
|
+
/**
|
|
359
|
+
* Apply tab selections to all tab groups in the DOM
|
|
360
|
+
*/
|
|
361
|
+
static applySelections(rootEl, tabs, cfgGroups) {
|
|
362
|
+
// Find all cv-tabgroup elements
|
|
363
|
+
const tabGroups = rootEl.querySelectorAll(TABGROUP_SELECTOR);
|
|
364
|
+
tabGroups.forEach((groupEl) => {
|
|
365
|
+
const groupId = groupEl.getAttribute('id');
|
|
366
|
+
if (!groupId)
|
|
367
|
+
return;
|
|
368
|
+
// Determine the active tab for this group
|
|
369
|
+
const activeTabId = this.resolveActiveTab(groupId, tabs, cfgGroups, groupEl);
|
|
370
|
+
// Apply visibility to child cv-tab elements
|
|
371
|
+
const tabElements = groupEl.querySelectorAll(TAB_SELECTOR);
|
|
372
|
+
tabElements.forEach((tabEl) => {
|
|
373
|
+
const tabId = tabEl.getAttribute('id');
|
|
374
|
+
if (!tabId)
|
|
375
|
+
return;
|
|
376
|
+
const isActive = tabId === activeTabId;
|
|
377
|
+
this.applyTabVisibility(tabEl, isActive);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Resolve the active tab for a group based on state, config, and DOM
|
|
383
|
+
*/
|
|
384
|
+
static resolveActiveTab(groupId, tabs, cfgGroups, groupEl) {
|
|
385
|
+
// 1. Check state
|
|
386
|
+
if (tabs[groupId]) {
|
|
387
|
+
return tabs[groupId];
|
|
388
|
+
}
|
|
389
|
+
// 2. Check config default
|
|
390
|
+
if (cfgGroups) {
|
|
391
|
+
const groupCfg = cfgGroups.find(g => g.id === groupId);
|
|
392
|
+
if (groupCfg) {
|
|
393
|
+
if (groupCfg.default) {
|
|
394
|
+
return groupCfg.default;
|
|
395
|
+
}
|
|
396
|
+
// Fallback to first tab in config
|
|
397
|
+
const firstConfigTab = groupCfg.tabs[0];
|
|
398
|
+
if (firstConfigTab) {
|
|
399
|
+
return firstConfigTab.id;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// 3. Fallback to first cv-tab child in DOM
|
|
404
|
+
const firstTab = groupEl.querySelector(TAB_SELECTOR);
|
|
405
|
+
if (firstTab) {
|
|
406
|
+
return firstTab.getAttribute('id');
|
|
407
|
+
}
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Apply visibility classes to a tab element
|
|
412
|
+
*/
|
|
413
|
+
static applyTabVisibility(tabEl, isActive) {
|
|
414
|
+
if (isActive) {
|
|
415
|
+
tabEl.classList.remove('cv-hidden');
|
|
416
|
+
tabEl.classList.add('cv-visible');
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
tabEl.classList.add('cv-hidden');
|
|
420
|
+
tabEl.classList.remove('cv-visible');
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Build navigation for tab groups with nav="auto" (one-time setup)
|
|
425
|
+
*/
|
|
426
|
+
static buildNavs(rootEl, cfgGroups, onTabClick) {
|
|
427
|
+
// Find all cv-tabgroup elements with nav="auto" or no nav attribute
|
|
428
|
+
const tabGroups = rootEl.querySelectorAll(NAV_AUTO_SELECTOR);
|
|
429
|
+
tabGroups.forEach((groupEl) => {
|
|
430
|
+
const groupId = groupEl.getAttribute('id');
|
|
431
|
+
if (!groupId)
|
|
432
|
+
return;
|
|
433
|
+
// Check if nav already exists - if so, skip building
|
|
434
|
+
let navContainer = groupEl.querySelector(`.${NAV_CONTAINER_CLASS}`);
|
|
435
|
+
if (navContainer)
|
|
436
|
+
return; // Already built
|
|
437
|
+
// Get all child tabs
|
|
438
|
+
const tabElements = Array.from(groupEl.querySelectorAll(TAB_SELECTOR));
|
|
439
|
+
if (tabElements.length === 0)
|
|
440
|
+
return;
|
|
441
|
+
// Create nav container
|
|
442
|
+
navContainer = document.createElement('ul');
|
|
443
|
+
navContainer.className = `${NAV_CONTAINER_CLASS} nav-tabs`;
|
|
444
|
+
navContainer.setAttribute('role', 'tablist');
|
|
445
|
+
groupEl.insertBefore(navContainer, groupEl.firstChild);
|
|
446
|
+
// Build nav items
|
|
447
|
+
tabElements.forEach((tabEl) => {
|
|
448
|
+
const tabId = tabEl.getAttribute('id');
|
|
449
|
+
if (!tabId)
|
|
450
|
+
return;
|
|
451
|
+
const header = tabEl.getAttribute('header') || this.getTabLabel(tabId, groupId, cfgGroups) || tabId;
|
|
452
|
+
const listItem = document.createElement('li');
|
|
453
|
+
listItem.className = 'nav-item';
|
|
454
|
+
const navLink = document.createElement('a');
|
|
455
|
+
navLink.className = 'nav-link';
|
|
456
|
+
navLink.textContent = header;
|
|
457
|
+
navLink.href = '#';
|
|
458
|
+
navLink.setAttribute('data-tab-id', tabId);
|
|
459
|
+
navLink.setAttribute('data-group-id', groupId);
|
|
460
|
+
navLink.setAttribute('role', 'tab');
|
|
461
|
+
// Check if this tab is currently active
|
|
462
|
+
const isActive = tabEl.classList.contains('cv-visible');
|
|
463
|
+
if (isActive) {
|
|
464
|
+
navLink.classList.add('active');
|
|
465
|
+
navLink.setAttribute('aria-selected', 'true');
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
navLink.setAttribute('aria-selected', 'false');
|
|
469
|
+
}
|
|
470
|
+
// Add click handler
|
|
471
|
+
if (onTabClick) {
|
|
472
|
+
navLink.addEventListener('click', (e) => {
|
|
473
|
+
e.preventDefault();
|
|
474
|
+
onTabClick(groupId, tabId);
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
listItem.appendChild(navLink);
|
|
478
|
+
navContainer.appendChild(listItem);
|
|
479
|
+
});
|
|
480
|
+
// Add bottom border line at the end of the tab group
|
|
481
|
+
const bottomBorder = document.createElement('div');
|
|
482
|
+
bottomBorder.className = 'cv-tabgroup-bottom-border';
|
|
483
|
+
groupEl.appendChild(bottomBorder);
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Get tab label from config
|
|
488
|
+
*/
|
|
489
|
+
static getTabLabel(tabId, groupId, cfgGroups) {
|
|
490
|
+
if (!cfgGroups)
|
|
491
|
+
return null;
|
|
492
|
+
const groupCfg = cfgGroups.find(g => g.id === groupId);
|
|
493
|
+
if (!groupCfg)
|
|
494
|
+
return null;
|
|
495
|
+
const tabCfg = groupCfg.tabs.find(t => t.id === tabId);
|
|
496
|
+
return tabCfg?.label || null;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Update active state in navs after selection change (single group)
|
|
500
|
+
*/
|
|
501
|
+
static updateNavActiveState(rootEl, groupId, activeTabId) {
|
|
502
|
+
const tabGroups = rootEl.querySelectorAll(`${TABGROUP_SELECTOR}[id="${groupId}"]`);
|
|
503
|
+
tabGroups.forEach((groupEl) => {
|
|
504
|
+
const navLinks = groupEl.querySelectorAll('.nav-link');
|
|
505
|
+
navLinks.forEach((link) => {
|
|
506
|
+
const tabId = link.getAttribute('data-tab-id');
|
|
507
|
+
if (tabId === activeTabId) {
|
|
508
|
+
link.classList.add('active');
|
|
509
|
+
link.setAttribute('aria-selected', 'true');
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
link.classList.remove('active');
|
|
513
|
+
link.setAttribute('aria-selected', 'false');
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Update active states for all tab groups based on current state
|
|
520
|
+
*/
|
|
521
|
+
static updateAllNavActiveStates(rootEl, tabs, cfgGroups) {
|
|
522
|
+
const tabGroups = rootEl.querySelectorAll(TABGROUP_SELECTOR);
|
|
523
|
+
tabGroups.forEach((groupEl) => {
|
|
524
|
+
const groupId = groupEl.getAttribute('id');
|
|
525
|
+
if (!groupId)
|
|
526
|
+
return;
|
|
527
|
+
// Determine the active tab for this group
|
|
528
|
+
const activeTabId = this.resolveActiveTab(groupId, tabs, cfgGroups, groupEl);
|
|
529
|
+
if (!activeTabId)
|
|
530
|
+
return;
|
|
531
|
+
// Update nav links for this group
|
|
532
|
+
const navLinks = groupEl.querySelectorAll('.nav-link');
|
|
533
|
+
navLinks.forEach((link) => {
|
|
534
|
+
const tabId = link.getAttribute('data-tab-id');
|
|
535
|
+
if (tabId === activeTabId) {
|
|
536
|
+
link.classList.add('active');
|
|
537
|
+
link.setAttribute('aria-selected', 'true');
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
link.classList.remove('active');
|
|
541
|
+
link.setAttribute('aria-selected', 'false');
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Styles for toggle visibility and animations
|
|
550
|
+
*/
|
|
551
|
+
const TOGGLE_STYLES = `
|
|
552
|
+
/* Core toggle visibility transitions */
|
|
553
|
+
[data-cv-toggle], [data-customviews-toggle] {
|
|
349
554
|
transition: opacity 150ms ease,
|
|
350
555
|
transform 150ms ease,
|
|
351
556
|
max-height 200ms ease,
|
|
@@ -372,6 +577,125 @@
|
|
|
372
577
|
margin-bottom: 0 !important;
|
|
373
578
|
overflow: hidden !important;
|
|
374
579
|
}
|
|
580
|
+
`;
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Styles for tab groups and tab navigation
|
|
584
|
+
*/
|
|
585
|
+
const TAB_STYLES = `
|
|
586
|
+
/* Tab navigation styles - Bootstrap-style tabs matching MarkBind */
|
|
587
|
+
.cv-tabs-nav {
|
|
588
|
+
display: flex;
|
|
589
|
+
flex-wrap: wrap;
|
|
590
|
+
padding-left: 0;
|
|
591
|
+
margin-top: 0.5rem;
|
|
592
|
+
margin-bottom: 1rem;
|
|
593
|
+
list-style: none;
|
|
594
|
+
border-bottom: 1px solid #dee2e6;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.cv-tabs-nav .nav-item {
|
|
598
|
+
margin-bottom: -1px;
|
|
599
|
+
list-style: none;
|
|
600
|
+
display: inline-block;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.cv-tabs-nav .nav-link {
|
|
604
|
+
display: block;
|
|
605
|
+
padding: 0.5rem 1rem;
|
|
606
|
+
color: #495057;
|
|
607
|
+
text-decoration: none;
|
|
608
|
+
background-color: transparent;
|
|
609
|
+
border: 1px solid transparent;
|
|
610
|
+
border-top-left-radius: 0.25rem;
|
|
611
|
+
border-top-right-radius: 0.25rem;
|
|
612
|
+
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
|
|
613
|
+
cursor: pointer;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.cv-tabs-nav .nav-link:hover,
|
|
617
|
+
.cv-tabs-nav .nav-link:focus {
|
|
618
|
+
border-color: #e9ecef #e9ecef #dee2e6;
|
|
619
|
+
isolation: isolate;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
.cv-tabs-nav .nav-link.active {
|
|
623
|
+
color: #495057;
|
|
624
|
+
background-color: #fff;
|
|
625
|
+
border-color: #dee2e6 #dee2e6 #fff;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
.cv-tabs-nav .nav-link:focus {
|
|
629
|
+
outline: 0;
|
|
630
|
+
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/* Legacy button-based nav (deprecated, kept for compatibility) */
|
|
634
|
+
.cv-tabs-nav-item {
|
|
635
|
+
background: none;
|
|
636
|
+
border: none;
|
|
637
|
+
border-bottom: 2px solid transparent;
|
|
638
|
+
padding: 0.5rem 1rem;
|
|
639
|
+
cursor: pointer;
|
|
640
|
+
font-size: 1rem;
|
|
641
|
+
color: #6c757d;
|
|
642
|
+
transition: color 150ms ease, border-color 150ms ease;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.cv-tabs-nav-item:hover {
|
|
646
|
+
color: #495057;
|
|
647
|
+
border-bottom-color: #dee2e6;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
.cv-tabs-nav-item.active {
|
|
651
|
+
color: #007bff;
|
|
652
|
+
border-bottom-color: #007bff;
|
|
653
|
+
font-weight: 500;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.cv-tabs-nav-item:focus {
|
|
657
|
+
outline: 2px solid #007bff;
|
|
658
|
+
outline-offset: 2px;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/* Tab panel base styles */
|
|
662
|
+
cv-tab {
|
|
663
|
+
display: block;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/* Override visibility for tab panels - use display instead of collapse animation */
|
|
667
|
+
cv-tab.cv-hidden {
|
|
668
|
+
display: none !important;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
cv-tab.cv-visible {
|
|
672
|
+
display: block !important;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
cv-tabgroup {
|
|
676
|
+
display: block;
|
|
677
|
+
margin-bottom: 1.5rem;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/* Bottom border line for tab groups */
|
|
681
|
+
.cv-tabgroup-bottom-border {
|
|
682
|
+
border-bottom: 1px solid #dee2e6;
|
|
683
|
+
margin-top: 1rem;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/* Tab content wrapper */
|
|
687
|
+
.cv-tab-content {
|
|
688
|
+
padding: 1rem 0;
|
|
689
|
+
}
|
|
690
|
+
`;
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Combined core styles for toggles and tabs
|
|
694
|
+
*/
|
|
695
|
+
const CORE_STYLES = `
|
|
696
|
+
${TOGGLE_STYLES}
|
|
697
|
+
|
|
698
|
+
${TAB_STYLES}
|
|
375
699
|
`;
|
|
376
700
|
/**
|
|
377
701
|
* Add styles for hiding and showing toggles animations and transitions to the document head
|
|
@@ -392,36 +716,85 @@
|
|
|
392
716
|
assetsManager;
|
|
393
717
|
persistenceManager;
|
|
394
718
|
visibilityManager;
|
|
395
|
-
|
|
396
|
-
localConfig;
|
|
719
|
+
config;
|
|
397
720
|
stateChangeListeners = [];
|
|
721
|
+
showUrlEnabled;
|
|
722
|
+
lastAppliedState = null;
|
|
398
723
|
constructor(opt) {
|
|
399
724
|
this.assetsManager = opt.assetsManager;
|
|
400
|
-
this.
|
|
725
|
+
this.config = opt.config;
|
|
401
726
|
this.rootEl = opt.rootEl || document.body;
|
|
402
727
|
this.persistenceManager = new PersistenceManager();
|
|
403
728
|
this.visibilityManager = new VisibilityManager();
|
|
729
|
+
this.showUrlEnabled = opt.showUrl ?? true;
|
|
730
|
+
this.lastAppliedState = this.cloneState(this.config?.defaultState);
|
|
731
|
+
}
|
|
732
|
+
getConfig() {
|
|
733
|
+
return this.config;
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Get tab groups from config
|
|
737
|
+
*/
|
|
738
|
+
getTabGroups() {
|
|
739
|
+
return this.config.tabGroups;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Get currently active tabs (from URL > persisted (localStorage) > defaults)
|
|
743
|
+
*/
|
|
744
|
+
getCurrentActiveTabs() {
|
|
745
|
+
if (this.lastAppliedState?.tabs) {
|
|
746
|
+
return { ...this.lastAppliedState.tabs };
|
|
747
|
+
}
|
|
748
|
+
const persistedState = this.persistenceManager.getPersistedState();
|
|
749
|
+
if (persistedState?.tabs) {
|
|
750
|
+
return { ...persistedState.tabs };
|
|
751
|
+
}
|
|
752
|
+
return this.config?.defaultState?.tabs ? { ...this.config.defaultState.tabs } : {};
|
|
404
753
|
}
|
|
405
|
-
|
|
406
|
-
|
|
754
|
+
/**
|
|
755
|
+
* Set active tab for a group and apply state
|
|
756
|
+
*/
|
|
757
|
+
setActiveTab(groupId, tabId) {
|
|
758
|
+
// Get current state
|
|
759
|
+
const currentToggles = this.getCurrentActiveToggles();
|
|
760
|
+
const currentTabs = this.getCurrentActiveTabs();
|
|
761
|
+
// Merge new tab selection
|
|
762
|
+
const newTabs = { ...currentTabs, [groupId]: tabId };
|
|
763
|
+
// Create new state
|
|
764
|
+
const newState = {
|
|
765
|
+
toggles: currentToggles,
|
|
766
|
+
tabs: newTabs
|
|
767
|
+
};
|
|
768
|
+
// Apply the state
|
|
769
|
+
this.applyState(newState);
|
|
770
|
+
// Emit custom event
|
|
771
|
+
const event = new CustomEvent('customviews:tab-change', {
|
|
772
|
+
detail: { groupId, tabId },
|
|
773
|
+
bubbles: true
|
|
774
|
+
});
|
|
775
|
+
document.dispatchEvent(event);
|
|
407
776
|
}
|
|
408
777
|
// Inject styles, setup listeners and call rendering logic
|
|
409
778
|
async init() {
|
|
410
779
|
injectCoreStyles();
|
|
780
|
+
// Build navigation once (with click handlers)
|
|
781
|
+
TabManager.buildNavs(this.rootEl, this.config.tabGroups, (groupId, tabId) => {
|
|
782
|
+
this.setActiveTab(groupId, tabId);
|
|
783
|
+
});
|
|
411
784
|
// For session history, clicks on back/forward button
|
|
412
785
|
window.addEventListener("popstate", () => {
|
|
413
|
-
this.
|
|
786
|
+
this.loadAndCallApplyState();
|
|
414
787
|
});
|
|
415
|
-
this.
|
|
788
|
+
this.loadAndCallApplyState();
|
|
416
789
|
}
|
|
417
790
|
// Priority: URL state > persisted state > default
|
|
418
791
|
// Also filters using the visibility manager to persist selection
|
|
419
792
|
// across back/forward button clicks
|
|
420
|
-
async
|
|
793
|
+
async loadAndCallApplyState() {
|
|
421
794
|
// 1. URL State
|
|
422
|
-
|
|
423
|
-
if (
|
|
424
|
-
this.applyState(
|
|
795
|
+
const urlState = URLStateManager.parseURL();
|
|
796
|
+
if (urlState) {
|
|
797
|
+
this.applyState(urlState);
|
|
425
798
|
return;
|
|
426
799
|
}
|
|
427
800
|
// 2. Persisted State
|
|
@@ -431,38 +804,48 @@
|
|
|
431
804
|
return;
|
|
432
805
|
}
|
|
433
806
|
// 3. Local Config Fallback
|
|
434
|
-
this.renderState(this.
|
|
807
|
+
this.renderState(this.config.defaultState);
|
|
435
808
|
}
|
|
436
809
|
/**
|
|
437
|
-
|
|
438
|
-
|
|
810
|
+
* Apply a custom state, saves to localStorage and updates the URL
|
|
811
|
+
*/
|
|
439
812
|
applyState(state) {
|
|
440
|
-
this.
|
|
441
|
-
this.
|
|
442
|
-
this.
|
|
443
|
-
|
|
813
|
+
const snapshot = this.cloneState(state);
|
|
814
|
+
this.renderState(snapshot);
|
|
815
|
+
this.persistenceManager.persistState(snapshot);
|
|
816
|
+
if (this.showUrlEnabled) {
|
|
817
|
+
URLStateManager.updateURL(snapshot);
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
URLStateManager.clearURL();
|
|
821
|
+
}
|
|
444
822
|
}
|
|
445
823
|
/** Render all toggles for the current state */
|
|
446
824
|
renderState(state) {
|
|
825
|
+
this.lastAppliedState = this.cloneState(state);
|
|
447
826
|
const toggles = state.toggles || [];
|
|
448
827
|
const finalToggles = this.visibilityManager.filterVisibleToggles(toggles);
|
|
449
828
|
// Toggles hide or show relevant toggles
|
|
450
|
-
this.rootEl.querySelectorAll("[data-customviews-toggle]").forEach(el => {
|
|
451
|
-
const category = el.dataset.customviewsToggle;
|
|
829
|
+
this.rootEl.querySelectorAll("[data-cv-toggle], [data-customviews-toggle]").forEach(el => {
|
|
830
|
+
const category = el.dataset.cvToggle || el.dataset.customviewsToggle;
|
|
452
831
|
const shouldShow = !!category && finalToggles.includes(category);
|
|
453
832
|
this.visibilityManager.applyElementVisibility(el, shouldShow);
|
|
454
833
|
});
|
|
455
834
|
// Render toggles
|
|
456
835
|
for (const category of finalToggles) {
|
|
457
|
-
this.rootEl.querySelectorAll(`[data-customviews-toggle="${category}"]`).forEach(el => {
|
|
458
|
-
// if it has an id, then we render the asset into it
|
|
459
|
-
//
|
|
460
|
-
const toggleId = el.dataset.customviewsId;
|
|
836
|
+
this.rootEl.querySelectorAll(`[data-cv-toggle="${category}"], [data-customviews-toggle="${category}"]`).forEach(el => {
|
|
837
|
+
// if it has an id, then we should render the asset into it
|
|
838
|
+
// Support both (data-cv-id) and (data-customviews-id) attributes
|
|
839
|
+
const toggleId = el.dataset.cvId || el.dataset.customviewsId;
|
|
461
840
|
if (toggleId) {
|
|
462
841
|
renderAssetInto(el, toggleId, this.assetsManager);
|
|
463
842
|
}
|
|
464
843
|
});
|
|
465
844
|
}
|
|
845
|
+
// Apply tab selections
|
|
846
|
+
TabManager.applySelections(this.rootEl, state.tabs || {}, this.config.tabGroups);
|
|
847
|
+
// Update nav active states (without rebuilding)
|
|
848
|
+
TabManager.updateAllNavActiveStates(this.rootEl, state.tabs || {}, this.config.tabGroups);
|
|
466
849
|
// Notify state change listeners (like widgets)
|
|
467
850
|
this.notifyStateChangeListeners();
|
|
468
851
|
}
|
|
@@ -470,10 +853,9 @@
|
|
|
470
853
|
* Reset to default state
|
|
471
854
|
*/
|
|
472
855
|
resetToDefault() {
|
|
473
|
-
this.stateFromUrl = null;
|
|
474
856
|
this.persistenceManager.clearAll();
|
|
475
|
-
if (this.
|
|
476
|
-
this.renderState(this.
|
|
857
|
+
if (this.config) {
|
|
858
|
+
this.renderState(this.config.defaultState);
|
|
477
859
|
}
|
|
478
860
|
else {
|
|
479
861
|
console.warn("No configuration loaded, cannot reset to default state");
|
|
@@ -485,15 +867,12 @@
|
|
|
485
867
|
* Get the currently active toggles regardless of whether they come from custom state or default configuration
|
|
486
868
|
*/
|
|
487
869
|
getCurrentActiveToggles() {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
return this.stateFromUrl.toggles || [];
|
|
870
|
+
if (this.lastAppliedState) {
|
|
871
|
+
return this.lastAppliedState.toggles || [];
|
|
491
872
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
return this.localConfig.defaultState.toggles || [];
|
|
873
|
+
if (this.config) {
|
|
874
|
+
return this.config.defaultState.toggles || [];
|
|
495
875
|
}
|
|
496
|
-
// No configuration or state
|
|
497
876
|
return [];
|
|
498
877
|
}
|
|
499
878
|
/**
|
|
@@ -501,15 +880,35 @@
|
|
|
501
880
|
*/
|
|
502
881
|
clearPersistence() {
|
|
503
882
|
this.persistenceManager.clearAll();
|
|
504
|
-
this.
|
|
505
|
-
|
|
506
|
-
this.renderState(this.localConfig.defaultState);
|
|
883
|
+
if (this.config) {
|
|
884
|
+
this.renderState(this.config.defaultState);
|
|
507
885
|
}
|
|
508
886
|
else {
|
|
509
887
|
console.warn("No configuration loaded, cannot reset to default state");
|
|
510
888
|
}
|
|
511
889
|
URLStateManager.clearURL();
|
|
512
890
|
}
|
|
891
|
+
setOption(flag, value) {
|
|
892
|
+
switch (flag) {
|
|
893
|
+
case 'showUrl': {
|
|
894
|
+
const nextValue = Boolean(value);
|
|
895
|
+
if (this.showUrlEnabled === nextValue) {
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
this.showUrlEnabled = nextValue;
|
|
899
|
+
if (nextValue) {
|
|
900
|
+
const stateForUrl = this.getTrackedStateSnapshot();
|
|
901
|
+
URLStateManager.updateURL(stateForUrl);
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
URLStateManager.clearURL();
|
|
905
|
+
}
|
|
906
|
+
break;
|
|
907
|
+
}
|
|
908
|
+
default:
|
|
909
|
+
console.warn(`[CustomViews] Unknown option '${flag}' passed to setOption`);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
513
912
|
// === STATE CHANGE LISTENER METHODS ===
|
|
514
913
|
/**
|
|
515
914
|
* Add a listener that will be called whenever the state changes
|
|
@@ -539,6 +938,20 @@
|
|
|
539
938
|
}
|
|
540
939
|
});
|
|
541
940
|
}
|
|
941
|
+
cloneState(state) {
|
|
942
|
+
const toggles = state?.toggles ? [...state.toggles] : [];
|
|
943
|
+
const tabs = state?.tabs ? { ...state.tabs } : undefined;
|
|
944
|
+
return tabs ? { toggles, tabs } : { toggles };
|
|
945
|
+
}
|
|
946
|
+
getTrackedStateSnapshot() {
|
|
947
|
+
if (this.lastAppliedState) {
|
|
948
|
+
return this.cloneState(this.lastAppliedState);
|
|
949
|
+
}
|
|
950
|
+
if (this.config) {
|
|
951
|
+
return this.cloneState(this.config.defaultState);
|
|
952
|
+
}
|
|
953
|
+
return { toggles: [] };
|
|
954
|
+
}
|
|
542
955
|
}
|
|
543
956
|
|
|
544
957
|
class AssetsManager {
|
|
@@ -587,6 +1000,70 @@
|
|
|
587
1000
|
}
|
|
588
1001
|
}
|
|
589
1002
|
|
|
1003
|
+
/**
|
|
1004
|
+
* Helper function to prepend baseUrl to a path
|
|
1005
|
+
* @param path The path to prepend the baseUrl to
|
|
1006
|
+
* @param baseUrl The base URL to prepend
|
|
1007
|
+
* @returns The full URL with baseUrl prepended if applicable
|
|
1008
|
+
*/
|
|
1009
|
+
function prependBaseUrl(path, baseUrl) {
|
|
1010
|
+
if (!baseUrl)
|
|
1011
|
+
return path;
|
|
1012
|
+
// Don't prepend if the path is already absolute (starts with http:// or https://)
|
|
1013
|
+
if (path.startsWith('http://') || path.startsWith('https://')) {
|
|
1014
|
+
return path;
|
|
1015
|
+
}
|
|
1016
|
+
// Ensure baseUrl doesn't end with / and path starts with /
|
|
1017
|
+
const cleanbaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
1018
|
+
const cleanPath = path.startsWith('/') ? path : '/' + path;
|
|
1019
|
+
return cleanbaseUrl + cleanPath;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Main CustomViews class for initializing and managing custom views
|
|
1024
|
+
*/
|
|
1025
|
+
class CustomViews {
|
|
1026
|
+
/**
|
|
1027
|
+
* Entry Point to use CustomViews
|
|
1028
|
+
* @param opts Initialization options including config object and assets path
|
|
1029
|
+
* @returns Promise resolving to the CustomViewsCore instance or null if initialization fails
|
|
1030
|
+
*/
|
|
1031
|
+
static async init(opts) {
|
|
1032
|
+
// Load assets JSON if provided
|
|
1033
|
+
let assetsManager;
|
|
1034
|
+
const baseURL = opts.baseURL || '';
|
|
1035
|
+
if (opts.assetsJsonPath) {
|
|
1036
|
+
const assetsPath = prependBaseUrl(opts.assetsJsonPath, baseURL);
|
|
1037
|
+
const assetsJson = await (await fetch(assetsPath)).json();
|
|
1038
|
+
assetsManager = new AssetsManager(assetsJson, baseURL);
|
|
1039
|
+
}
|
|
1040
|
+
else {
|
|
1041
|
+
assetsManager = new AssetsManager({}, baseURL);
|
|
1042
|
+
}
|
|
1043
|
+
// Use provided config or create a minimal default one
|
|
1044
|
+
let config;
|
|
1045
|
+
if (opts.config) {
|
|
1046
|
+
config = opts.config;
|
|
1047
|
+
}
|
|
1048
|
+
else {
|
|
1049
|
+
console.error("No config provided, using minimal default config");
|
|
1050
|
+
// Create a minimal default config
|
|
1051
|
+
config = { allToggles: [], defaultState: { toggles: [] } };
|
|
1052
|
+
}
|
|
1053
|
+
const coreOptions = {
|
|
1054
|
+
assetsManager,
|
|
1055
|
+
config: config,
|
|
1056
|
+
rootEl: opts.rootEl,
|
|
1057
|
+
};
|
|
1058
|
+
if (opts.showUrl !== undefined) {
|
|
1059
|
+
coreOptions.showUrl = opts.showUrl;
|
|
1060
|
+
}
|
|
1061
|
+
const core = new CustomViewsCore(coreOptions);
|
|
1062
|
+
core.init();
|
|
1063
|
+
return core;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
590
1067
|
/**
|
|
591
1068
|
* Widget styles for CustomViews
|
|
592
1069
|
* Extracted from widget.ts for better maintainability
|
|
@@ -598,8 +1075,10 @@
|
|
|
598
1075
|
/* Rounded rectangle widget icon styles */
|
|
599
1076
|
.cv-widget-icon {
|
|
600
1077
|
position: fixed;
|
|
601
|
-
|
|
602
|
-
|
|
1078
|
+
/* Slightly transparent by default so the widget is subtle at the page edge */
|
|
1079
|
+
background: rgba(255, 255, 255, 0.92);
|
|
1080
|
+
color: rgba(0, 0, 0, 0.9);
|
|
1081
|
+
opacity: 0.6;
|
|
603
1082
|
display: flex;
|
|
604
1083
|
align-items: center;
|
|
605
1084
|
justify-content: center;
|
|
@@ -613,8 +1092,10 @@
|
|
|
613
1092
|
}
|
|
614
1093
|
|
|
615
1094
|
.cv-widget-icon:hover {
|
|
616
|
-
|
|
617
|
-
|
|
1095
|
+
/* Become fully opaque on hover to improve readability */
|
|
1096
|
+
background: rgba(255, 255, 255, 1);
|
|
1097
|
+
color: rgba(0, 0, 0, 1);
|
|
1098
|
+
opacity: 1;
|
|
618
1099
|
}
|
|
619
1100
|
|
|
620
1101
|
/* Top-right: rounded end on left, sticks out leftward on hover */
|
|
@@ -1021,6 +1502,43 @@
|
|
|
1021
1502
|
width: auto;
|
|
1022
1503
|
}
|
|
1023
1504
|
|
|
1505
|
+
.cv-tab-groups {
|
|
1506
|
+
margin-top: 20px;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
.cv-tab-group-control {
|
|
1510
|
+
margin-bottom: 15px;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
.cv-tab-group-control label {
|
|
1514
|
+
display: block;
|
|
1515
|
+
margin-bottom: 5px;
|
|
1516
|
+
font-weight: 500;
|
|
1517
|
+
font-size: 14px;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
.cv-tab-group-select {
|
|
1521
|
+
width: 100%;
|
|
1522
|
+
padding: 8px 12px;
|
|
1523
|
+
border: 1px solid #ced4da;
|
|
1524
|
+
border-radius: 4px;
|
|
1525
|
+
font-size: 14px;
|
|
1526
|
+
background-color: white;
|
|
1527
|
+
cursor: pointer;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
.cv-tab-group-select:focus {
|
|
1531
|
+
outline: none;
|
|
1532
|
+
border-color: #007bff;
|
|
1533
|
+
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
.cv-widget-theme-dark .cv-tab-group-select {
|
|
1537
|
+
background-color: #2d3748;
|
|
1538
|
+
border-color: #4a5568;
|
|
1539
|
+
color: #e2e8f0;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1024
1542
|
.cv-custom-state-actions {
|
|
1025
1543
|
display: flex;
|
|
1026
1544
|
gap: 10px;
|
|
@@ -1209,7 +1727,8 @@
|
|
|
1209
1727
|
description: options.description || 'Toggle different content sections to customize your view. Changes are applied instantly and the URL will be updated for sharing.',
|
|
1210
1728
|
showWelcome: options.showWelcome ?? false,
|
|
1211
1729
|
welcomeTitle: options.welcomeTitle || 'Welcome to Custom Views!',
|
|
1212
|
-
welcomeMessage: options.welcomeMessage || 'This website uses Custom Views to let you personalize your experience. Use the widget on the side (⚙) to show or hide different content sections based on your preferences. Your selections will be saved and can be shared via URL.'
|
|
1730
|
+
welcomeMessage: options.welcomeMessage || 'This website uses Custom Views to let you personalize your experience. Use the widget on the side (⚙) to show or hide different content sections based on your preferences. Your selections will be saved and can be shared via URL.',
|
|
1731
|
+
showTabGroups: options.showTabGroups ?? true
|
|
1213
1732
|
};
|
|
1214
1733
|
// No external state manager to initialize
|
|
1215
1734
|
}
|
|
@@ -1274,8 +1793,8 @@
|
|
|
1274
1793
|
*/
|
|
1275
1794
|
openStateModal() {
|
|
1276
1795
|
// Get toggles from current configuration and open the modal regardless of count
|
|
1277
|
-
const
|
|
1278
|
-
const toggles =
|
|
1796
|
+
const config = this.core.getConfig();
|
|
1797
|
+
const toggles = config?.allToggles || [];
|
|
1279
1798
|
this.createCustomStateModal(toggles);
|
|
1280
1799
|
}
|
|
1281
1800
|
/**
|
|
@@ -1297,6 +1816,28 @@
|
|
|
1297
1816
|
</div>
|
|
1298
1817
|
`).join('')
|
|
1299
1818
|
: `<p class="cv-no-toggles">No configurable sections available.</p>`;
|
|
1819
|
+
// Get tab groups
|
|
1820
|
+
const tabGroups = this.core.getTabGroups();
|
|
1821
|
+
let tabGroupsHTML = '';
|
|
1822
|
+
if (this.options.showTabGroups && tabGroups && tabGroups.length > 0) {
|
|
1823
|
+
const tabGroupControls = tabGroups.map(group => {
|
|
1824
|
+
const options = group.tabs.map(tab => `<option value="${tab.id}">${tab.label || tab.id}</option>`).join('');
|
|
1825
|
+
return `
|
|
1826
|
+
<div class="cv-tab-group-control">
|
|
1827
|
+
<label for="tab-group-${group.id}">${group.label || group.id}</label>
|
|
1828
|
+
<select id="tab-group-${group.id}" class="cv-tab-group-select" data-group-id="${group.id}">
|
|
1829
|
+
${options}
|
|
1830
|
+
</select>
|
|
1831
|
+
</div>
|
|
1832
|
+
`;
|
|
1833
|
+
}).join('');
|
|
1834
|
+
tabGroupsHTML = `
|
|
1835
|
+
<h4>Tab Groups</h4>
|
|
1836
|
+
<div class="cv-tab-groups">
|
|
1837
|
+
${tabGroupControls}
|
|
1838
|
+
</div>
|
|
1839
|
+
`;
|
|
1840
|
+
}
|
|
1300
1841
|
this.modal.innerHTML = `
|
|
1301
1842
|
<div class="cv-widget-modal cv-custom-state-modal">
|
|
1302
1843
|
<div class="cv-widget-modal-header">
|
|
@@ -1312,6 +1853,8 @@
|
|
|
1312
1853
|
${toggleControls}
|
|
1313
1854
|
</div>
|
|
1314
1855
|
|
|
1856
|
+
${tabGroupsHTML}
|
|
1857
|
+
|
|
1315
1858
|
<div class="cv-custom-state-actions">
|
|
1316
1859
|
${this.options.showReset ? `<button class="cv-custom-state-reset">Reset to Default</button>` : ''}
|
|
1317
1860
|
<button class="cv-custom-state-copy-url">Copy Shareable URL</button>
|
|
@@ -1361,6 +1904,17 @@
|
|
|
1361
1904
|
this.core.applyState(state);
|
|
1362
1905
|
});
|
|
1363
1906
|
});
|
|
1907
|
+
// Listen to tab group selects
|
|
1908
|
+
const tabGroupSelects = this.modal.querySelectorAll('.cv-tab-group-select');
|
|
1909
|
+
tabGroupSelects.forEach(select => {
|
|
1910
|
+
select.addEventListener('change', () => {
|
|
1911
|
+
const groupId = select.dataset.groupId;
|
|
1912
|
+
const tabId = select.value;
|
|
1913
|
+
if (groupId && tabId) {
|
|
1914
|
+
this.core.setActiveTab(groupId, tabId);
|
|
1915
|
+
}
|
|
1916
|
+
});
|
|
1917
|
+
});
|
|
1364
1918
|
// Overlay click to close
|
|
1365
1919
|
this.modal.addEventListener('click', (e) => {
|
|
1366
1920
|
if (e.target === this.modal) {
|
|
@@ -1405,7 +1959,16 @@
|
|
|
1405
1959
|
toggles.push(toggle);
|
|
1406
1960
|
}
|
|
1407
1961
|
});
|
|
1408
|
-
|
|
1962
|
+
// Collect tab selections
|
|
1963
|
+
const tabGroupSelects = this.modal.querySelectorAll('.cv-tab-group-select');
|
|
1964
|
+
const tabs = {};
|
|
1965
|
+
tabGroupSelects.forEach(select => {
|
|
1966
|
+
const groupId = select.dataset.groupId;
|
|
1967
|
+
if (groupId) {
|
|
1968
|
+
tabs[groupId] = select.value;
|
|
1969
|
+
}
|
|
1970
|
+
});
|
|
1971
|
+
return Object.keys(tabs).length > 0 ? { toggles, tabs } : { toggles };
|
|
1409
1972
|
}
|
|
1410
1973
|
/**
|
|
1411
1974
|
* Copy shareable URL to clipboard
|
|
@@ -1441,6 +2004,15 @@
|
|
|
1441
2004
|
}
|
|
1442
2005
|
}
|
|
1443
2006
|
});
|
|
2007
|
+
// Load tab group selections
|
|
2008
|
+
const activeTabs = this.core.getCurrentActiveTabs();
|
|
2009
|
+
const tabGroupSelects = this.modal.querySelectorAll('.cv-tab-group-select');
|
|
2010
|
+
tabGroupSelects.forEach(select => {
|
|
2011
|
+
const groupId = select.dataset.groupId;
|
|
2012
|
+
if (groupId && activeTabs[groupId]) {
|
|
2013
|
+
select.value = activeTabs[groupId];
|
|
2014
|
+
}
|
|
2015
|
+
});
|
|
1444
2016
|
}
|
|
1445
2017
|
/**
|
|
1446
2018
|
* Format toggle name for display
|
|
@@ -1534,61 +2106,204 @@
|
|
|
1534
2106
|
}
|
|
1535
2107
|
}
|
|
1536
2108
|
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
2109
|
+
/**
|
|
2110
|
+
* Custom Elements for Tab Groups and Tabs
|
|
2111
|
+
*/
|
|
2112
|
+
/**
|
|
2113
|
+
* <cv-tab> element - represents a single tab panel
|
|
2114
|
+
*/
|
|
2115
|
+
class CVTab extends HTMLElement {
|
|
2116
|
+
connectedCallback() {
|
|
2117
|
+
// Element is managed by TabManager
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
/**
|
|
2121
|
+
* <cv-tabgroup> element - represents a group of tabs
|
|
2122
|
+
*/
|
|
2123
|
+
class CVTabgroup extends HTMLElement {
|
|
2124
|
+
connectedCallback() {
|
|
2125
|
+
// Element is managed by TabManager
|
|
2126
|
+
// Emit ready event after a brief delay to ensure children are parsed
|
|
2127
|
+
setTimeout(() => {
|
|
2128
|
+
const event = new CustomEvent('cv:tabgroup-ready', {
|
|
2129
|
+
bubbles: true,
|
|
2130
|
+
detail: { groupId: this.getAttribute('id') }
|
|
2131
|
+
});
|
|
2132
|
+
this.dispatchEvent(event);
|
|
2133
|
+
}, 0);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
/**
|
|
2137
|
+
* Register custom elements
|
|
2138
|
+
*/
|
|
2139
|
+
function registerCustomElements() {
|
|
2140
|
+
// Only register if not already defined
|
|
2141
|
+
if (!customElements.get('cv-tab')) {
|
|
2142
|
+
customElements.define('cv-tab', CVTab);
|
|
2143
|
+
}
|
|
2144
|
+
if (!customElements.get('cv-tabgroup')) {
|
|
2145
|
+
customElements.define('cv-tabgroup', CVTabgroup);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
/**
|
|
2150
|
+
* Initialize CustomViews from script tag attributes and config file
|
|
2151
|
+
* This function handles the automatic initialization of CustomViews when included via script tag
|
|
2152
|
+
*
|
|
2153
|
+
* Data attributes supported:
|
|
2154
|
+
* - data-base-url: Base URL for the site (e.g., "/customviews" for subdirectory deployments)
|
|
2155
|
+
* - data-config-path: Path to config file (default: "/customviews.config.json")
|
|
2156
|
+
*
|
|
2157
|
+
* The function fetches the config file and uses it directly to initialize CustomViews.
|
|
2158
|
+
* Widget visibility is controlled via the config file (widget.enabled property).
|
|
2159
|
+
*/
|
|
2160
|
+
function initializeFromScript() {
|
|
2161
|
+
// Only run in browser environment
|
|
2162
|
+
if (typeof window === 'undefined')
|
|
2163
|
+
return;
|
|
2164
|
+
// Use the typed global `window` (see src/types/global.d.ts)
|
|
2165
|
+
// Idempotency guard: if already initialized, skip setting up listener again.
|
|
2166
|
+
if (window.__customViewsInitialized) {
|
|
2167
|
+
// Informational for developers; harmless in production.
|
|
2168
|
+
console.info('[CustomViews] Auto-init skipped: already initialized.');
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
document.addEventListener('DOMContentLoaded', async function () {
|
|
2172
|
+
// Prevent concurrent initialization runs (race conditions when script is loaded twice)
|
|
2173
|
+
if (window.__customViewsInitInProgress || window.__customViewsInitialized) {
|
|
2174
|
+
return;
|
|
1554
2175
|
}
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
2176
|
+
window.__customViewsInitInProgress = true;
|
|
2177
|
+
// Register custom elements early
|
|
2178
|
+
registerCustomElements();
|
|
2179
|
+
try {
|
|
2180
|
+
// Find the script tag
|
|
2181
|
+
let scriptTag = document.currentScript;
|
|
2182
|
+
// Fallback if currentScript is not available (executed after page load)
|
|
2183
|
+
if (!scriptTag) {
|
|
2184
|
+
// Try to find the script tag by looking for our script
|
|
2185
|
+
const scripts = document.querySelectorAll('script[src*="custom-views"]');
|
|
2186
|
+
if (scripts.length > 0) {
|
|
2187
|
+
// Find the most specific match (to avoid matching other custom-views scripts)
|
|
2188
|
+
for (let i = 0; i < scripts.length; i++) {
|
|
2189
|
+
const script = scripts[i];
|
|
2190
|
+
const src = script.getAttribute('src') || '';
|
|
2191
|
+
// Look for .min.js or .js at the end
|
|
2192
|
+
if (src.match(/custom-views(\.min)?\.js($|\?)/)) {
|
|
2193
|
+
scriptTag = script;
|
|
2194
|
+
break;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
// If no specific match found, use the first one
|
|
2198
|
+
if (!scriptTag) {
|
|
2199
|
+
scriptTag = scripts[0];
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
// Read data attributes from script tag
|
|
2204
|
+
let baseURL = '';
|
|
2205
|
+
let configPath = '/customviews.config.json';
|
|
2206
|
+
if (scriptTag) {
|
|
2207
|
+
baseURL = scriptTag.getAttribute('data-base-url') || '';
|
|
2208
|
+
configPath = scriptTag.getAttribute('data-config-path') || configPath;
|
|
1559
2209
|
}
|
|
2210
|
+
// Fetch config file
|
|
2211
|
+
let configFile;
|
|
1560
2212
|
try {
|
|
1561
|
-
|
|
2213
|
+
const fullConfigPath = prependBaseUrl(configPath, baseURL);
|
|
2214
|
+
console.log(`[CustomViews] Loading config from: ${fullConfigPath}`);
|
|
2215
|
+
const response = await fetch(fullConfigPath);
|
|
2216
|
+
if (!response.ok) {
|
|
2217
|
+
console.warn(`[CustomViews] Config file not found at ${fullConfigPath}. Using defaults.`);
|
|
2218
|
+
// Provide minimal default config structure
|
|
2219
|
+
configFile = {
|
|
2220
|
+
config: {
|
|
2221
|
+
allToggles: [],
|
|
2222
|
+
defaultState: { toggles: [] }
|
|
2223
|
+
},
|
|
2224
|
+
widget: {
|
|
2225
|
+
enabled: true
|
|
2226
|
+
}
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
else {
|
|
2230
|
+
configFile = await response.json();
|
|
2231
|
+
console.log('[CustomViews] Config loaded successfully');
|
|
2232
|
+
}
|
|
1562
2233
|
}
|
|
1563
2234
|
catch (error) {
|
|
1564
|
-
console.error(
|
|
1565
|
-
return
|
|
2235
|
+
console.error('[CustomViews] Error loading config file:', error);
|
|
2236
|
+
return; // Abort initialization
|
|
2237
|
+
}
|
|
2238
|
+
// Determine effective baseURL (data attribute takes precedence over config)
|
|
2239
|
+
const effectiveBaseURL = baseURL || configFile.baseURL || '';
|
|
2240
|
+
const options = {
|
|
2241
|
+
config: configFile.config,
|
|
2242
|
+
assetsJsonPath: configFile.assetsJsonPath,
|
|
2243
|
+
baseURL: effectiveBaseURL,
|
|
2244
|
+
};
|
|
2245
|
+
if (configFile.showUrl !== undefined) {
|
|
2246
|
+
options.showUrl = configFile.showUrl;
|
|
1566
2247
|
}
|
|
2248
|
+
// Initialize CustomViews core
|
|
2249
|
+
const core = await CustomViews.init(options);
|
|
2250
|
+
if (!core) {
|
|
2251
|
+
console.error('[CustomViews] Failed to initialize core.');
|
|
2252
|
+
return; // Abort widget creation
|
|
2253
|
+
}
|
|
2254
|
+
// Store instance
|
|
2255
|
+
window.customViewsInstance = { core };
|
|
2256
|
+
// Initialize widget if enabled in config
|
|
2257
|
+
let widget;
|
|
2258
|
+
if (configFile.widget?.enabled !== false) {
|
|
2259
|
+
widget = new CustomViewsWidget({
|
|
2260
|
+
core,
|
|
2261
|
+
...configFile.widget
|
|
2262
|
+
});
|
|
2263
|
+
widget.render();
|
|
2264
|
+
// Store widget instance
|
|
2265
|
+
window.customViewsInstance.widget = widget;
|
|
2266
|
+
console.log('[CustomViews] Widget initialized and rendered');
|
|
2267
|
+
}
|
|
2268
|
+
else {
|
|
2269
|
+
console.log('[CustomViews] Widget disabled in config - skipping initialization');
|
|
2270
|
+
}
|
|
2271
|
+
// Dispatch ready event
|
|
2272
|
+
const readyEvent = new CustomEvent('customviews:ready', {
|
|
2273
|
+
detail: {
|
|
2274
|
+
core,
|
|
2275
|
+
widget
|
|
2276
|
+
}
|
|
2277
|
+
});
|
|
2278
|
+
document.dispatchEvent(readyEvent);
|
|
2279
|
+
// Mark initialized and clear in-progress flag
|
|
2280
|
+
window.__customViewsInitialized = true;
|
|
2281
|
+
window.__customViewsInitInProgress = false;
|
|
1567
2282
|
}
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
|
-
core.init();
|
|
1575
|
-
return core;
|
|
1576
|
-
}
|
|
2283
|
+
catch (error) {
|
|
2284
|
+
// Clear in-progress flag so a future attempt can retry
|
|
2285
|
+
window.__customViewsInitInProgress = false;
|
|
2286
|
+
console.error('[CustomViews] Auto-initialization error:', error);
|
|
2287
|
+
}
|
|
2288
|
+
});
|
|
1577
2289
|
}
|
|
2290
|
+
|
|
2291
|
+
// Import from new modules
|
|
2292
|
+
// Set up globals and auto-initialization
|
|
1578
2293
|
if (typeof window !== "undefined") {
|
|
1579
|
-
//
|
|
2294
|
+
// Expose to window to enable usage (e.g. const app = new window.CustomViews(...))
|
|
1580
2295
|
window.CustomViews = CustomViews;
|
|
1581
|
-
// @ts-ignore
|
|
1582
2296
|
window.CustomViewsWidget = CustomViewsWidget;
|
|
2297
|
+
// Run auto-initialization
|
|
2298
|
+
initializeFromScript();
|
|
1583
2299
|
}
|
|
1584
2300
|
|
|
1585
2301
|
exports.AssetsManager = AssetsManager;
|
|
1586
2302
|
exports.CustomViews = CustomViews;
|
|
1587
2303
|
exports.CustomViewsCore = CustomViewsCore;
|
|
1588
2304
|
exports.CustomViewsWidget = CustomViewsWidget;
|
|
1589
|
-
exports.LocalConfig = Config;
|
|
1590
2305
|
exports.PersistenceManager = PersistenceManager;
|
|
1591
2306
|
exports.URLStateManager = URLStateManager;
|
|
1592
2307
|
|
|
1593
2308
|
}));
|
|
1594
|
-
//# sourceMappingURL=custom-views.
|
|
2309
|
+
//# sourceMappingURL=custom-views.js.map
|