@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
  */
@@ -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 all tab groups in the DOM
435
+ * Apply tab selections to a given list of tab group elements
436
436
  */
437
- static applyTabSelections(rootEl, tabs, cfgGroups) {
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(rootEl, cfgGroups, onTabClick, onTabDoubleClick) {
545
- // Find all cv-tabgroup elements with nav="auto" or no nav attribute
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(rootEl, tabs, cfgGroups) {
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(rootEl, tabs) {
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 all toggle elements in the DOM
988
+ * Apply toggle visibility to a given list of toggle elements
989
989
  */
990
- static applyToggles(rootEl, activeToggles) {
991
- rootEl.querySelectorAll(TOGGLE_SELECTOR).forEach(el => {
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(rootEl, activeToggles, assetsManager) {
1001
- rootEl.querySelectorAll(TOGGLE_SELECTOR).forEach(el => {
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
- if (toggleId && categories.some(cat => activeToggles.includes(cat))) {
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: [...(this.config.allToggles || [])],
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
- // Build navigation once (with click and double-click handlers)
1456
- TabManager.buildNavs(this.rootEl, this.config.tabGroups,
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
- // Apply stored nav visibility preference on page load
1480
- const navPref = this.persistenceManager.getPersistedTabNavVisibility();
1481
- if (navPref !== null) {
1482
- TabManager.setNavsVisibility(this.rootEl, navPref);
1483
- }
1484
- // For session history, clicks on back/forward button
1485
- window.addEventListener("popstate", () => {
1486
- this.loadAndCallApplyState();
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
- this.loadAndCallApplyState();
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
- /** Render all toggles for the current state */
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(this.rootEl, finalToggles);
1677
+ ToggleManager.applyToggles(toggleElements, finalToggles);
1549
1678
  // Render assets into toggles
1550
- ToggleManager.renderAssets(this.rootEl, finalToggles, this.assetsManager);
1679
+ ToggleManager.renderAssets(toggleElements, finalToggles, this.assetsManager);
1551
1680
  // Apply tab selections
1552
- TabManager.applyTabSelections(this.rootEl, state.tabs || {}, this.config.tabGroups);
1681
+ TabManager.applyTabSelections(tabGroupElements, state.tabs || {}, this.config.tabGroups);
1553
1682
  // Update nav active states (without rebuilding)
1554
- TabManager.updateAllNavActiveStates(this.rootEl, state.tabs || {}, this.config.tabGroups);
1683
+ TabManager.updateAllNavActiveStates(tabGroupElements, state.tabs || {}, this.config.tabGroups);
1555
1684
  // Update pin icons to show which tabs are persisted
1556
- TabManager.updatePinIcons(this.rootEl, state.tabs || {});
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 = { allToggles: [], defaultState: {} };
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
- modal = null;
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
- // No external state manager to initialize
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
- render() {
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.modal) {
2981
- this.modal.remove();
2982
- this.modal = null;
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.modal) {
2996
- this.modal.remove();
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
- // Get toggles from current configuration and open the modal regardless of count
3005
- const config = this.core.getConfig();
3006
- const toggles = config?.allToggles || [];
3007
- this.createCustomStateModal(toggles);
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
- createCustomStateModal(toggles) {
3013
- // Close existing modal
3014
- this.closeModal();
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
- const toggleControlsHtml = toggles.map(toggle => `
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">${this.formatToggleName(toggle)}</p>
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 tabGroups = this.core.getTabGroups();
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.modal.innerHTML = `
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
- ${toggles.length ? `
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
- document.body.appendChild(this.modal);
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 custom state creator
3316
+ * Attach event listeners for the modal frame (delegated events)
3128
3317
  */
3129
- attachStateModalEventListeners() {
3130
- if (!this.modal)
3318
+ _attachStateModalFrameEventListeners() {
3319
+ if (!this.stateModal)
3131
3320
  return;
3132
- // Close button
3133
- const closeBtn = this.modal.querySelector('.cv-modal-close');
3134
- if (closeBtn) {
3135
- closeBtn.addEventListener('click', () => {
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
- // Copy URL button
3140
- const copyUrlBtn = this.modal.querySelector('.cv-share-btn');
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
- // Visual feedback: change icon to tick for 3 seconds
3145
- const iconContainer = copyUrlBtn.querySelector('.cv-share-btn-icon');
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
- // Reset to default button
3157
- const resetBtn = this.modal.querySelector('.cv-reset-btn');
3158
- if (resetBtn) {
3159
- resetBtn.addEventListener('click', () => {
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); // 600ms matches the animation duration
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.modal.querySelectorAll('.cv-toggle-input');
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.modal.querySelectorAll('.cv-tabgroup-select');
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.modal.querySelector('.cv-nav-pref-input');
3206
- const navIcon = this.modal?.querySelector('#cv-nav-icon');
3207
- const navHeaderCard = this.modal?.querySelector('.cv-tabgroup-card.cv-tabgroup-header');
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
- // Add hover listeners to entire header card
3221
- navHeaderCard.addEventListener('mouseenter', () => {
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
- const rootEl = document.body;
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.modal)
3438
+ if (!this.stateModal)
3265
3439
  return;
3266
3440
  if (this.options.theme === 'dark') {
3267
- this.modal.classList.add('cv-widget-theme-dark');
3441
+ this.stateModal.classList.add('cv-widget-theme-dark');
3268
3442
  }
3269
3443
  else {
3270
- this.modal.classList.remove('cv-widget-theme-dark');
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.modal) {
3451
+ if (!this.stateModal) {
3278
3452
  return {};
3279
3453
  }
3280
3454
  // Collect toggle values
3281
3455
  const toggles = [];
3282
- const toggleInputs = this.modal.querySelectorAll('.cv-toggle-input');
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.modal.querySelectorAll('.cv-tabgroup-select');
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.modal)
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.modal.querySelectorAll('.cv-toggle-input');
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.modal?.querySelector(`[data-toggle="${toggle}"]`);
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.modal.querySelectorAll('.cv-tabgroup-select');
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.modal.querySelector('.cv-nav-pref-input');
3352
- const navIcon = this.modal?.querySelector('#cv-nav-icon');
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
- // Check if this page has any custom views elements
3378
- const hasCustomViewElements = document.querySelector('cv-tabgroup') !== null ||
3379
- document.querySelector('cv-tab') !== null ||
3380
- document.querySelector('cv-toggle') !== null ||
3381
- document.querySelector('[data-cv-toggle]') !== null;
3382
- if (hasCustomViewElements) {
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.modal)
3560
+ if (this.stateModal && !this.stateModal.classList.contains('cv-hidden'))
3398
3561
  return;
3399
- this.modal = document.createElement('div');
3400
- this.modal.className = 'cv-widget-modal-overlay cv-welcome-modal-overlay';
3401
- this.applyThemeToModal();
3402
- this.modal.innerHTML = `
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(this.modal);
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
- if (!this.modal)
3434
- return;
3597
+ attachWelcomeModalEventListeners(welcomeModal) {
3598
+ const closeModal = () => {
3599
+ welcomeModal.remove();
3600
+ document.removeEventListener('keydown', handleEscape);
3601
+ };
3435
3602
  // Got it button
3436
- const gotItBtn = this.modal.querySelector('.cv-welcome-got-it');
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
- this.modal.addEventListener('click', (e) => {
3444
- if (e.target === this.modal) {
3445
- this.closeModal();
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
- this.closeModal();
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: { allToggles: [], defaultState: {} },
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.render();
3724
+ widget.renderModalIcon();
3561
3725
  // Store widget instance
3562
3726
  window.customViewsInstance.widget = widget;
3563
3727
  console.log('[CustomViews] Widget initialized and rendered');