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