@customviews-js/customviews 1.1.11 → 1.2.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/dist/custom-views.core.cjs.js +360 -196
- package/dist/custom-views.core.cjs.js.map +1 -1
- package/dist/custom-views.core.esm.js +360 -196
- package/dist/custom-views.core.esm.js.map +1 -1
- package/dist/custom-views.esm.js +360 -196
- package/dist/custom-views.esm.js.map +1 -1
- package/dist/custom-views.js +360 -196
- package/dist/custom-views.js.map +1 -1
- package/dist/custom-views.min.js +2 -2
- package/dist/custom-views.min.js.map +1 -1
- package/dist/types/core/core.d.ts +18 -1
- package/dist/types/core/core.d.ts.map +1 -1
- package/dist/types/core/tab-manager.d.ts +6 -6
- package/dist/types/core/tab-manager.d.ts.map +1 -1
- package/dist/types/core/toggle-manager.d.ts +9 -4
- package/dist/types/core/toggle-manager.d.ts.map +1 -1
- package/dist/types/core/widget.d.ts +20 -11
- package/dist/types/core/widget.d.ts.map +1 -1
- package/dist/types/lib/custom-views.d.ts +1 -1
- package/dist/types/lib/custom-views.d.ts.map +1 -1
- 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 +15 -2
- package/dist/types/types/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* @customviews-js/customviews v1.
|
|
2
|
+
* @customviews-js/customviews v1.2.0
|
|
3
3
|
* (c) 2025 Chan Ger Teck
|
|
4
4
|
* Released under the MIT License.
|
|
5
5
|
*/
|
|
@@ -416,7 +416,7 @@ function getPinIcon(isPinned = false) {
|
|
|
416
416
|
}
|
|
417
417
|
|
|
418
418
|
// Constants for selectors
|
|
419
|
-
const TABGROUP_SELECTOR = 'cv-tabgroup';
|
|
419
|
+
const TABGROUP_SELECTOR$1 = 'cv-tabgroup';
|
|
420
420
|
const TAB_SELECTOR = 'cv-tab';
|
|
421
421
|
const NAV_AUTO_SELECTOR = 'cv-tabgroup[nav="auto"], cv-tabgroup:not([nav])';
|
|
422
422
|
const NAV_CONTAINER_CLASS = 'cv-tabs-nav';
|
|
@@ -432,11 +432,9 @@ class TabManager {
|
|
|
432
432
|
return trimmedIds;
|
|
433
433
|
}
|
|
434
434
|
/**
|
|
435
|
-
* Apply tab selections to
|
|
435
|
+
* Apply tab selections to a given list of tab group elements
|
|
436
436
|
*/
|
|
437
|
-
static applyTabSelections(
|
|
438
|
-
// Find all cv-tabgroup elements
|
|
439
|
-
const tabGroups = rootEl.querySelectorAll(TABGROUP_SELECTOR);
|
|
437
|
+
static applyTabSelections(tabGroups, tabs, cfgGroups) {
|
|
440
438
|
tabGroups.forEach((groupEl) => {
|
|
441
439
|
const groupId = groupEl.getAttribute('id');
|
|
442
440
|
// Determine the active tab for this group
|
|
@@ -541,10 +539,18 @@ class TabManager {
|
|
|
541
539
|
/**
|
|
542
540
|
* Build navigation for tab groups (one-time setup)
|
|
543
541
|
*/
|
|
544
|
-
static buildNavs(
|
|
545
|
-
|
|
546
|
-
const tabGroups = rootEl.querySelectorAll(NAV_AUTO_SELECTOR);
|
|
542
|
+
static buildNavs(tabGroups, cfgGroups, onTabClick, onTabDoubleClick) {
|
|
543
|
+
const rootEl = document.body; // Needed for NAV_HIDE_ROOT_CLASS check
|
|
547
544
|
tabGroups.forEach((groupEl) => {
|
|
545
|
+
// Prevent re-initialization
|
|
546
|
+
if (groupEl.hasAttribute('data-cv-initialized')) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
groupEl.setAttribute('data-cv-initialized', 'true');
|
|
550
|
+
// Filter to only build for groups with nav="auto" or no nav attribute
|
|
551
|
+
if (!groupEl.matches(NAV_AUTO_SELECTOR)) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
548
554
|
const groupId = groupEl.getAttribute('id') || null;
|
|
549
555
|
// Note: groupId can be null for standalone tabgroups
|
|
550
556
|
// These won't sync with other groups or persist state
|
|
@@ -748,8 +754,7 @@ class TabManager {
|
|
|
748
754
|
/**
|
|
749
755
|
* Update active states for all tab groups based on current state
|
|
750
756
|
*/
|
|
751
|
-
static updateAllNavActiveStates(
|
|
752
|
-
const tabGroups = rootEl.querySelectorAll(TABGROUP_SELECTOR);
|
|
757
|
+
static updateAllNavActiveStates(tabGroups, tabs, cfgGroups) {
|
|
753
758
|
tabGroups.forEach((groupEl) => {
|
|
754
759
|
const groupId = groupEl.getAttribute('id');
|
|
755
760
|
if (!groupId)
|
|
@@ -812,7 +817,7 @@ class TabManager {
|
|
|
812
817
|
*/
|
|
813
818
|
static getTabgroupsWithId(rootEl, sourceGroupId, tabId) {
|
|
814
819
|
const syncedGroupEls = [];
|
|
815
|
-
const allGroupEls = Array.from(rootEl.querySelectorAll(`${TABGROUP_SELECTOR}[id="${sourceGroupId}"]`));
|
|
820
|
+
const allGroupEls = Array.from(rootEl.querySelectorAll(`${TABGROUP_SELECTOR$1}[id="${sourceGroupId}"]`));
|
|
816
821
|
allGroupEls.forEach((targetGroupEl) => {
|
|
817
822
|
// Only sync if target group actually contains this tab
|
|
818
823
|
if (this.groupHasTab(targetGroupEl, tabId)) {
|
|
@@ -824,9 +829,8 @@ class TabManager {
|
|
|
824
829
|
/**
|
|
825
830
|
* Update pin icon visibility for all tab groups based on current state.
|
|
826
831
|
* Shows pin icon for tabs that are in the persisted state (i.e., have been double-clicked).
|
|
827
|
-
|
|
828
|
-
static updatePinIcons(
|
|
829
|
-
const tabGroups = rootEl.querySelectorAll(TABGROUP_SELECTOR);
|
|
832
|
+
*/
|
|
833
|
+
static updatePinIcons(tabGroups, tabs) {
|
|
830
834
|
tabGroups.forEach((groupEl) => {
|
|
831
835
|
const groupId = groupEl.getAttribute('id');
|
|
832
836
|
if (!groupId)
|
|
@@ -976,33 +980,31 @@ function renderAssetInto(el, assetId, assetsManager) {
|
|
|
976
980
|
}
|
|
977
981
|
}
|
|
978
982
|
|
|
979
|
-
// Constants for selectors
|
|
980
|
-
const TOGGLE_DATA_SELECTOR = "[data-cv-toggle], [data-customviews-toggle]";
|
|
981
|
-
const TOGGLE_ELEMENT_SELECTOR = "cv-toggle";
|
|
982
|
-
const TOGGLE_SELECTOR = `${TOGGLE_DATA_SELECTOR}, ${TOGGLE_ELEMENT_SELECTOR}`;
|
|
983
983
|
/**
|
|
984
984
|
* ToggleManager handles discovery, visibility, and asset rendering for toggle elements
|
|
985
985
|
*/
|
|
986
986
|
class ToggleManager {
|
|
987
987
|
/**
|
|
988
|
-
* Apply toggle visibility to
|
|
988
|
+
* Apply toggle visibility to a given list of toggle elements
|
|
989
989
|
*/
|
|
990
|
-
static applyToggles(
|
|
991
|
-
|
|
990
|
+
static applyToggles(elements, activeToggles) {
|
|
991
|
+
elements.forEach(el => {
|
|
992
992
|
const categories = this.getToggleCategories(el);
|
|
993
993
|
const shouldShow = categories.some(cat => activeToggles.includes(cat));
|
|
994
994
|
this.applyToggleVisibility(el, shouldShow);
|
|
995
995
|
});
|
|
996
996
|
}
|
|
997
997
|
/**
|
|
998
|
-
* Render assets into toggle elements that are currently visible
|
|
998
|
+
* Render assets into a given list of toggle elements that are currently visible
|
|
999
999
|
*/
|
|
1000
|
-
static renderAssets(
|
|
1001
|
-
|
|
1000
|
+
static renderAssets(elements, activeToggles, assetsManager) {
|
|
1001
|
+
elements.forEach(el => {
|
|
1002
1002
|
const categories = this.getToggleCategories(el);
|
|
1003
1003
|
const toggleId = this.getToggleId(el);
|
|
1004
|
-
|
|
1004
|
+
const isRendered = el.dataset.cvRendered === 'true';
|
|
1005
|
+
if (toggleId && !isRendered && categories.some(cat => activeToggles.includes(cat))) {
|
|
1005
1006
|
renderAssetInto(el, toggleId, assetsManager);
|
|
1007
|
+
el.dataset.cvRendered = 'true';
|
|
1006
1008
|
}
|
|
1007
1009
|
});
|
|
1008
1010
|
}
|
|
@@ -1038,6 +1040,21 @@ class ToggleManager {
|
|
|
1038
1040
|
el.classList.remove('cv-visible');
|
|
1039
1041
|
}
|
|
1040
1042
|
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Scans a given DOM subtree for toggle elements and initializes them.
|
|
1045
|
+
* This includes applying visibility and rendering assets.
|
|
1046
|
+
*/
|
|
1047
|
+
static initializeToggles(root, activeToggles, assetsManager) {
|
|
1048
|
+
const elements = [];
|
|
1049
|
+
if (root.matches('[data-cv-toggle], [data-customviews-toggle], cv-toggle')) {
|
|
1050
|
+
elements.push(root);
|
|
1051
|
+
}
|
|
1052
|
+
root.querySelectorAll('[data-cv-toggle], [data-customviews-toggle], cv-toggle').forEach(el => elements.push(el));
|
|
1053
|
+
if (elements.length === 0)
|
|
1054
|
+
return;
|
|
1055
|
+
this.applyToggles(elements, activeToggles);
|
|
1056
|
+
this.renderAssets(elements, activeToggles, assetsManager);
|
|
1057
|
+
}
|
|
1041
1058
|
}
|
|
1042
1059
|
|
|
1043
1060
|
// src/utils/scroll-manager.ts
|
|
@@ -1361,11 +1378,18 @@ function injectCoreStyles() {
|
|
|
1361
1378
|
document.head.appendChild(style);
|
|
1362
1379
|
}
|
|
1363
1380
|
|
|
1381
|
+
const TOGGLE_SELECTOR = "[data-cv-toggle], [data-customviews-toggle], cv-toggle";
|
|
1382
|
+
const TABGROUP_SELECTOR = 'cv-tabgroup';
|
|
1364
1383
|
class CustomViewsCore {
|
|
1365
1384
|
rootEl;
|
|
1366
1385
|
assetsManager;
|
|
1367
1386
|
persistenceManager;
|
|
1368
1387
|
visibilityManager;
|
|
1388
|
+
observer = null;
|
|
1389
|
+
componentRegistry = {
|
|
1390
|
+
toggles: new Set(),
|
|
1391
|
+
tabGroups: new Set(),
|
|
1392
|
+
};
|
|
1369
1393
|
config;
|
|
1370
1394
|
stateChangeListeners = [];
|
|
1371
1395
|
showUrlEnabled;
|
|
@@ -1379,6 +1403,57 @@ class CustomViewsCore {
|
|
|
1379
1403
|
this.showUrlEnabled = opt.showUrl ?? false;
|
|
1380
1404
|
this.lastAppliedState = this.cloneState(this.getComputedDefaultState());
|
|
1381
1405
|
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Scan the given element for toggles and tab groups, register them
|
|
1408
|
+
* Returns true if new components were found
|
|
1409
|
+
*/
|
|
1410
|
+
scan(element) {
|
|
1411
|
+
let newComponentsFound = false;
|
|
1412
|
+
// Scan for toggles
|
|
1413
|
+
const toggles = Array.from(element.querySelectorAll(TOGGLE_SELECTOR));
|
|
1414
|
+
if (element.matches(TOGGLE_SELECTOR)) {
|
|
1415
|
+
toggles.unshift(element);
|
|
1416
|
+
}
|
|
1417
|
+
toggles.forEach((toggle) => {
|
|
1418
|
+
if (!this.componentRegistry.toggles.has(toggle)) {
|
|
1419
|
+
this.componentRegistry.toggles.add(toggle);
|
|
1420
|
+
newComponentsFound = true;
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1423
|
+
// Scan for tab groups
|
|
1424
|
+
const tabGroups = Array.from(element.querySelectorAll(TABGROUP_SELECTOR));
|
|
1425
|
+
if (element.matches(TABGROUP_SELECTOR)) {
|
|
1426
|
+
tabGroups.unshift(element);
|
|
1427
|
+
}
|
|
1428
|
+
tabGroups.forEach((tabGroup) => {
|
|
1429
|
+
if (!this.componentRegistry.tabGroups.has(tabGroup)) {
|
|
1430
|
+
this.componentRegistry.tabGroups.add(tabGroup);
|
|
1431
|
+
newComponentsFound = true;
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
1434
|
+
return newComponentsFound;
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Unscan the given element for toggles and tab groups, de-register them
|
|
1438
|
+
*/
|
|
1439
|
+
unscan(element) {
|
|
1440
|
+
// Unscan for toggles
|
|
1441
|
+
const toggles = Array.from(element.querySelectorAll(TOGGLE_SELECTOR));
|
|
1442
|
+
if (element.matches(TOGGLE_SELECTOR)) {
|
|
1443
|
+
toggles.unshift(element);
|
|
1444
|
+
}
|
|
1445
|
+
toggles.forEach((toggle) => {
|
|
1446
|
+
this.componentRegistry.toggles.delete(toggle);
|
|
1447
|
+
});
|
|
1448
|
+
// Unscan for tab groups
|
|
1449
|
+
const tabGroups = Array.from(element.querySelectorAll(TABGROUP_SELECTOR));
|
|
1450
|
+
if (element.matches(TABGROUP_SELECTOR)) {
|
|
1451
|
+
tabGroups.unshift(element);
|
|
1452
|
+
}
|
|
1453
|
+
tabGroups.forEach((tabGroup) => {
|
|
1454
|
+
this.componentRegistry.tabGroups.delete(tabGroup);
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1382
1457
|
getConfig() {
|
|
1383
1458
|
return this.config;
|
|
1384
1459
|
}
|
|
@@ -1413,7 +1488,7 @@ class CustomViewsCore {
|
|
|
1413
1488
|
});
|
|
1414
1489
|
}
|
|
1415
1490
|
const computedState = {
|
|
1416
|
-
toggles:
|
|
1491
|
+
toggles: this.config.toggles?.map(t => t.id) || [],
|
|
1417
1492
|
tabs
|
|
1418
1493
|
};
|
|
1419
1494
|
return computedState;
|
|
@@ -1452,8 +1527,25 @@ class CustomViewsCore {
|
|
|
1452
1527
|
// Inject styles, setup listeners and call rendering logic
|
|
1453
1528
|
async init() {
|
|
1454
1529
|
injectCoreStyles();
|
|
1455
|
-
|
|
1456
|
-
|
|
1530
|
+
this.scan(this.rootEl);
|
|
1531
|
+
// Initialize all components found on initial scan
|
|
1532
|
+
this.initializeNewComponents();
|
|
1533
|
+
// Apply stored nav visibility preference on page load
|
|
1534
|
+
const navPref = this.persistenceManager.getPersistedTabNavVisibility();
|
|
1535
|
+
if (navPref !== null) {
|
|
1536
|
+
TabManager.setNavsVisibility(this.rootEl, navPref);
|
|
1537
|
+
}
|
|
1538
|
+
// For session history, clicks on back/forward button
|
|
1539
|
+
window.addEventListener("popstate", () => {
|
|
1540
|
+
this.loadAndCallApplyState();
|
|
1541
|
+
});
|
|
1542
|
+
this.loadAndCallApplyState();
|
|
1543
|
+
this.initObserver();
|
|
1544
|
+
}
|
|
1545
|
+
initializeNewComponents() {
|
|
1546
|
+
// Build navigation for any newly added tab groups.
|
|
1547
|
+
// The `data-cv-initialized` attribute in `buildNavs` prevents re-initialization.
|
|
1548
|
+
TabManager.buildNavs(Array.from(this.componentRegistry.tabGroups), this.config.tabGroups,
|
|
1457
1549
|
// Single click: update clicked group only (local, no persistence)
|
|
1458
1550
|
(groupId, tabId, groupEl) => {
|
|
1459
1551
|
this.setActiveTab(groupId, tabId, groupEl);
|
|
@@ -1476,16 +1568,46 @@ class CustomViewsCore {
|
|
|
1476
1568
|
scrollAnchor: { element: anchorElement, top: initialTop }
|
|
1477
1569
|
});
|
|
1478
1570
|
});
|
|
1479
|
-
//
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1571
|
+
// Future components (e.g., toggles, widgets) can be initialized here
|
|
1572
|
+
}
|
|
1573
|
+
initObserver() {
|
|
1574
|
+
this.observer = new MutationObserver((mutations) => {
|
|
1575
|
+
let newComponentsFound = false;
|
|
1576
|
+
for (const mutation of mutations) {
|
|
1577
|
+
if (mutation.type === 'childList') {
|
|
1578
|
+
mutation.addedNodes.forEach((node) => {
|
|
1579
|
+
if (node instanceof Element) {
|
|
1580
|
+
// Scan the new node for components and add them to the registry
|
|
1581
|
+
if (this.scan(node)) {
|
|
1582
|
+
newComponentsFound = true;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
mutation.removedNodes.forEach((node) => {
|
|
1587
|
+
if (node instanceof Element) {
|
|
1588
|
+
// Unscan the removed node to cleanup the registry
|
|
1589
|
+
this.unscan(node);
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
if (newComponentsFound) {
|
|
1595
|
+
// Initialize navs for new components.
|
|
1596
|
+
this.initializeNewComponents();
|
|
1597
|
+
// Re-apply the last known state. renderState will handle disconnecting
|
|
1598
|
+
// the observer to prevent infinite loops.
|
|
1599
|
+
if (this.lastAppliedState) {
|
|
1600
|
+
this.renderState(this.lastAppliedState);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1487
1603
|
});
|
|
1488
|
-
|
|
1604
|
+
// Observe only the root element to avoid performance issues on large pages.
|
|
1605
|
+
if (this.rootEl) {
|
|
1606
|
+
this.observer.observe(this.rootEl, {
|
|
1607
|
+
childList: true,
|
|
1608
|
+
subtree: true,
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1489
1611
|
}
|
|
1490
1612
|
// Priority: URL state > persisted state > config default > computed default
|
|
1491
1613
|
// Also filters using the visibility manager to persist selection
|
|
@@ -1539,23 +1661,34 @@ class CustomViewsCore {
|
|
|
1539
1661
|
ScrollManager.handleScrollAnchor(options.scrollAnchor);
|
|
1540
1662
|
}
|
|
1541
1663
|
}
|
|
1542
|
-
/**
|
|
1664
|
+
/**
|
|
1665
|
+
* Renders state on components in ComponentRegistry
|
|
1666
|
+
* Applies the given state.
|
|
1667
|
+
* Disconnects the mutation observer during rendering to prevent loops
|
|
1668
|
+
**/
|
|
1543
1669
|
renderState(state) {
|
|
1670
|
+
this.observer?.disconnect();
|
|
1544
1671
|
this.lastAppliedState = this.cloneState(state);
|
|
1545
1672
|
const toggles = state?.toggles || [];
|
|
1546
1673
|
const finalToggles = this.visibilityManager.filterVisibleToggles(toggles);
|
|
1674
|
+
const toggleElements = Array.from(this.componentRegistry.toggles);
|
|
1675
|
+
const tabGroupElements = Array.from(this.componentRegistry.tabGroups);
|
|
1547
1676
|
// Apply toggle visibility
|
|
1548
|
-
ToggleManager.applyToggles(
|
|
1677
|
+
ToggleManager.applyToggles(toggleElements, finalToggles);
|
|
1549
1678
|
// Render assets into toggles
|
|
1550
|
-
ToggleManager.renderAssets(
|
|
1679
|
+
ToggleManager.renderAssets(toggleElements, finalToggles, this.assetsManager);
|
|
1551
1680
|
// Apply tab selections
|
|
1552
|
-
TabManager.applyTabSelections(
|
|
1681
|
+
TabManager.applyTabSelections(tabGroupElements, state.tabs || {}, this.config.tabGroups);
|
|
1553
1682
|
// Update nav active states (without rebuilding)
|
|
1554
|
-
TabManager.updateAllNavActiveStates(
|
|
1683
|
+
TabManager.updateAllNavActiveStates(tabGroupElements, state.tabs || {}, this.config.tabGroups);
|
|
1555
1684
|
// Update pin icons to show which tabs are persisted
|
|
1556
|
-
TabManager.updatePinIcons(
|
|
1685
|
+
TabManager.updatePinIcons(tabGroupElements, state.tabs || {});
|
|
1557
1686
|
// Notify state change listeners (like widgets)
|
|
1558
1687
|
this.notifyStateChangeListeners();
|
|
1688
|
+
this.observer?.observe(document.body, {
|
|
1689
|
+
childList: true,
|
|
1690
|
+
subtree: true,
|
|
1691
|
+
});
|
|
1559
1692
|
}
|
|
1560
1693
|
/**
|
|
1561
1694
|
* Reset to default state
|
|
@@ -1807,7 +1940,7 @@ class CustomViews {
|
|
|
1807
1940
|
else {
|
|
1808
1941
|
console.error("No config provided, using minimal default config");
|
|
1809
1942
|
// Create a minimal default config
|
|
1810
|
-
config = {
|
|
1943
|
+
config = { toggles: [], defaultState: {} };
|
|
1811
1944
|
}
|
|
1812
1945
|
const coreOptions = {
|
|
1813
1946
|
assetsManager,
|
|
@@ -2064,6 +2197,10 @@ const WIDGET_STYLES = `
|
|
|
2064
2197
|
animation: fadeIn 0.2s ease;
|
|
2065
2198
|
}
|
|
2066
2199
|
|
|
2200
|
+
.cv-widget-modal-overlay.cv-hidden {
|
|
2201
|
+
display: none;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2067
2204
|
@keyframes fadeIn {
|
|
2068
2205
|
from { opacity: 0; }
|
|
2069
2206
|
to { opacity: 1; }
|
|
@@ -2920,8 +3057,11 @@ class CustomViewsWidget {
|
|
|
2920
3057
|
container;
|
|
2921
3058
|
widgetIcon = null;
|
|
2922
3059
|
options;
|
|
3060
|
+
_hasVisibleConfig = false;
|
|
3061
|
+
pageToggleIds = new Set();
|
|
3062
|
+
pageTabIds = new Set();
|
|
2923
3063
|
// Modal state
|
|
2924
|
-
|
|
3064
|
+
stateModal = null;
|
|
2925
3065
|
constructor(options) {
|
|
2926
3066
|
this.core = options.core;
|
|
2927
3067
|
this.container = options.container || document.body;
|
|
@@ -2939,12 +3079,41 @@ class CustomViewsWidget {
|
|
|
2939
3079
|
welcomeMessage: options.welcomeMessage || 'This site is powered by Custom Views. Use the widget on the side (⚙) to customize your experience. Your preferences will be saved and can be shared via URL.<br><br>Learn more at <a href="https://github.com/customviews-js/customviews" target="_blank">customviews GitHub</a>.',
|
|
2940
3080
|
showTabGroups: options.showTabGroups ?? true
|
|
2941
3081
|
};
|
|
2942
|
-
//
|
|
3082
|
+
// Determine if there are any configurations to show
|
|
3083
|
+
const config = this.core.getConfig();
|
|
3084
|
+
const allToggles = config?.toggles || [];
|
|
3085
|
+
const visibleToggles = allToggles.filter(toggle => {
|
|
3086
|
+
if (toggle.isLocal) {
|
|
3087
|
+
return !!document.querySelector(`[data-cv-toggle="${toggle.id}"], [data-cv-toggle-group-id="${toggle.id}"]`);
|
|
3088
|
+
}
|
|
3089
|
+
return true;
|
|
3090
|
+
});
|
|
3091
|
+
const allTabGroups = this.core.getTabGroups() || [];
|
|
3092
|
+
const visibleTabGroups = allTabGroups.filter(group => {
|
|
3093
|
+
if (group.isLocal) {
|
|
3094
|
+
return !!document.querySelector(`cv-tabgroup[id="${group.id}"]`);
|
|
3095
|
+
}
|
|
3096
|
+
return true;
|
|
3097
|
+
});
|
|
3098
|
+
if (visibleToggles.length > 0 || (this.options.showTabGroups && visibleTabGroups.length > 0)) {
|
|
3099
|
+
this._hasVisibleConfig = true;
|
|
3100
|
+
}
|
|
3101
|
+
// Scan for page-declared local components and cache them
|
|
3102
|
+
// Do this on initialization to avoid querying DOM repeatedly
|
|
3103
|
+
const pageTogglesAttr = document.querySelector('[data-cv-page-local-toggles]')?.getAttribute('data-cv-page-local-toggles') || '';
|
|
3104
|
+
this.pageToggleIds = new Set(pageTogglesAttr.split(',').map(id => id.trim()).filter(id => id));
|
|
3105
|
+
const pageTabsAttr = document.querySelector('[data-cv-page-local-tabs]')?.getAttribute('data-cv-page-local-tabs') || '';
|
|
3106
|
+
this.pageTabIds = new Set(pageTabsAttr.split(',').map(id => id.trim()).filter(id => id));
|
|
2943
3107
|
}
|
|
2944
3108
|
/**
|
|
2945
|
-
* Render the widget
|
|
3109
|
+
* Render the widget modal icon
|
|
3110
|
+
*
|
|
3111
|
+
* Does not render if there are no visible toggles or tab groups.
|
|
2946
3112
|
*/
|
|
2947
|
-
|
|
3113
|
+
renderModalIcon() {
|
|
3114
|
+
if (!this._hasVisibleConfig) {
|
|
3115
|
+
return;
|
|
3116
|
+
}
|
|
2948
3117
|
this.widgetIcon = this.createWidgetIcon();
|
|
2949
3118
|
this.attachEventListeners();
|
|
2950
3119
|
// Always append to body since it's a floating icon
|
|
@@ -2977,9 +3146,9 @@ class CustomViewsWidget {
|
|
|
2977
3146
|
this.widgetIcon = null;
|
|
2978
3147
|
}
|
|
2979
3148
|
// Clean up modal
|
|
2980
|
-
if (this.
|
|
2981
|
-
this.
|
|
2982
|
-
this.
|
|
3149
|
+
if (this.stateModal) {
|
|
3150
|
+
this.stateModal.remove();
|
|
3151
|
+
this.stateModal = null;
|
|
2983
3152
|
}
|
|
2984
3153
|
}
|
|
2985
3154
|
attachEventListeners() {
|
|
@@ -2992,49 +3161,71 @@ class CustomViewsWidget {
|
|
|
2992
3161
|
* Close the modal
|
|
2993
3162
|
*/
|
|
2994
3163
|
closeModal() {
|
|
2995
|
-
if (this.
|
|
2996
|
-
this.
|
|
2997
|
-
this.modal = null;
|
|
3164
|
+
if (this.stateModal) {
|
|
3165
|
+
this.stateModal.classList.add('cv-hidden');
|
|
2998
3166
|
}
|
|
2999
3167
|
}
|
|
3000
3168
|
/**
|
|
3001
3169
|
* Open the custom state creator
|
|
3002
3170
|
*/
|
|
3003
3171
|
openStateModal() {
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
this.
|
|
3172
|
+
if (!this.stateModal) {
|
|
3173
|
+
this._createStateModal();
|
|
3174
|
+
}
|
|
3175
|
+
this._updateStateModalContent();
|
|
3176
|
+
this.stateModal.classList.remove('cv-hidden');
|
|
3008
3177
|
}
|
|
3009
3178
|
/**
|
|
3010
|
-
* Create the custom state creator modal
|
|
3179
|
+
* Create the custom state creator modal shell and attach listeners
|
|
3011
3180
|
*/
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
this.
|
|
3015
|
-
this.modal = document.createElement('div');
|
|
3016
|
-
this.modal.className = 'cv-widget-modal-overlay';
|
|
3181
|
+
_createStateModal() {
|
|
3182
|
+
this.stateModal = document.createElement('div');
|
|
3183
|
+
this.stateModal.className = 'cv-widget-modal-overlay cv-hidden';
|
|
3017
3184
|
this.applyThemeToModal();
|
|
3018
|
-
|
|
3185
|
+
document.body.appendChild(this.stateModal);
|
|
3186
|
+
this._attachStateModalFrameEventListeners();
|
|
3187
|
+
}
|
|
3188
|
+
/**
|
|
3189
|
+
* Update the content of the state modal
|
|
3190
|
+
*/
|
|
3191
|
+
_updateStateModalContent() {
|
|
3192
|
+
if (!this.stateModal)
|
|
3193
|
+
return;
|
|
3194
|
+
const pageToggleIds = this.pageToggleIds;
|
|
3195
|
+
const pageTabIds = this.pageTabIds;
|
|
3196
|
+
// Get toggles from current configuration
|
|
3197
|
+
const config = this.core.getConfig();
|
|
3198
|
+
const allToggles = config?.toggles || [];
|
|
3199
|
+
// Filter toggles to only include global and visible/declared local toggles
|
|
3200
|
+
const visibleToggles = allToggles.filter(toggle => {
|
|
3201
|
+
if (toggle.isLocal) {
|
|
3202
|
+
return pageToggleIds.has(toggle.id) || !!document.querySelector(`[data-cv-toggle="${toggle.id}"], [data-cv-toggle-group-id="${toggle.id}"]`);
|
|
3203
|
+
}
|
|
3204
|
+
return true; // Keep global toggles
|
|
3205
|
+
});
|
|
3206
|
+
const toggleControlsHtml = visibleToggles.map(toggle => `
|
|
3019
3207
|
<div class="cv-toggle-card">
|
|
3020
3208
|
<div class="cv-toggle-content">
|
|
3021
3209
|
<div>
|
|
3022
|
-
<p class="cv-toggle-title">${
|
|
3210
|
+
<p class="cv-toggle-title">${toggle.label || toggle.id}</p>
|
|
3023
3211
|
</div>
|
|
3024
3212
|
<label class="cv-toggle-label">
|
|
3025
|
-
<input class="cv-toggle-input" type="checkbox" data-toggle="${toggle}"/>
|
|
3213
|
+
<input class="cv-toggle-input" type="checkbox" data-toggle="${toggle.id}"/>
|
|
3026
3214
|
<span class="cv-toggle-slider"></span>
|
|
3027
3215
|
</label>
|
|
3028
3216
|
</div>
|
|
3029
3217
|
</div>
|
|
3030
3218
|
`).join('');
|
|
3031
|
-
// Todo: Re-add description if needed (Line 168, add label field to toggles if needed change structure)
|
|
3032
|
-
// <p class="cv-toggle-description">Show or hide the ${this.formatToggleName(toggle).toLowerCase()} area </p>
|
|
3033
3219
|
// Get tab groups
|
|
3034
|
-
const
|
|
3220
|
+
const allTabGroups = this.core.getTabGroups() || [];
|
|
3221
|
+
const tabGroups = allTabGroups.filter(group => {
|
|
3222
|
+
if (group.isLocal) {
|
|
3223
|
+
return pageTabIds.has(group.id) || !!document.querySelector(`cv-tabgroup[id="${group.id}"]`);
|
|
3224
|
+
}
|
|
3225
|
+
return true; // Keep global tab groups
|
|
3226
|
+
});
|
|
3035
3227
|
let tabGroupControlsHTML = '';
|
|
3036
3228
|
if (this.options.showTabGroups && tabGroups && tabGroups.length > 0) {
|
|
3037
|
-
// Determine initial nav visibility state
|
|
3038
3229
|
const initialNavsVisible = TabManager.areNavsVisible(document.body);
|
|
3039
3230
|
tabGroupControlsHTML = `
|
|
3040
3231
|
<div class="cv-tabgroup-card cv-tabgroup-header">
|
|
@@ -3069,7 +3260,7 @@ class CustomViewsWidget {
|
|
|
3069
3260
|
</div>
|
|
3070
3261
|
`;
|
|
3071
3262
|
}
|
|
3072
|
-
this.
|
|
3263
|
+
this.stateModal.innerHTML = `
|
|
3073
3264
|
<div class="cv-widget-modal cv-custom-state-modal">
|
|
3074
3265
|
<header class="cv-modal-header">
|
|
3075
3266
|
<div class="cv-modal-header-content">
|
|
@@ -3085,7 +3276,7 @@ class CustomViewsWidget {
|
|
|
3085
3276
|
<main class="cv-modal-main">
|
|
3086
3277
|
${this.options.description ? `<p class="cv-modal-description">${this.options.description}</p>` : ''}
|
|
3087
3278
|
|
|
3088
|
-
${
|
|
3279
|
+
${visibleToggles.length ? `
|
|
3089
3280
|
<div class="cv-content-section">
|
|
3090
3281
|
<div class="cv-section-heading">Toggles</div>
|
|
3091
3282
|
<div class="cv-toggles-container">
|
|
@@ -3118,62 +3309,76 @@ class CustomViewsWidget {
|
|
|
3118
3309
|
</footer>
|
|
3119
3310
|
</div>
|
|
3120
3311
|
`;
|
|
3121
|
-
|
|
3122
|
-
this.attachStateModalEventListeners();
|
|
3123
|
-
// Load current state into form if we're already in a custom state
|
|
3312
|
+
this._attachStateModalContentEventListeners();
|
|
3124
3313
|
this.loadCurrentStateIntoForm();
|
|
3125
3314
|
}
|
|
3126
3315
|
/**
|
|
3127
|
-
* Attach event listeners for
|
|
3316
|
+
* Attach event listeners for the modal frame (delegated events)
|
|
3128
3317
|
*/
|
|
3129
|
-
|
|
3130
|
-
if (!this.
|
|
3318
|
+
_attachStateModalFrameEventListeners() {
|
|
3319
|
+
if (!this.stateModal)
|
|
3131
3320
|
return;
|
|
3132
|
-
//
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3321
|
+
// Delegated click events
|
|
3322
|
+
this.stateModal.addEventListener('click', (e) => {
|
|
3323
|
+
const target = e.target;
|
|
3324
|
+
// Close button
|
|
3325
|
+
if (target.closest('.cv-modal-close')) {
|
|
3136
3326
|
this.closeModal();
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
if (copyUrlBtn) {
|
|
3142
|
-
copyUrlBtn.addEventListener('click', () => {
|
|
3327
|
+
return;
|
|
3328
|
+
}
|
|
3329
|
+
// Copy URL button
|
|
3330
|
+
if (target.closest('.cv-share-btn')) {
|
|
3143
3331
|
this.copyShareableURL();
|
|
3144
|
-
|
|
3145
|
-
const iconContainer = copyUrlBtn
|
|
3332
|
+
const copyUrlBtn = target.closest('.cv-share-btn');
|
|
3333
|
+
const iconContainer = copyUrlBtn?.querySelector('.cv-share-btn-icon');
|
|
3146
3334
|
if (iconContainer) {
|
|
3147
3335
|
const originalIcon = iconContainer.innerHTML;
|
|
3148
3336
|
iconContainer.innerHTML = getTickIcon();
|
|
3149
|
-
// Revert after 3 seconds
|
|
3150
3337
|
setTimeout(() => {
|
|
3151
3338
|
iconContainer.innerHTML = originalIcon;
|
|
3152
3339
|
}, 3000);
|
|
3153
3340
|
}
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
// Add spinning animation to icon
|
|
3161
|
-
const resetIcon = resetBtn.querySelector('.cv-reset-btn-icon');
|
|
3341
|
+
return;
|
|
3342
|
+
}
|
|
3343
|
+
// Reset to default button
|
|
3344
|
+
if (target.closest('.cv-reset-btn')) {
|
|
3345
|
+
const resetBtn = target.closest('.cv-reset-btn');
|
|
3346
|
+
const resetIcon = resetBtn?.querySelector('.cv-reset-btn-icon');
|
|
3162
3347
|
if (resetIcon) {
|
|
3163
3348
|
resetIcon.classList.add('cv-spinning');
|
|
3164
3349
|
}
|
|
3165
3350
|
this.core.resetToDefault();
|
|
3166
3351
|
this.loadCurrentStateIntoForm();
|
|
3167
|
-
// Remove spinning animation after it completes
|
|
3168
3352
|
setTimeout(() => {
|
|
3169
3353
|
if (resetIcon) {
|
|
3170
3354
|
resetIcon.classList.remove('cv-spinning');
|
|
3171
3355
|
}
|
|
3172
|
-
}, 600);
|
|
3173
|
-
|
|
3174
|
-
|
|
3356
|
+
}, 600);
|
|
3357
|
+
return;
|
|
3358
|
+
}
|
|
3359
|
+
// Overlay click to close
|
|
3360
|
+
if (e.target === this.stateModal) {
|
|
3361
|
+
this.closeModal();
|
|
3362
|
+
}
|
|
3363
|
+
});
|
|
3364
|
+
// Escape key to close
|
|
3365
|
+
const handleEscape = (e) => {
|
|
3366
|
+
if (e.key === 'Escape') {
|
|
3367
|
+
this.closeModal();
|
|
3368
|
+
}
|
|
3369
|
+
};
|
|
3370
|
+
// We can't remove this listener easily if it's anonymous, so we attach it to the document
|
|
3371
|
+
// and it will stay for the lifetime of the widget. This is acceptable.
|
|
3372
|
+
document.addEventListener('keydown', handleEscape);
|
|
3373
|
+
}
|
|
3374
|
+
/**
|
|
3375
|
+
* Attach event listeners for custom state creator's dynamic content
|
|
3376
|
+
*/
|
|
3377
|
+
_attachStateModalContentEventListeners() {
|
|
3378
|
+
if (!this.stateModal)
|
|
3379
|
+
return;
|
|
3175
3380
|
// Listen to toggle switches
|
|
3176
|
-
const toggleInputs = this.
|
|
3381
|
+
const toggleInputs = this.stateModal.querySelectorAll('.cv-toggle-input');
|
|
3177
3382
|
toggleInputs.forEach(toggleInput => {
|
|
3178
3383
|
toggleInput.addEventListener('change', () => {
|
|
3179
3384
|
const state = this.getCurrentCustomStateFromModal();
|
|
@@ -3181,17 +3386,14 @@ class CustomViewsWidget {
|
|
|
3181
3386
|
});
|
|
3182
3387
|
});
|
|
3183
3388
|
// Listen to tab group selects
|
|
3184
|
-
const tabGroupSelects = this.
|
|
3389
|
+
const tabGroupSelects = this.stateModal.querySelectorAll('.cv-tabgroup-select');
|
|
3185
3390
|
tabGroupSelects.forEach(select => {
|
|
3186
3391
|
select.addEventListener('change', () => {
|
|
3187
3392
|
const groupId = select.dataset.groupId;
|
|
3188
3393
|
const tabId = select.value;
|
|
3189
3394
|
if (groupId && tabId) {
|
|
3190
|
-
// Get current state and update the tab for this group, then apply globally
|
|
3191
|
-
// This triggers sync behavior and persistence
|
|
3192
3395
|
const currentTabs = this.core.getCurrentActiveTabs();
|
|
3193
3396
|
currentTabs[groupId] = tabId;
|
|
3194
|
-
// Apply state globally for persistence and sync
|
|
3195
3397
|
const currentToggles = this.core.getCurrentActiveToggles();
|
|
3196
3398
|
const newState = {
|
|
3197
3399
|
toggles: currentToggles,
|
|
@@ -3202,84 +3404,56 @@ class CustomViewsWidget {
|
|
|
3202
3404
|
});
|
|
3203
3405
|
});
|
|
3204
3406
|
// Listener for show/hide tab navs
|
|
3205
|
-
const tabNavToggle = this.
|
|
3206
|
-
const navIcon = this.
|
|
3207
|
-
const navHeaderCard = this.
|
|
3407
|
+
const tabNavToggle = this.stateModal.querySelector('.cv-nav-pref-input');
|
|
3408
|
+
const navIcon = this.stateModal?.querySelector('#cv-nav-icon');
|
|
3409
|
+
const navHeaderCard = this.stateModal?.querySelector('.cv-tabgroup-card.cv-tabgroup-header');
|
|
3208
3410
|
if (tabNavToggle && navIcon && navHeaderCard) {
|
|
3209
|
-
// Helper to update icon based on state
|
|
3210
3411
|
const updateIcon = (isVisible, isHovering = false) => {
|
|
3211
3412
|
if (isHovering) {
|
|
3212
|
-
// On hover, show the transition icon
|
|
3213
3413
|
navIcon.innerHTML = getNavDashed();
|
|
3214
3414
|
}
|
|
3215
3415
|
else {
|
|
3216
|
-
// Normal state, show the status icon (on if visible, off if hidden)
|
|
3217
3416
|
navIcon.innerHTML = isVisible ? getNavHeadingOnIcon() : getNavHeadingOffIcon();
|
|
3218
3417
|
}
|
|
3219
3418
|
};
|
|
3220
|
-
|
|
3221
|
-
navHeaderCard.addEventListener('
|
|
3222
|
-
updateIcon(tabNavToggle.checked, true);
|
|
3223
|
-
});
|
|
3224
|
-
navHeaderCard.addEventListener('mouseleave', () => {
|
|
3225
|
-
updateIcon(tabNavToggle.checked, false);
|
|
3226
|
-
});
|
|
3227
|
-
// Add change listener
|
|
3419
|
+
navHeaderCard.addEventListener('mouseenter', () => updateIcon(tabNavToggle.checked, true));
|
|
3420
|
+
navHeaderCard.addEventListener('mouseleave', () => updateIcon(tabNavToggle.checked, false));
|
|
3228
3421
|
tabNavToggle.addEventListener('change', () => {
|
|
3229
3422
|
const visible = tabNavToggle.checked;
|
|
3230
|
-
// Update the icon based on new state (not hovering)
|
|
3231
3423
|
updateIcon(visible, false);
|
|
3232
|
-
// Persist preference via core
|
|
3233
3424
|
this.core.persistTabNavVisibility(visible);
|
|
3234
|
-
// Apply to DOM using TabManager via core
|
|
3235
3425
|
try {
|
|
3236
|
-
|
|
3237
|
-
TabManager.setNavsVisibility(rootEl, visible);
|
|
3426
|
+
TabManager.setNavsVisibility(document.body, visible);
|
|
3238
3427
|
}
|
|
3239
3428
|
catch (e) {
|
|
3240
|
-
// ignore errors
|
|
3241
3429
|
console.error('Failed to set tab nav visibility:', e);
|
|
3242
3430
|
}
|
|
3243
3431
|
});
|
|
3244
3432
|
}
|
|
3245
|
-
// Overlay click to close
|
|
3246
|
-
this.modal.addEventListener('click', (e) => {
|
|
3247
|
-
if (e.target === this.modal) {
|
|
3248
|
-
this.closeModal();
|
|
3249
|
-
}
|
|
3250
|
-
});
|
|
3251
|
-
// Escape key to close
|
|
3252
|
-
const handleEscape = (e) => {
|
|
3253
|
-
if (e.key === 'Escape') {
|
|
3254
|
-
this.closeModal();
|
|
3255
|
-
document.removeEventListener('keydown', handleEscape);
|
|
3256
|
-
}
|
|
3257
|
-
};
|
|
3258
|
-
document.addEventListener('keydown', handleEscape);
|
|
3259
3433
|
}
|
|
3260
3434
|
/**
|
|
3261
3435
|
* Apply theme class to the modal overlay based on options
|
|
3262
3436
|
*/
|
|
3263
3437
|
applyThemeToModal() {
|
|
3264
|
-
if (!this.
|
|
3438
|
+
if (!this.stateModal)
|
|
3265
3439
|
return;
|
|
3266
3440
|
if (this.options.theme === 'dark') {
|
|
3267
|
-
this.
|
|
3441
|
+
this.stateModal.classList.add('cv-widget-theme-dark');
|
|
3268
3442
|
}
|
|
3269
3443
|
else {
|
|
3270
|
-
this.
|
|
3444
|
+
this.stateModal.classList.remove('cv-widget-theme-dark');
|
|
3271
3445
|
}
|
|
3272
3446
|
}
|
|
3273
3447
|
/**
|
|
3274
3448
|
* Get current state from form values
|
|
3275
3449
|
*/
|
|
3276
3450
|
getCurrentCustomStateFromModal() {
|
|
3277
|
-
if (!this.
|
|
3451
|
+
if (!this.stateModal) {
|
|
3278
3452
|
return {};
|
|
3279
3453
|
}
|
|
3280
3454
|
// Collect toggle values
|
|
3281
3455
|
const toggles = [];
|
|
3282
|
-
const toggleInputs = this.
|
|
3456
|
+
const toggleInputs = this.stateModal.querySelectorAll('.cv-toggle-input');
|
|
3283
3457
|
toggleInputs.forEach(toggleInput => {
|
|
3284
3458
|
const toggle = toggleInput.dataset.toggle;
|
|
3285
3459
|
if (toggle && toggleInput.checked) {
|
|
@@ -3287,7 +3461,7 @@ class CustomViewsWidget {
|
|
|
3287
3461
|
}
|
|
3288
3462
|
});
|
|
3289
3463
|
// Collect tab selections
|
|
3290
|
-
const tabGroupSelects = this.
|
|
3464
|
+
const tabGroupSelects = this.stateModal.querySelectorAll('.cv-tabgroup-select');
|
|
3291
3465
|
const tabs = {};
|
|
3292
3466
|
tabGroupSelects.forEach(select => {
|
|
3293
3467
|
const groupId = select.dataset.groupId;
|
|
@@ -3315,25 +3489,25 @@ class CustomViewsWidget {
|
|
|
3315
3489
|
* Load current state into form based on currently active toggles
|
|
3316
3490
|
*/
|
|
3317
3491
|
loadCurrentStateIntoForm() {
|
|
3318
|
-
if (!this.
|
|
3492
|
+
if (!this.stateModal)
|
|
3319
3493
|
return;
|
|
3320
3494
|
// Get currently active toggles (from custom state or default configuration)
|
|
3321
3495
|
const activeToggles = this.core.getCurrentActiveToggles();
|
|
3322
3496
|
// First, uncheck all toggle inputs
|
|
3323
|
-
const allToggleInputs = this.
|
|
3497
|
+
const allToggleInputs = this.stateModal.querySelectorAll('.cv-toggle-input');
|
|
3324
3498
|
allToggleInputs.forEach(toggleInput => {
|
|
3325
3499
|
toggleInput.checked = false;
|
|
3326
3500
|
});
|
|
3327
3501
|
// Then check the ones that should be active
|
|
3328
3502
|
activeToggles.forEach(toggle => {
|
|
3329
|
-
const toggleInput = this.
|
|
3503
|
+
const toggleInput = this.stateModal?.querySelector(`[data-toggle="${toggle}"]`);
|
|
3330
3504
|
if (toggleInput) {
|
|
3331
3505
|
toggleInput.checked = true;
|
|
3332
3506
|
}
|
|
3333
3507
|
});
|
|
3334
3508
|
// Load tab group selections
|
|
3335
3509
|
const activeTabs = this.core.getCurrentActiveTabs();
|
|
3336
|
-
const tabGroupSelects = this.
|
|
3510
|
+
const tabGroupSelects = this.stateModal.querySelectorAll('.cv-tabgroup-select');
|
|
3337
3511
|
tabGroupSelects.forEach(select => {
|
|
3338
3512
|
const groupId = select.dataset.groupId;
|
|
3339
3513
|
if (groupId && activeTabs[groupId]) {
|
|
@@ -3348,8 +3522,8 @@ class CustomViewsWidget {
|
|
|
3348
3522
|
}
|
|
3349
3523
|
return TabManager.areNavsVisible(document.body);
|
|
3350
3524
|
})();
|
|
3351
|
-
const tabNavToggle = this.
|
|
3352
|
-
const navIcon = this.
|
|
3525
|
+
const tabNavToggle = this.stateModal.querySelector('.cv-nav-pref-input');
|
|
3526
|
+
const navIcon = this.stateModal?.querySelector('#cv-nav-icon');
|
|
3353
3527
|
if (tabNavToggle) {
|
|
3354
3528
|
tabNavToggle.checked = navPref;
|
|
3355
3529
|
// Ensure UI matches actual visibility
|
|
@@ -3360,33 +3534,22 @@ class CustomViewsWidget {
|
|
|
3360
3534
|
}
|
|
3361
3535
|
}
|
|
3362
3536
|
}
|
|
3363
|
-
/**
|
|
3364
|
-
* Format toggle name for display
|
|
3365
|
-
*/
|
|
3366
|
-
formatToggleName(toggle) {
|
|
3367
|
-
return toggle.charAt(0).toUpperCase() + toggle.slice(1);
|
|
3368
|
-
}
|
|
3369
3537
|
/**
|
|
3370
3538
|
* Check if this is the first visit and show welcome modal
|
|
3371
3539
|
*/
|
|
3372
3540
|
showWelcomeModalIfFirstVisit() {
|
|
3541
|
+
if (!this._hasVisibleConfig)
|
|
3542
|
+
return;
|
|
3373
3543
|
const STORAGE_KEY = 'cv-welcome-shown';
|
|
3374
3544
|
// Check if welcome has been shown before
|
|
3375
3545
|
const hasSeenWelcome = localStorage.getItem(STORAGE_KEY);
|
|
3376
3546
|
if (!hasSeenWelcome) {
|
|
3377
|
-
//
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
// Show welcome modal after a short delay to let the page settle
|
|
3384
|
-
setTimeout(() => {
|
|
3385
|
-
this.createWelcomeModal();
|
|
3386
|
-
}, 500);
|
|
3387
|
-
// Mark as shown
|
|
3388
|
-
localStorage.setItem(STORAGE_KEY, 'true');
|
|
3389
|
-
}
|
|
3547
|
+
// Show welcome modal after a short delay to let the page settle
|
|
3548
|
+
setTimeout(() => {
|
|
3549
|
+
this.createWelcomeModal();
|
|
3550
|
+
}, 500);
|
|
3551
|
+
// Mark as shown
|
|
3552
|
+
localStorage.setItem(STORAGE_KEY, 'true');
|
|
3390
3553
|
}
|
|
3391
3554
|
}
|
|
3392
3555
|
/**
|
|
@@ -3394,12 +3557,14 @@ class CustomViewsWidget {
|
|
|
3394
3557
|
*/
|
|
3395
3558
|
createWelcomeModal() {
|
|
3396
3559
|
// Don't show if there's already a modal open
|
|
3397
|
-
if (this.
|
|
3560
|
+
if (this.stateModal && !this.stateModal.classList.contains('cv-hidden'))
|
|
3398
3561
|
return;
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
this.
|
|
3402
|
-
|
|
3562
|
+
const welcomeModal = document.createElement('div');
|
|
3563
|
+
welcomeModal.className = 'cv-widget-modal-overlay cv-welcome-modal-overlay';
|
|
3564
|
+
if (this.options.theme === 'dark') {
|
|
3565
|
+
welcomeModal.classList.add('cv-widget-theme-dark');
|
|
3566
|
+
}
|
|
3567
|
+
welcomeModal.innerHTML = `
|
|
3403
3568
|
<div class="cv-widget-modal cv-welcome-modal">
|
|
3404
3569
|
<header class="cv-modal-header">
|
|
3405
3570
|
<div class="cv-modal-header-content">
|
|
@@ -3423,33 +3588,32 @@ class CustomViewsWidget {
|
|
|
3423
3588
|
</div>
|
|
3424
3589
|
</div>
|
|
3425
3590
|
`;
|
|
3426
|
-
document.body.appendChild(
|
|
3427
|
-
this.attachWelcomeModalEventListeners();
|
|
3591
|
+
document.body.appendChild(welcomeModal);
|
|
3592
|
+
this.attachWelcomeModalEventListeners(welcomeModal);
|
|
3428
3593
|
}
|
|
3429
3594
|
/**
|
|
3430
3595
|
* Attach event listeners for welcome modal
|
|
3431
3596
|
*/
|
|
3432
|
-
attachWelcomeModalEventListeners() {
|
|
3433
|
-
|
|
3434
|
-
|
|
3597
|
+
attachWelcomeModalEventListeners(welcomeModal) {
|
|
3598
|
+
const closeModal = () => {
|
|
3599
|
+
welcomeModal.remove();
|
|
3600
|
+
document.removeEventListener('keydown', handleEscape);
|
|
3601
|
+
};
|
|
3435
3602
|
// Got it button
|
|
3436
|
-
const gotItBtn =
|
|
3603
|
+
const gotItBtn = welcomeModal.querySelector('.cv-welcome-got-it');
|
|
3437
3604
|
if (gotItBtn) {
|
|
3438
|
-
gotItBtn.addEventListener('click',
|
|
3439
|
-
this.closeModal();
|
|
3440
|
-
});
|
|
3605
|
+
gotItBtn.addEventListener('click', closeModal);
|
|
3441
3606
|
}
|
|
3442
3607
|
// Overlay click to close
|
|
3443
|
-
|
|
3444
|
-
if (e.target ===
|
|
3445
|
-
|
|
3608
|
+
welcomeModal.addEventListener('click', (e) => {
|
|
3609
|
+
if (e.target === welcomeModal) {
|
|
3610
|
+
closeModal();
|
|
3446
3611
|
}
|
|
3447
3612
|
});
|
|
3448
3613
|
// Escape key to close
|
|
3449
3614
|
const handleEscape = (e) => {
|
|
3450
3615
|
if (e.key === 'Escape') {
|
|
3451
|
-
|
|
3452
|
-
document.removeEventListener('keydown', handleEscape);
|
|
3616
|
+
closeModal();
|
|
3453
3617
|
}
|
|
3454
3618
|
};
|
|
3455
3619
|
document.addEventListener('keydown', handleEscape);
|
|
@@ -3519,7 +3683,7 @@ function initializeFromScript() {
|
|
|
3519
3683
|
console.warn(`[CustomViews] Config file not found at ${fullConfigPath}. Using defaults.`);
|
|
3520
3684
|
// Provide minimal default config structure
|
|
3521
3685
|
configFile = {
|
|
3522
|
-
config: {
|
|
3686
|
+
config: { toggles: [], defaultState: {} },
|
|
3523
3687
|
widget: { enabled: true }
|
|
3524
3688
|
};
|
|
3525
3689
|
}
|
|
@@ -3557,7 +3721,7 @@ function initializeFromScript() {
|
|
|
3557
3721
|
core,
|
|
3558
3722
|
...configFile.widget
|
|
3559
3723
|
});
|
|
3560
|
-
widget.
|
|
3724
|
+
widget.renderModalIcon();
|
|
3561
3725
|
// Store widget instance
|
|
3562
3726
|
window.customViewsInstance.widget = widget;
|
|
3563
3727
|
console.log('[CustomViews] Widget initialized and rendered');
|