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