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