// ==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();
})();