@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
  */
@@ -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 all tab groups in the DOM
433
+ * Apply tab selections to a given list of tab group elements
434
434
  */
435
- static applyTabSelections(rootEl, tabs, cfgGroups) {
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(rootEl, cfgGroups, onTabClick, onTabDoubleClick) {
543
- // Find all cv-tabgroup elements with nav="auto" or no nav attribute
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(rootEl, tabs, cfgGroups) {
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(rootEl, tabs) {
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 all toggle elements in the DOM
986
+ * Apply toggle visibility to a given list of toggle elements
987
987
  */
988
- static applyToggles(rootEl, activeToggles) {
989
- rootEl.querySelectorAll(TOGGLE_SELECTOR).forEach(el => {
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(rootEl, activeToggles, assetsManager) {
999
- rootEl.querySelectorAll(TOGGLE_SELECTOR).forEach(el => {
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
- if (toggleId && categories.some(cat => activeToggles.includes(cat))) {
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: [...(this.config.allToggles || [])],
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
- // Build navigation once (with click and double-click handlers)
1454
- TabManager.buildNavs(this.rootEl, this.config.tabGroups,
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
- // Apply stored nav visibility preference on page load
1478
- const navPref = this.persistenceManager.getPersistedTabNavVisibility();
1479
- if (navPref !== null) {
1480
- TabManager.setNavsVisibility(this.rootEl, navPref);
1481
- }
1482
- // For session history, clicks on back/forward button
1483
- window.addEventListener("popstate", () => {
1484
- this.loadAndCallApplyState();
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
- this.loadAndCallApplyState();
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
- /** Render all toggles for the current state */
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(this.rootEl, finalToggles);
1675
+ ToggleManager.applyToggles(toggleElements, finalToggles);
1547
1676
  // Render assets into toggles
1548
- ToggleManager.renderAssets(this.rootEl, finalToggles, this.assetsManager);
1677
+ ToggleManager.renderAssets(toggleElements, finalToggles, this.assetsManager);
1549
1678
  // Apply tab selections
1550
- TabManager.applyTabSelections(this.rootEl, state.tabs || {}, this.config.tabGroups);
1679
+ TabManager.applyTabSelections(tabGroupElements, state.tabs || {}, this.config.tabGroups);
1551
1680
  // Update nav active states (without rebuilding)
1552
- TabManager.updateAllNavActiveStates(this.rootEl, state.tabs || {}, this.config.tabGroups);
1681
+ TabManager.updateAllNavActiveStates(tabGroupElements, state.tabs || {}, this.config.tabGroups);
1553
1682
  // Update pin icons to show which tabs are persisted
1554
- TabManager.updatePinIcons(this.rootEl, state.tabs || {});
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 = { allToggles: [], defaultState: {} };
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
- modal = null;
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
- // No external state manager to initialize
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
- render() {
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.modal) {
2979
- this.modal.remove();
2980
- this.modal = null;
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.modal) {
2994
- this.modal.remove();
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
- // Get toggles from current configuration and open the modal regardless of count
3003
- const config = this.core.getConfig();
3004
- const toggles = config?.allToggles || [];
3005
- this.createCustomStateModal(toggles);
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
- createCustomStateModal(toggles) {
3011
- // Close existing modal
3012
- this.closeModal();
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
- const toggleControlsHtml = toggles.map(toggle => `
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">${this.formatToggleName(toggle)}</p>
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 tabGroups = this.core.getTabGroups();
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.modal.innerHTML = `
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
- ${toggles.length ? `
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
- document.body.appendChild(this.modal);
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 custom state creator
3314
+ * Attach event listeners for the modal frame (delegated events)
3126
3315
  */
3127
- attachStateModalEventListeners() {
3128
- if (!this.modal)
3316
+ _attachStateModalFrameEventListeners() {
3317
+ if (!this.stateModal)
3129
3318
  return;
3130
- // Close button
3131
- const closeBtn = this.modal.querySelector('.cv-modal-close');
3132
- if (closeBtn) {
3133
- closeBtn.addEventListener('click', () => {
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
- // Copy URL button
3138
- const copyUrlBtn = this.modal.querySelector('.cv-share-btn');
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
- // Visual feedback: change icon to tick for 3 seconds
3143
- const iconContainer = copyUrlBtn.querySelector('.cv-share-btn-icon');
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
- // Reset to default button
3155
- const resetBtn = this.modal.querySelector('.cv-reset-btn');
3156
- if (resetBtn) {
3157
- resetBtn.addEventListener('click', () => {
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); // 600ms matches the animation duration
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.modal.querySelectorAll('.cv-toggle-input');
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.modal.querySelectorAll('.cv-tabgroup-select');
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.modal.querySelector('.cv-nav-pref-input');
3204
- const navIcon = this.modal?.querySelector('#cv-nav-icon');
3205
- const navHeaderCard = this.modal?.querySelector('.cv-tabgroup-card.cv-tabgroup-header');
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
- // Add hover listeners to entire header card
3219
- navHeaderCard.addEventListener('mouseenter', () => {
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
- const rootEl = document.body;
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.modal)
3436
+ if (!this.stateModal)
3263
3437
  return;
3264
3438
  if (this.options.theme === 'dark') {
3265
- this.modal.classList.add('cv-widget-theme-dark');
3439
+ this.stateModal.classList.add('cv-widget-theme-dark');
3266
3440
  }
3267
3441
  else {
3268
- this.modal.classList.remove('cv-widget-theme-dark');
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.modal) {
3449
+ if (!this.stateModal) {
3276
3450
  return {};
3277
3451
  }
3278
3452
  // Collect toggle values
3279
3453
  const toggles = [];
3280
- const toggleInputs = this.modal.querySelectorAll('.cv-toggle-input');
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.modal.querySelectorAll('.cv-tabgroup-select');
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.modal)
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.modal.querySelectorAll('.cv-toggle-input');
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.modal?.querySelector(`[data-toggle="${toggle}"]`);
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.modal.querySelectorAll('.cv-tabgroup-select');
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.modal.querySelector('.cv-nav-pref-input');
3350
- const navIcon = this.modal?.querySelector('#cv-nav-icon');
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
- // Check if this page has any custom views elements
3376
- const hasCustomViewElements = document.querySelector('cv-tabgroup') !== null ||
3377
- document.querySelector('cv-tab') !== null ||
3378
- document.querySelector('cv-toggle') !== null ||
3379
- document.querySelector('[data-cv-toggle]') !== null;
3380
- if (hasCustomViewElements) {
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.modal)
3558
+ if (this.stateModal && !this.stateModal.classList.contains('cv-hidden'))
3396
3559
  return;
3397
- this.modal = document.createElement('div');
3398
- this.modal.className = 'cv-widget-modal-overlay cv-welcome-modal-overlay';
3399
- this.applyThemeToModal();
3400
- this.modal.innerHTML = `
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(this.modal);
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
- if (!this.modal)
3432
- return;
3595
+ attachWelcomeModalEventListeners(welcomeModal) {
3596
+ const closeModal = () => {
3597
+ welcomeModal.remove();
3598
+ document.removeEventListener('keydown', handleEscape);
3599
+ };
3433
3600
  // Got it button
3434
- const gotItBtn = this.modal.querySelector('.cv-welcome-got-it');
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
- this.modal.addEventListener('click', (e) => {
3442
- if (e.target === this.modal) {
3443
- this.closeModal();
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
- this.closeModal();
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: { allToggles: [], defaultState: {} },
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.render();
3722
+ widget.renderModalIcon();
3559
3723
  // Store widget instance
3560
3724
  window.customViewsInstance.widget = widget;
3561
3725
  console.log('[CustomViews] Widget initialized and rendered');