Pages View Google Docs

Adds a thumbnail view similar to ms word

当前为 2025-04-02 提交的版本,查看 最新版本

// ==UserScript==
// @name         Pages View Google Docs
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Adds a thumbnail view similar to ms word
// @match        https://docs.google.com/document/d/*
// @license MIT
// @grant        none
// ==/UserScript==

(() => {
  'use strict';

  /* ===================================
     Utility Functions & Constants
  =================================== */
  const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

       function clickElement(element) {
        const mouseDown = new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window });
        const mouseUp = new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window });
        const clickEvt = new MouseEvent('click', { bubbles: true, cancelable: true, view: window });
        element.dispatchEvent(mouseDown);
        element.dispatchEvent(mouseUp);
        element.dispatchEvent(clickEvt);
        console.log('Simulated click on', element);
    }


  /* ==========================================================
     Module 1: Document & Scroll Utilities
  ========================================================== */
  const getDocumentId = () => {
    const match = window.location.href.match(/\/d\/([a-zA-Z0-9_-]+)/);
    return match ? match[1] : 'default';
  };

  const getScrollableElement = () => document.querySelector('.kix-appview-editor');

  const saveScrollPosition = () => {
    const docId = getDocumentId();
    const scrollable = getScrollableElement();
    if (scrollable) {
      const scrollPos = scrollable.scrollTop;
      const data = JSON.parse(localStorage.getItem('googleDocsScrollData') || '{}');
      data[docId] = scrollPos;
      localStorage.setItem('googleDocsScrollData', JSON.stringify(data));
    }
  };

  const restoreScrollPosition = () => {
    const docId = getDocumentId();
    const data = JSON.parse(localStorage.getItem('googleDocsScrollData') || '{}');
    const scrollPos = data[docId];
    const scrollable = getScrollableElement();
    if (scrollable && scrollPos !== undefined) {
      scrollable.scrollTop = parseInt(scrollPos, 10);
    }
  };

  /* ==========================================================
     Module 2: Thumbnail Overlay Management & Dynamic Positioning
  ========================================================== */
  let thumbnailOverlay = null;
  let overlayPositionObserver = null;

  const updateOverlayPosition = () => {
    let topOffset = '50px';
    let ruler = document.getElementById('kix-horizontal-ruler');
    if (!ruler) {
      ruler = document.querySelector('div#docs-chrome[aria-label="Menu bar"]');
    }
    if (ruler) {
      topOffset = `${ruler.getBoundingClientRect().bottom}px`;
    }
    let leftOffset = '10px';
    const sidebar = document.querySelector('.left-sidebar-container');
    if (sidebar) {
      leftOffset = `${sidebar.getBoundingClientRect().right}px`;
    }
    if (thumbnailOverlay) {
      thumbnailOverlay.style.top = topOffset;
      thumbnailOverlay.style.left = leftOffset;
    }
  };

  const startOverlayPositionObserver = () => {
    const targets = [];
    const sidebar = document.querySelector('.left-sidebar-container');
    if (sidebar) targets.push(sidebar);
    let ruler = document.getElementById('kix-horizontal-ruler');
    if (!ruler) {
      ruler = document.querySelector('div#docs-chrome[aria-label="Menu bar"]');
    }
    if (ruler) targets.push(ruler);
    if (targets.length === 0) return;
    overlayPositionObserver = new MutationObserver(() => {
      updateOverlayPosition();
    });
    targets.forEach(target => {
      overlayPositionObserver.observe(target, { attributes: true, attributeFilter: ['style', 'class'] });
    });
  };

  const stopOverlayPositionObserver = () => {
    if (overlayPositionObserver) {
      overlayPositionObserver.disconnect();
      overlayPositionObserver = null;
    }
  };

  const createThumbnailOverlay = () => {
    thumbnailOverlay = document.createElement('div');
    thumbnailOverlay.id = 'thumbnailOverlay';
    updateOverlayPosition();
    Object.assign(thumbnailOverlay.style, {
      position: 'fixed',
      right: '0',
      bottom: '0',
      background: '#f9fbfd',
      zIndex: '10000',
      overflowY: 'auto',
      display: 'flex',
      flexWrap: 'wrap',
      padding: '10px',
      alignContent: 'flex-start'
    });
    document.body.appendChild(thumbnailOverlay);
    startOverlayPositionObserver();
  };

  const removeThumbnailOverlay = () => {
    if (thumbnailOverlay) {
      thumbnailOverlay.remove();
      thumbnailOverlay = null;
    }
    stopOverlayPositionObserver();
  };

  /* ==========================================================
     Module 3: Thumbnail Display & Zoom Functionality
  ========================================================== */
  const insertThumbnailInOrder = (thumbElement, pageNumber) => {
    if (!thumbnailOverlay) return;
    const thumbnails = Array.from(thumbnailOverlay.querySelectorAll('.thumbnail-entry'));
    const insertIndex = thumbnails.findIndex(el => parseInt(el.dataset.pageNumber, 10) > pageNumber);
    if (insertIndex >= 0) {
      thumbnailOverlay.insertBefore(thumbElement, thumbnails[insertIndex]);
    } else {
      thumbnailOverlay.appendChild(thumbElement);
    }
  };

  let thumbnailZoomFactor = 1;
  const ZOOM_STEP = 0.1;
  const MIN_ZOOM = 0.5;
  const MAX_ZOOM = 2.0;

  const updateThumbnailZoom = () => {
    if (!thumbnailOverlay) return;
    thumbnailOverlay.querySelectorAll('.thumbnail-entry img').forEach(img => {
      img.style.width = `${200 * thumbnailZoomFactor}px`;
    });
  };

  const handleCtrlZoom = event => {
    if (!isThumbnailViewActive || !event.ctrlKey) return;
    if (['=', 'Add', 'NumpadAdd'].includes(event.key)) {
      event.preventDefault();
      event.stopImmediatePropagation();
      if (thumbnailZoomFactor < MAX_ZOOM) {
        thumbnailZoomFactor += ZOOM_STEP;
        updateThumbnailZoom();
      }
    } else if (['-', 'Subtract', 'NumpadSubtract'].includes(event.key)) {
      event.preventDefault();
      event.stopImmediatePropagation();
      if (thumbnailZoomFactor > MIN_ZOOM) {
        thumbnailZoomFactor -= ZOOM_STEP;
        updateThumbnailZoom();
      }
    }
  };

  const attachZoomListeners = () => {
    window.addEventListener('keydown', handleCtrlZoom, true);
    document.querySelectorAll('iframe').forEach(iframe => {
      try {
        (iframe.contentDocument || iframe.contentWindow.document)
          .addEventListener('keydown', handleCtrlZoom, true);
      } catch (err) {
        console.error('Error attaching zoom listener:', err);
      }
    });
  };

  const detachZoomListeners = () => {
    window.removeEventListener('keydown', handleCtrlZoom, true);
    document.querySelectorAll('iframe').forEach(iframe => {
      try {
        (iframe.contentDocument || iframe.contentWindow.document)
          .removeEventListener('keydown', handleCtrlZoom, true);
      } catch (err) {
        console.error('Error detaching zoom listener:', err);
      }
    });
  };

  /* ==========================================================
     Module 4: Page Capture Module
  ========================================================== */
  const capturedPages = new Set();
  let captureTimeoutId = null;
  let mutationObserver = null;

  // --- New globals for Heading Mapping & Grouping ---
  let isGroupingEnabled = false; // Toggle for grouped view.
  // Stores captured page data: { [pageNumber]: { thumbEntry, headings: Set() } }
  const capturedPageData = {};
  // Replace the old headingGroups object with an array for the new grouping logic.
  let headingGroupsArr = [];

  // --- (Old heading extraction functions remain unchanged) ---
  const getCurrentPageNumber = () => {
    const tooltip = document.querySelector('div.jfk-tooltip-contentId[style*="direction: ltr"]');
    if (tooltip) {
      const match = tooltip.textContent.match(/(\d+)\s+of/);
      return match ? parseInt(match[1], 10) : null;
    }
    return null;
  };

  const getCurrentSelectedHeading = () => {
    const selector = '#chapter-container-t\\.wf0m5iat3jku > div.chapter-item.chapter-item-subchapters-indent-enabled > div.updating-navigation-item-list > div.navigation-item-list.goog-container .navigation-item.location-indicator-highlight';
    const headingElem = document.querySelector(selector);
    if (headingElem) {
      const content = headingElem.querySelector('.navigation-item-content');
      const headingText = content ? content.textContent.trim() : 'Unknown';
      let navLevel = 0;
      const levelMatch = content ? content.className.match(/navigation-item-level-(\d+)/) : null;
      if (levelMatch) {
        navLevel = parseInt(levelMatch[1], 10);
      }
      return { headingElem, headingText, navLevel };
    }
    return null;
  };

  const getHighestParentHeading = (currentHeadingElem) => {
    const parentContainer = currentHeadingElem.parentNode;
    const allHeadings = Array.from(parentContainer.querySelectorAll('.navigation-item'));
    const currentIndex = allHeadings.indexOf(currentHeadingElem);
    let highestHeading = currentHeadingElem;
    let highestLevel = Infinity;
    for (let i = currentIndex - 1; i >= 0; i--) {
      const elem = allHeadings[i];
      const content = elem.querySelector('.navigation-item-content');
      if (content) {
        const levelMatch = content.className.match(/navigation-item-level-(\d+)/);
        let level = levelMatch ? parseInt(levelMatch[1], 10) : 0;
        if (level < highestLevel) {
          highestLevel = level;
          highestHeading = elem;
        }
      }
    }
    const content = highestHeading.querySelector('.navigation-item-content');
    return {
      headingElem: highestHeading,
      headingText: content ? content.textContent.trim() : 'Unknown',
      navLevel: highestLevel === Infinity ? 0 : highestLevel
    };
  };

  // ----- REMOVED updateHeadingMapping() -----
  // The previous per-page heading mapping is no longer used.
  // Instead, heading starting pages are recorded via recordHeadingStartingPages() below.

  // NEW: Function to record top-level heading starting pages.
  const recordHeadingStartingPages = async () => {
    // Clear any previous heading groups.
    headingGroupsArr = [];
    // Get all navigation items and filter for top-level (navLevel 0).
    const allNavItems = document.querySelectorAll('.navigation-item');
    const topLevelHeadings = Array.from(allNavItems).filter(item => {
      const content = item.querySelector('.navigation-item-content');
      return content && content.classList.contains('navigation-item-level-0');
    });
    // Loop through each top-level heading.
    for (const item of topLevelHeadings) {
      const content = item.querySelector('.navigation-item-content');
      const headingText = content ? content.textContent.trim() : 'Unknown';
      // Simulate click on the heading.
      clickElement(item);
      // Wait for the page scroll to update.
      await sleep(500);
      const currentPage = getCurrentPageNumber();
      if (currentPage) {
        headingGroupsArr.push({
          headingText,
          navLevel: 0,
          startingPage: currentPage,
          pages: new Set()
        });
      }
    }
  };

  // NEW: Function to assign pages to each heading group based on starting pages.
  const assignPagesToGroups = () => {
    // Sort the heading groups by startingPage.
    headingGroupsArr.sort((a, b) => a.startingPage - b.startingPage);
    // Get sorted captured page numbers.
    const capturedPageNumbers = Object.keys(capturedPageData).map(Number).sort((a, b) => a - b);
    // Build a mapping for each distinct starting page.
    const startingPageMap = {};
    const distinctStartPages = [...new Set(headingGroupsArr.map(g => g.startingPage))].sort((a, b) => a - b);
    distinctStartPages.forEach((sp, index) => {
      let nextSP = Infinity;
      if (index < distinctStartPages.length - 1) {
        nextSP = distinctStartPages[index + 1];
      }
      startingPageMap[sp] = new Set(capturedPageNumbers.filter(pageNum => pageNum >= sp && pageNum < nextSP));
    });
    // Assign pages to each heading group.
    headingGroupsArr.forEach(group => {
      group.pages = new Set(startingPageMap[group.startingPage]);
    });
  };

  const capturePages = () => {
    if (!thumbnailOverlay) return;
    const pages = Array.from(document.querySelectorAll('.kix-page-paginated'));
    const scrollable = getScrollableElement();

    pages.forEach((page, index) => {
      const rotatingTileManager = page.closest('.kix-rotatingtilemanager.docs-ui-hit-region-surface');
      if (
        rotatingTileManager &&
        rotatingTileManager.parentElement &&
        window.getComputedStyle(rotatingTileManager.parentElement).display === 'none'
      ) {
        return;
      }
      let pageNumber = parseInt(page.style.zIndex, 10);
      pageNumber = !isNaN(pageNumber) ? pageNumber + 1 : index + 1;
      if (capturedPages.has(pageNumber)) return;

      const canvas = page.querySelector('canvas.kix-canvas-tile-content');
      if (!canvas) return;

      // Force a reflow/repaint on the canvas.
      canvas.style.display = 'none';
      void canvas.offsetHeight;
      canvas.style.display = '';

      let dataUrl;
      try {
        dataUrl = canvas.toDataURL();
      } catch (err) {
        console.error('Error converting canvas to image:', err);
        return;
      }

      let pageScrollPos = 0;
      if (scrollable) {
        const containerRect = scrollable.getBoundingClientRect();
        const pageRect = page.getBoundingClientRect();
        pageScrollPos = scrollable.scrollTop + (pageRect.top - containerRect.top);
      }

      const thumbEntry = document.createElement('div');
      thumbEntry.className = 'thumbnail-entry';
      thumbEntry.dataset.pageNumber = pageNumber;
      thumbEntry.dataset.scrollPos = pageScrollPos;
      Object.assign(thumbEntry.style, {
        margin: '10px',
        textAlign: 'center',
        cursor: 'pointer',
        opacity: '0',
        transition: 'opacity 0.5s'
      });

      const img = document.createElement('img');
      img.src = dataUrl;
      img.style.width = `${200 * thumbnailZoomFactor}px`;
      img.style.height = 'auto';
      img.name = `page_${pageNumber}`;
      thumbEntry.appendChild(img);

      const pageLabel = document.createElement('div');
      pageLabel.innerText = `Page ${pageNumber}`;
      pageLabel.style.marginTop = '5px';
      thumbEntry.appendChild(pageLabel);

      // Bind click event to the thumbnail.
      thumbEntry.addEventListener('click', () => {
        exitThumbnailView();
        isGroupingEnabled = false;
        const targetPos = parseInt(thumbEntry.dataset.scrollPos, 10);
        if (scrollable) {
          scrollable.scrollTop = targetPos;
        }
      });

      if (!isGroupingEnabled) {
        insertThumbnailInOrder(thumbEntry, pageNumber);
      }

      capturedPageData[pageNumber] = capturedPageData[pageNumber] || { thumbEntry: null, headings: new Set() };
      capturedPageData[pageNumber].thumbEntry = thumbEntry;

      // ----- REMOVED call to updateHeadingMapping() -----
      // (No longer recording headings per page.)

      setTimeout(() => { thumbEntry.style.opacity = '1'; }, 50);
      capturedPages.add(pageNumber);
    });
    updateProgressBar();
  };

  const startObservingPages = () => {
    const container = document.querySelector('.kix-rotatingtilemanager-content');
    if (!container) return;
    mutationObserver = new MutationObserver(() => {
      clearTimeout(captureTimeoutId);
      captureTimeoutId = setTimeout(capturePagesWrapper, 100);
    });
    mutationObserver.observe(container, { childList: true, subtree: true, attributes: true });
  };

  const stopObservingPages = () => {
    if (mutationObserver) {
      mutationObserver.disconnect();
      mutationObserver = null;
    }
  };

  /* ==========================================================
     Module 5: Fast Scroll Simulation Module
  ========================================================== */
  let cancelScrollSequence = false;
  const simulateScrollSequence = async () => {
    const scrollable = getScrollableElement();
    if (!scrollable) return;
    scrollable.scrollTop = 0;
    startObservingPages();
    capturePagesWrapper();
    const intervalId = setInterval(() => {
      if (cancelScrollSequence) {
        clearInterval(intervalId);
        return;
      }
      const currentScroll = scrollable.scrollTop;
      const viewportHeight = scrollable.clientHeight;
      const newScroll = currentScroll + viewportHeight;
      if (newScroll >= scrollable.scrollHeight) {
        scrollable.scrollTop = scrollable.scrollHeight;
        capturePagesWrapper();
        if (progressBarInner) {
          progressBarInner.style.background = '#2684fc';
        }
        clearInterval(intervalId);
        console.log("Reached bottom of page, scroll sequence complete.");
        // NEW: After reaching bottom, record headings and assign pages.
        recordHeadingStartingPages().then(() => {
          assignPagesToGroups();
          if (isGroupingEnabled) {
            renderGroupedThumbnails();
          }
        });
      } else {
        scrollable.scrollTop = newScroll;
        capturePagesWrapper();
      }
    }, 100);
  };

  /* ==========================================================
     Module 6: Thumbnail View Toggle & Cleanup
  ========================================================== */
  let isThumbnailViewActive = false;
  let pagesViewButton = null;
  let progressBarContainer = null;
  let progressBarInner = null;

  const createProgressBar = () => {
    if (!pagesViewButton) return;
    progressBarContainer = document.createElement('div');
    progressBarContainer.style.position = 'absolute';
    progressBarContainer.style.top = '0';
    progressBarContainer.style.left = '50%';
    progressBarContainer.style.transform = 'translateX(-50%) translateY(1px)';
    progressBarContainer.style.width = '60%';
    progressBarContainer.style.height = '2px';
    progressBarContainer.style.background = 'transparent';
    progressBarContainer.style.pointerEvents = 'none';
    progressBarInner = document.createElement('div');
    progressBarInner.style.height = '100%';
    progressBarInner.style.width = '0%';
    progressBarInner.style.background = '#555';
    progressBarContainer.appendChild(progressBarInner);
    pagesViewButton.appendChild(progressBarContainer);
  };

  const removeProgressBar = () => {
    if (progressBarContainer && progressBarContainer.parentNode) {
      progressBarContainer.parentNode.removeChild(progressBarContainer);
    }
    progressBarContainer = null;
    progressBarInner = null;
  };

  const updateProgressBar = () => {
    if (!progressBarInner) return;
    let tooltipElem = document.querySelector('div.jfk-tooltip-contentId[style*="direction: ltr"]');
    if (!tooltipElem) {
      setTimeout(updateProgressBar, 500);
      return;
    }
    let match = tooltipElem.textContent.match(/of\s*(\d+)/);
    if (!match) return;
    let maxPages = parseInt(match[1], 10);
    if (maxPages === 0) return;
    let capturedCount = capturedPages.size;
    let progressPercent = Math.min((capturedCount / maxPages) * 100, 100);
    progressBarInner.style.width = progressPercent + '%';
  };

  const toggleThumbnailView = () => {
    if (!isThumbnailViewActive) {
      cancelScrollSequence = false;
      saveScrollPosition();
      createThumbnailOverlay();
      simulateScrollSequence();
      isThumbnailViewActive = true;
      attachZoomListeners();
      createProgressBar();
      createCustomMenu(pagesViewButton);
    } else {
      exitThumbnailView();
    }
  };

  const exitThumbnailView = (skipRestore = false) => {
    cancelScrollSequence = true;
    removeThumbnailOverlay();
    removeProgressBar();
      const customMenu = document.getElementById('customMenu');
  if (customMenu) {
    customMenu.remove();
  }
    if (!skipRestore) restoreScrollPosition();
    stopObservingPages();
    capturedPages.clear();
    // Reset the heading groups and captured page data.
    Object.keys(capturedPageData).forEach(key => delete capturedPageData[key]);
    headingGroupsArr = [];
    isThumbnailViewActive = false;
    detachZoomListeners();
  };

  /* ==========================================================
     Module 7: Button Management Module
  ========================================================== */
  const waitForElement = (selector, timeout = 20000) =>
    new Promise((resolve, reject) => {
      const observer = new MutationObserver((_, obs) => {
        const el = document.querySelector(selector);
        if (el) {
          obs.disconnect();
          resolve(el);
        }
      });
      observer.observe(document.body, { childList: true, subtree: true });
      setTimeout(() => {
        observer.disconnect();
        reject(new Error(`Timeout waiting for element: ${selector}`));
      }, timeout);
    });

  const addPagesViewButton = referenceElement => {
    const newButton = document.createElement('div');
    newButton.setAttribute('role', 'button');
    newButton.className = 'goog-inline-block jfk-button jfk-button-standard kix-outlines-widget-header-add-chapter-button-icon custom-pages-view-button';
    newButton.tabIndex = 0;
    newButton.setAttribute('data-tooltip-class', 'kix-outlines-widget-header-add-chapter-button-tooltip');
    newButton.setAttribute('aria-label', 'Pages view');
    newButton.setAttribute('data-tooltip', 'Pages view');
    const iconWrapper = document.createElement('div');
    iconWrapper.className = 'docs-icon goog-inline-block';
    const iconInner = document.createElement('div');
    iconInner.className = 'docs-icon-img-container docs-icon-img docs-icon-editors-ia-content-copy';
    iconInner.setAttribute('aria-hidden', 'true');
    iconInner.textContent = '\u00A0';
    iconWrapper.appendChild(iconInner);
    newButton.appendChild(iconWrapper);
    const style = document.createElement('style');
    style.textContent = `
      .custom-pages-view-button {
        user-select: none;
        direction: ltr;
        visibility: visible;
        position: relative;
        display: inline-block;
        cursor: pointer;
        font-size: 11px;
        text-align: center;
        white-space: nowrap;
        line-height: 27px;
        outline: 0;
        color: #333;
        border: 1px solid rgba(0,0,0,.1);
        font-family: "Google Sans",Roboto,RobotoDraft,Helvetica,Arial,sans-serif;
        font-weight: 500;
        background-color: transparent;
        background-image: none;
        border-radius: 50%;
        border-width: 0;
        box-shadow: none;
        min-width: unset;
        height: 28px;
        margin: 2px;
        padding: 0;
        width: 28px;
        transition: background-color 0.3s ease;
      }
      .custom-pages-view-button:hover {
        background-color: rgba(0,0,0,0.1);
      }
    `;
    document.head.appendChild(style);
    referenceElement.parentNode.insertBefore(newButton, referenceElement.nextSibling);
    pagesViewButton = newButton;
    newButton.addEventListener('click', toggleThumbnailView);
  };
const createCustomMenu = (referenceButton) => {
  const menu = document.createElement('div');
  menu.id = 'customMenu';

  // Position the menu above the reference (Pages View) button.
  const rect = referenceButton.getBoundingClientRect();
  menu.style.position = 'absolute';
  menu.style.left = (rect.left - 180) + 'px';
  menu.style.top = (rect.top - 40 -15) + 'px'; // Adjust offset as needed.

  // Apply styling to mimic the Docs menu appearance.
  menu.style.width = '200px';
  menu.style.backgroundColor = '#fff';
  menu.style.borderRadius = '26px';
  menu.style.padding = '8px';
  menu.style.zIndex = '10001';
  menu.style.boxShadow = '0px 1px 4px rgba(0, 0, 0, 0.2)';
  menu.style.fontFamily = 'Roboto,RobotoDraft,Helvetica,Arial,sans-serif';
  menu.style.fontWeight = '400';
  menu.style.fontSize = '13px';
  menu.style.color = '#000';
  menu.style.cursor = 'default';
  menu.style.userSelect = 'none';

  // Use flex layout to arrange the buttons evenly.
  menu.style.display = 'flex';
  menu.style.justifyContent = 'space-around';
  menu.style.alignItems = 'center';

  // Create 4 buttons with the Docs icon wrapper structure.
  for (let i = 1; i <= 4; i++) {
    const btn = document.createElement('div');
    // Use both the default Pages view button style and a custom menu button style.
    btn.className = 'goog-inline-block jfk-button jfk-button-standard custom-pages-view-button custom-menu-button';
    btn.tabIndex = 0;
    // These inline styles ensure the button dimensions match the Pages view button.
    btn.style.width = '28px';
    btn.style.height = '28px';
    btn.style.borderRadius = '50%';
    btn.style.cursor = 'pointer';
    btn.style.display = 'flex';
    btn.style.alignItems = 'center';
    btn.style.justifyContent = 'center';


    // Set tooltip attributes identical to the Pages view button.
    if (i === 1) {
      btn.setAttribute('data-tooltip', 'Section grouping');
    } else if (i === 2) {
      btn.setAttribute('data-tooltip', 'Heading grouping');
    } else if (i === 3) {
      btn.setAttribute('data-tooltip', 'Left-to-right');
    } else if (i === 4) {
      btn.setAttribute('data-tooltip', 'Right-to-left');
    }
    btn.setAttribute('data-tooltip-class', 'kix-outlines-widget-header-add-chapter-button-tooltip');

    // Create the Docs icon wrapper element.
    const iconWrapper = document.createElement('div');
    iconWrapper.className = 'docs-icon goog-inline-block goog-menuitem-icon';
    iconWrapper.setAttribute('aria-hidden', 'true');
    iconWrapper.style.userSelect = 'none';

    // Create the inner icon element.
    const iconInner = document.createElement('div');
    iconInner.className = 'docs-icon-img-container docs-icon-img';
    iconInner.style.userSelect = 'none';

    if (i === 1) {
      // Button 1: Section Grouping.
      iconInner.classList.add('docs-icon-editors-ia-square-grid-view');
    } else if (i === 2) {
      // Button 2: Heading Grouping.
      iconInner.classList.add('docs-icon-editors-ia-header-footer');
         // Add an event listener to toggle grouping on click.
      btn.addEventListener('click', () => {
        isGroupingEnabled = !isGroupingEnabled;
        if (isGroupingEnabled) {
          renderGroupedThumbnails();
        } else {
          // Render flat view by re-appending each thumbnail in order.
          thumbnailOverlay.replaceChildren();
          Object.keys(capturedPageData).sort((a, b) => a - b).forEach(pageNum => {
            const data = capturedPageData[pageNum];
            if (data && data.thumbEntry) {
              thumbnailOverlay.appendChild(data.thumbEntry);
            }
          });
        }
      });
    } else if (i === 3) {
      // Button 3: Left-to-right.
      iconInner.classList.add('docs-icon-text-ltr-20');
    } else if (i === 4) {
      // Button 4: Right-to-left.
      iconInner.classList.add('docs-icon-text-rtl-20');
    }

    iconWrapper.appendChild(iconInner);
    btn.appendChild(iconWrapper);
    menu.appendChild(btn);
  }

  document.body.appendChild(menu);
  return menu;
};






  /* ==========================================================
     Module 8: Grouping & Heading Mapping Functions
  ========================================================== */
  // NEW: Render grouped thumbnails using the new headingGroupsArr and assigned pages.
  const renderGroupedThumbnails = () => {
    if (!thumbnailOverlay) return;
    thumbnailOverlay.replaceChildren();

    // Ensure groups are sorted by startingPage.
    headingGroupsArr.sort((a, b) => a.startingPage - b.startingPage);
    headingGroupsArr.forEach(group => {
      const groupContainer = document.createElement('div');
      groupContainer.style.margin = '10px';
      groupContainer.style.padding = '10px';
      groupContainer.style.border = '1px solid #ccc';
      groupContainer.style.borderRadius = '8px';
      groupContainer.style.background = '#fff';
      // Display heading title with its starting page.
      const headingTitle = document.createElement('div');
      headingTitle.textContent = group.headingText //+ " (Page " + group.startingPage + ")";
      headingTitle.style.fontWeight = 'bold';
      headingTitle.style.marginBottom = '5px';
      groupContainer.appendChild(headingTitle);
      const thumbsContainer = document.createElement('div');
      thumbsContainer.style.display = 'flex';
      thumbsContainer.style.flexWrap = 'wrap';
      thumbsContainer.style.gap = '8px';
      // For each assigned page in the group, clone its thumbnail.
      const pagesSorted = Array.from(group.pages).sort((a, b) => a - b);
      pagesSorted.forEach(pageNum => {
        const data = capturedPageData[pageNum];
        if (data && data.thumbEntry) {
          const thumbClone = data.thumbEntry.cloneNode(true);
          thumbClone.addEventListener('click', () => {
            exitThumbnailView();
            const targetPos = parseInt(thumbClone.dataset.scrollPos, 10);
            const scrollable = getScrollableElement();
            if (scrollable) {
              scrollable.scrollTop = targetPos;
            }
          });
          thumbsContainer.appendChild(thumbClone);
        }
      });
      groupContainer.appendChild(thumbsContainer);
      thumbnailOverlay.appendChild(groupContainer);
    });
  };

  // Wrapper for capturePages that also handles grouping rendering.
  const capturePagesWrapper = () => {
    capturePages();
    if (isGroupingEnabled) {
      renderGroupedThumbnails();
    }
  };

  /* ==========================================================
     Module 9: Grouping Toggle Button
  ========================================================== */
//  const addGroupingToggleButton = () => {
//    const groupingButton = document.createElement('button');
//    groupingButton.textContent = 'Toggle Grouping';
//    groupingButton.style.position = 'fixed';
//    groupingButton.style.bottom = '20px';
//    groupingButton.style.left = '20px';
//    groupingButton.style.zIndex = '10001';
//    groupingButton.style.padding = '5px 10px';
//    groupingButton.style.fontSize = '12px';
//    groupingButton.addEventListener('click', () => {
//      isGroupingEnabled = !isGroupingEnabled;
 //     if (isGroupingEnabled) {
   //     renderGroupedThumbnails();
    //  } else {
      //  // Render the flat view safely:
     //   thumbnailOverlay.replaceChildren();
       // Object.keys(capturedPageData).sort((a, b) => a - b).forEach(pageNum => {
         // const data = capturedPageData[pageNum];
         // if (data && data.thumbEntry) {
           // thumbnailOverlay.appendChild(data.thumbEntry);
         // }
      //  });
     // }
   // });
   // document.body.appendChild(groupingButton);
//  };

  /* ==========================================================
     Initialization
  ========================================================== */
  waitForElement('.kix-outlines-widget-header-add-chapter-button')
    .then(addPagesViewButton)
    .catch(console.error);

  // Add the grouping toggle button.
 // addGroupingToggleButton();

})();