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