您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a thumbnail view similar to ms word
当前为
// ==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(); })();