// ==UserScript== // @name MangaDex Condensed // @namespace suckerfree // @license MIT // @version 26 // @description Enhance MangaDex with lots of display options to make it easier to find unread chapters. // @author Nalin // @match https://mangadex.org/* // @icon https://www.google.com/s2/favicons?domain=mangadex.org // // @require https://cdn.jsdelivr.net/gh/sizzlemctwizzle/GM_config@2207c5c1322ebb56e401f03c2e581719f909762a/gm_config.js // @grant GM_getValue // @grant GM_setValue // @downloadURL none // ==/UserScript== (function() { // Configure GM_config. GM_config.init( { 'id': 'MangaDexCondensed', 'fields': { 'CoverMode': { 'label': 'Popup/Enlarge Cover When Hovered On', 'type': 'select', 'options': ['Container', 'Title + Cover', 'Title', 'Cover', 'Never'], 'default': 'Cover' }, 'CoverStyle': { 'label': 'Preview Cover Style', 'type': 'select', 'options': ['Small', 'Full Size', 'Hidden'], 'default': 'Small' }, 'ReadChapterStyle': { 'label': 'Read Chapter Style', 'type': 'select', 'options': ['Darken Background', 'Lighten Text', 'Hide'], 'default': 'Darken Background' }, 'LeftClickMode': { 'label': 'Left Click Opens In', 'type': 'select', 'options': ['Same Window', 'New Window'], 'default': 'Same Window' }, 'CondenseElements': { 'label': 'Condense Page Elements (reduce whitespace)', 'type': 'checkbox', 'default': true }, 'CondenseFonts': { 'label': 'Adjust Font Sizes and Weights', 'type': 'checkbox', 'default': true } }, 'events': { 'open': function() { const s = GM_config.frame.style; s.inset = '100px auto auto calc(50% - 500px/2)'; s.width = '500px'; s.height = '300px'; }, 'save': function() { location.reload(); }, 'reset': function() { location.reload(); } }, 'css': '#MangaDexCondensed_header { margin-bottom: 15px !important; }' }); // Adds a style to the tags. function addGlobalStyle(css) { const head = document.getElementsByTagName('head')[0]; if (!head) { return; } const style = document.createElement('style'); style.type = 'text/css'; style.setAttribute('from', 'mdc'); style.innerHTML = css; head.appendChild(style); } // Creates the settings button. function createSettingsButton(divData, svgData) { const config = document.createElement('div'); const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); const path1 = document.createElementNS('http://www.w3.org/2000/svg', 'path'); const path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path'); // Settings gear icon uses the CC0 (public domain) license. // https://www.svgrepo.com/svg/201666/settings-gear path1.setAttribute('stroke', 'currentColor'); path1.setAttribute('stroke-width', '1'); path1.setAttribute('d', `M491.584,192.579l-55.918-6.914c-0.919-2.351-1.884-4.682-2.892-6.993l34.648-44.428 c7.227-9.267,6.412-22.464-1.899-30.773l-57.028-56.996c-8.308-8.304-21.502-9.114-30.763-1.893L333.32,79.216 c-2.312-1.008-4.644-1.974-6.994-2.894l-6.915-55.904c-1.443-11.66-11.348-20.415-23.097-20.415h-80.637 c-11.748,0-21.656,8.755-23.097,20.416l-6.914,55.904c-2.349,0.919-4.681,1.884-6.988,2.89l-44.415-34.642 c-9.261-7.222-22.458-6.414-30.768,1.894l-57.021,57.009c-8.31,8.307-9.123,21.506-1.896,30.771l34.644,44.417 c-1.012,2.312-1.978,4.647-2.9,7.002l-55.906,6.914C8.757,194.022,0,203.927,0,215.676v80.64c0,11.75,8.758,21.658,20.421,23.097 l55.901,6.903c0.919,2.352,1.884,4.686,2.894,6.994l-34.641,44.417c-7.224,9.264-6.411,22.46,1.894,30.767l57.021,57.031 c8.307,8.31,21.507,9.121,30.773,1.896l44.417-34.648c2.306,1.007,4.638,1.974,6.987,2.891l6.914,55.921 c1.441,11.66,11.348,20.416,23.097,20.416h80.637c11.748,0,21.655-8.755,23.097-20.416l6.915-55.92 c2.351-0.92,4.682-1.885,6.993-2.892l44.425,34.65c9.266,7.225,22.463,6.414,30.771-1.898l57.015-57.031 c8.307-8.308,9.117-21.504,1.893-30.768l-34.641-44.409c1.012-2.313,1.978-4.647,2.898-7.002l55.901-6.903 c11.661-1.44,20.421-11.348,20.421-23.097v-80.64C512,203.927,503.243,194.022,491.584,192.579z M465.455,275.74l-49.864,6.158 c-9.151,1.131-16.772,7.556-19.431,16.386c-2.813,9.337-6.56,18.387-11.138,26.903c-4.367,8.124-3.525,18.063,2.147,25.335 l30.898,39.613l-27.924,27.932l-39.621-30.905c-7.269-5.668-17.202-6.513-25.327-2.15c-8.513,4.572-17.565,8.319-26.905,11.134 c-8.827,2.661-15.25,10.279-16.381,19.427l-6.169,49.883h-39.492l-6.167-49.883c-1.131-9.146-7.551-16.763-16.375-19.425 c-9.367-2.825-18.417-6.571-26.899-11.132c-8.122-4.369-18.061-3.527-25.336,2.147l-39.615,30.902L93.929,390.13l30.897-39.618 c5.671-7.273,6.513-17.206,2.147-25.328c-4.568-8.501-8.315-17.554-11.137-26.911c-2.662-8.825-10.282-15.247-19.43-16.376 l-49.861-6.156v-39.492l49.866-6.167c9.146-1.131,16.763-7.551,19.423-16.375c2.824-9.356,6.572-18.406,11.143-26.9 c4.374-8.124,3.533-18.067-2.143-25.342l-30.903-39.62l27.924-27.918l39.62,30.902c7.273,5.672,17.209,6.513,25.335,2.146 c8.493-4.565,17.541-8.31,26.896-11.132c8.825-2.662,15.247-10.279,16.378-19.427l6.166-49.867h39.494l6.169,49.869 c1.133,9.148,7.557,16.767,16.384,19.427c9.328,2.811,18.379,6.557,26.902,11.135c8.122,4.364,18.055,3.522,25.325-2.149 l39.616-30.894l27.927,27.912l-30.897,39.618c-5.666,7.267-6.513,17.191-2.158,25.311c4.58,8.54,8.328,17.599,11.138,26.923 c2.661,8.825,10.279,15.248,19.427,16.381l49.878,6.169V275.74z`); path2.setAttribute('stroke', 'currentColor'); path2.setAttribute('stroke-width', '1'); path2.setAttribute('d', `M255.997,155.153c-55.606,0-100.845,45.244-100.845,100.856c0,55.603,45.239,100.839,100.845,100.839 c55.609,0,100.852-45.236,100.852-100.839C356.849,200.397,311.606,155.153,255.997,155.153z M255.997,310.303 c-29.941,0-54.3-24.356-54.3-54.294c0-29.947,24.359-54.311,54.3-54.311c29.944,0,54.306,24.363,54.306,54.311 C310.303,285.947,285.941,310.303,255.997,310.303z`); icon.classList.add('text-icon-black', "dark:text-icon-white", 'text-false', 'icon'); icon.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); icon.setAttribute('width', '24'); icon.setAttribute('height', '24'); icon.setAttribute('viewBox', '0 0 512 512'); icon.setAttribute('fill', 'black'); config.classList.add('condensed-settings'); config.addEventListener('click', function() { GM_config.open(); }); if (svgData !== undefined) { path1.setAttribute(svgData, ''); path2.setAttribute(svgData, ''); icon.setAttribute(svgData, ''); } if (divData !== undefined) { icon.setAttribute(divData, ''); config.setAttribute(divData, ''); } icon.append(path1); icon.append(path2); config.append(icon); return config; } // Helper function for finding data- attributes. const findDataAttribute = function(e) { for (const attribute of e.attributes) { if (attribute.name.startsWith('data-')) return attribute.name; return null; } } // Function for toggling a read chapter on mouse click. const toggleRead = function(ev) { //debugger; const tag = ev.target.tagName.toUpperCase(); if (['SVG', 'PATH'].includes(tag)) return; if (ev.target.classList.contains('group-tag')) return; if (ev.target.classList.contains('user-tag')) return; if (ev.target.classList.contains('pill')) return; if (ev.target.closest('.read') !== null) return; const chapter = ev.target.closest('.chapter'); if (chapter === null) return; const ind = chapter.getElementsByTagName('svg')[0]; if (ind !== undefined) ind.dispatchEvent(new MouseEvent('click')); } // Store this so when we change pages, we can disconnect it. let current_page_observers = []; let previous_pathname = ''; /////////////////////////////////////////////////////////////////////////////// function addStyles() { // Follow. { const style = ` /* Thin out the container padding. */ #__nuxt[mdcpage="follow"][mdcce="true"] .chapter-feed__container.details {padding: 0.25rem !important;} /* Adjust the location of the cover image. */ #__nuxt[mdcpage="follow"][mdccover="Small"] .chapter-feed__container.details {grid-template-columns: 41px minmax(0,1fr) !important;} #__nuxt[mdcpage="follow"][mdccover="Small"] .chapter-feed__cover {width: 41px !important; height: 53px !important; max-height: initial !important; padding-bottom: 0px !important;} #__nuxt[mdcpage="follow"][mdccover="Hidden"] .chapter-feed__container.details {grid-template-areas: "title title" "divider divider" "art list" !important;} /* Remove bolding of the chapter titles. */ /* Adjust the font size of the title. */ #__nuxt[mdcpage="follow"][mdccf="true"] .chapter-grid > div:first-child > a {font-weight: normal !important; font-size: 0.75rem !important;} `; addGlobalStyle(style); } // Title / Group. { const style = ` /* Remove the spacing and apply chapter line separators. */ #__nuxt[mdcpage="title"][mdcce="true"] .flex.flex-col.gap-2 {gap: 0rem !important;} #__nuxt[mdcpage="title"][mdcce="true"] .chapter {border-bottom: 1px solid var(--md-accent-darken) !important;} /* Remove bolding of chapter titles and adjust the font size, but leave a little bolding for unread. */ #__nuxt[mdcpage="title"][mdccf="true"] .chapter:not(.read) .chapter-grid > div:first-child > a {font-weight: 500 !important; font-size: 0.75rem !important;} #__nuxt[mdcpage="title"][mdccf="true"] .chapter.read .chapter-grid > div:first-child > a {font-weight: normal !important; font-size: 0.75rem !important;} #__nuxt[mdcpage="title"][mdccf="true"] .bg-accent.rounded-sm.read .font-bold {font-weight: normal !important;} /* Adjust line height of unread chapters. */ #__nuxt[mdcpage="title"][mdcce="true"] .chapter:not(.read) > div.chapter-grid {line-height: 1.25rem;} `; addGlobalStyle(style); } // All. { const style = ` /* Adjust the font size and styling. */ #__nuxt[mdccf="true"] .chapter-feed__title {font-size: 0.75rem !important;} #__nuxt[mdccf="true"] .chapter-grid {font-size: 0.75rem !important;} #__nuxt[mdccf="true"] .chapter-grid .font-bold {font-weight: normal !important;} /* Alter the grid spacing to give more room for the chapter name. */ #__nuxt[mdcce="true"] .chapter-grid {grid-template-columns: minmax(0,6fr) minmax(0,1fr) minmax(0,1fr) minmax(0,120px) auto auto !important;} #__nuxt[mdcce="true"] .chapter-grid {grid-template-areas: "title groups uploader timestamp views comments" !important;} #__nuxt[mdcce="true"] .chapter-grid {padding-top: 0.15rem !important; padding-bottom: 0 !important; row-gap: 0.15rem !important;} #__nuxt[mdcce="true"] .chapter-grid > [title*="comment"] {min-width: 6ch;} /* Adjust container margin to be smaller. */ #__nuxt[mdcce="true"] .chapter-feed__container.mb-4 {margin-bottom: 0.5rem !important;} /* Adjust the lift color for read chapters. */ .chapter.read .group-tag.lift:hover {background-color:var(--md-accent-darken) !important;} .chapter.read .pill.lift:hover {background-color:var(--md-accent-darken) !important;} /* Add a lift for comments. */ .chapter [title*="comment"]:hover {background-color:var(--md-accent-darken2); border-radius:0.25rem;} .dark .chapter [title*="comment"]:hover {background-color:var(--md-accent-lighten2);} .dark .chapter.read [title*="comment"]:hover {background-color:var(--md-accent-lighten);} .light .chapter.read [title*="comment"]:hover {background-color:var(--md-accent-darken);} /* Identify read chapters easier. */ /* Darken the background color. */ #__nuxt[mdcstyle="Darken Background"] .chapter.read {background-color:var(--md-accent-darken2) !important;} #__nuxt[mdcstyle="Darken Background"] .condensed-read {background-color:var(--md-accent-darken2) !important;} #__nuxt[mdcstyle="Darken Background"] .bg-accent.rounded-sm.read {background-color:var(--md-accent-darken2) !important;} .light #__nuxt[mdcstyle="Darken Background"] .chapter.read {color:#828282 !important;} .dark #__nuxt[mdcstyle="Darken Background"] .chapter.read {color:#6a6a6a !important;} /* Gray out the chapter name. */ .light #__nuxt[mdcstyle="Lighten Text"] .chapter.read {color:#b9b9b9 !important;} .dark #__nuxt[mdcstyle="Lighten Text"] .chapter.read {color:#6a6a6a !important;} /* Hide. */ #__nuxt[mdcstyle="Hide"] .chapter.read {display:none !important;} #__nuxt[mdcstyle="Hide"] .condensed-read {display:none !important;} `; addGlobalStyle(style); } } /////////////////////////////////////////////////////////////////////////////// function pageFollows() { const container_selector = '#__layout'; const config_class = 'controls'; function style() { const coverStyle = GM_config.get('CoverStyle'); const readStyle = GM_config.get('ReadChapterStyle'); const condenseElements = GM_config.get('CondenseElements'); const condenseFonts = GM_config.get('CondenseFonts'); const nuxt = document.getElementById('__nuxt'); nuxt.setAttribute('mdcpage', 'follow'); nuxt.setAttribute('mdccover', coverStyle); nuxt.setAttribute('mdcstyle', readStyle); if (condenseElements) nuxt.setAttribute('mdcce', condenseElements); if (condenseFonts) nuxt.setAttribute('mdccf', condenseFonts); } function observer() { const apply_js_cb = async function(mutationsList, observer) { const containers = document.getElementsByClassName('chapter-feed__container'); for (const container of containers) { const title = container.getElementsByClassName('chapter-feed__title')[0]; const cover = container.getElementsByClassName('chapter-feed__cover')[0]; const chapters = container.getElementsByClassName('chapter-feed__chapters')[0]; if (title && cover && chapters) { // Abort if we've already processed this title. // Our observer can get called multiple times. if (title.classList.contains('condensed-parsed')) return; const coverMode = GM_config.get('CoverMode'); const coverStyle = GM_config.get('CoverStyle'); let count = 0; let hideTimeout = 0; // If we are popping the cover out, add some grace for the hide. if (coverStyle === 'Hidden') { if (coverMode === 'Title + Cover') hideTimeout = 100; if (coverMode === 'Container') hideTimeout = 50; } const hide = function(e, t = hideTimeout) { // Compact mode doesn't show the cover. Trying to mess with it will break the page. if (container.classList.contains('compact')) return; setTimeout(function() { if (--count <= 0) { count = 0; if (coverStyle !== 'Hidden') { cover.style = ""; container.style = ""; } else { cover.style.display = "none"; chapters.style.gridColumn = "span 2 / span 2"; } } }, t); }; const show = function() { // Compact mode doesn't show the cover. Trying to mess with it will break the page. if (container.classList.contains('compact')) return; ++count; if (coverStyle !== 'Hidden') { cover.style = "width: 140px !important; height: 196px !important;"; container.style = "grid-template-columns: 140px minmax(0,1fr) !important;" } else { cover.style.display = "grid"; chapters.style.gridColumn = "span 1 / span 1"; } }; // Controls our method of showing covers. // Mouse enters: Show the cover and move the chapters over to the next column. // Mouse leaves: Hide the cover and span the chapters across the whole grid row. if (coverMode === 'Container') { container.addEventListener('mouseleave', hide); container.addEventListener('mouseenter', show); } if (coverMode === "Title" || coverMode == "Title + Cover") { title.addEventListener('mouseleave', hide); title.addEventListener('mouseenter', show); } if (coverMode === "Cover" || coverMode == "Title + Cover") { cover.addEventListener('mouseleave', hide); cover.addEventListener('mouseenter', show); } // Adding our event listeners might have triggered a weird browser issue where our mouseenter event got triggered twice. // I noticed this happens on Firefox if your mouse is already over a cover image on page load. // Set our count to 0 to allow the hide function to properly clean up. setTimeout(() => { count = 0; }, 0); // Set up default state (cover hidden). if (coverStyle === 'Hidden') { hide(undefined, 0); } // Alter functionality around the chapter title. for (const chapter of chapters.querySelectorAll('.chapter')) { const read = chapter.classList.contains('read'); // Add event to mark the chapter as read when clicked. chapter.addEventListener('click', toggleRead); chapter.addEventListener('auxclick', toggleRead); // Alter anchor target. const leftClickMode = GM_config.get('LeftClickMode'); if (leftClickMode === 'New Window') { chapter.setAttribute('target', '_blank'); } } // Mark that we've processed this title. title.classList.add('condensed-parsed'); } } }; // Used to prevent spawning a bunch of setTimeouts if we have rapid mutation callbacks. let waiting_for_timeout = false; const apply_read_cb = async function(mutationsList, observer) { // We disconnect our observer so the changes we make don't cause new mutations. if (observer !== undefined) { observer.disconnect(); observer.takeRecords(); } if (mutationsList.some((e) => e.attributeName === 'class')) { // Only process if we are waiting for a callback. if (!waiting_for_timeout) { waiting_for_timeout = true; // Set a small timeout so the changes apply before we test for read chapters. setTimeout(() => { waiting_for_timeout = false; const containers = document.getElementsByClassName('chapter-feed__container'); for (const container of containers) { const chapters = container.getElementsByClassName('chapter-feed__chapters')[0]; if (!chapters) continue; const chapter = chapters.getElementsByClassName('chapter'); // If all chapters for this title have been read, apply the condensed-read class to the container. if (Array.prototype.every.call(chapter, (e) => e.classList.contains('read'))) { container.classList.add('condensed-read'); } else { container.classList.remove('condensed-read'); } } // Reconnect our observer now that we pushed changes. try { const page_container = document.querySelector(container_selector); observer.observe(page_container, {attributes: true, subtree: true, attributeFilter: ['class']}); } catch (error) {} }, 10); } } }; try { debugger; const page_container = document.querySelector(container_selector); const chapter_observer = new MutationObserver(apply_js_cb); chapter_observer.observe(page_container, {attributes: false, childList: true, subtree: true}); const read_observer = new MutationObserver(apply_read_cb); read_observer.observe(page_container, {attributes: true, subtree: true, attributeFilter: ['class']}); current_page_observers.push(chapter_observer, read_observer); apply_js_cb(); } catch (error) {} } function addConfig() { const controls = document.getElementsByClassName(config_class)[0]; if (controls === undefined || controls.getElementsByClassName('condensed-settings').length !== 0) return; const divData = findDataAttribute(controls.firstElementChild); const svgData = findDataAttribute(controls.firstElementChild.firstElementChild); const config = createSettingsButton(divData, svgData); config.classList.add('item'); controls.append(config); } // Avoid adding tons of duplicate styles. if (current_page_observers.length === 0) { style(); observer(); addConfig(); } } /////////////////////////////////////////////////////////////////////////////// function pageTitle() { const container_selector = '#__layout'; const config_class = '.layout-container div.sm\\:ml-2 .flex.mb-6'; const config_class2 = '.layout-container div.sm\\:ml-2 .flex.mb-2'; function style() { const coverStyle = GM_config.get('CoverStyle'); const readStyle = GM_config.get('ReadChapterStyle'); const condenseElements = GM_config.get('CondenseElements'); const condenseFonts = GM_config.get('CondenseFonts'); const nuxt = document.getElementById('__nuxt'); nuxt.setAttribute('mdcpage', 'title'); nuxt.setAttribute('mdccover', coverStyle); nuxt.setAttribute('mdcstyle', readStyle); if (condenseElements) nuxt.setAttribute('mdcce', condenseElements); if (condenseFonts) nuxt.setAttribute('mdccf', condenseFonts); } function observer() { const apply_js_cb = function(mutationsList, observer) { // Try to add our settings button. addConfig(); const chapters = document.querySelectorAll('.chapter'); for (const chapter of chapters) { // Abort if we've already processed this chapter. // Our observer can get called multiple times. if (chapter.classList.contains('condensed-parsed')) return; // Mark that we've processed this chapter. chapter.classList.add('condensed-parsed'); // Put the "read" class on chapter group titles so we can gray out the group text. const read = chapter.classList.contains('read'); if (read) { // Check if we are in a group. We can test this by going to the parent and checking if we have a sibling (the title). const is_group = chapter.parentElement.previousElementSibling !== null; if (is_group) { chapter.parentElement.parentElement.classList.add('read'); } } // Alter functionality around the chapter title. // Add event to mark the chapter as read when clicked. chapter.addEventListener('click', toggleRead); chapter.addEventListener('auxclick', toggleRead); // Alter anchor target. const leftClickMode = GM_config.get('LeftClickMode'); if (leftClickMode === 'New Window') { chapter.setAttribute('target', '_blank'); } } }; try { debugger; const page_container = document.querySelector(container_selector); const chapter_observer = new MutationObserver(apply_js_cb); chapter_observer.observe(page_container, {attributes: false, childList: true, subtree: true}); current_page_observers.push(chapter_observer); } catch (error) {} } function addConfig() { let controls = document.getElementsByClassName('controls')[0]; if (controls === undefined) controls = document.querySelector(config_class); if (controls === null) controls = document.querySelector(config_class2); if (controls === undefined || controls === null || controls.getElementsByClassName('condensed-settings').length !== 0) return; // Abort if we've already added our control. if (controls.classList.contains('condensed-parsed')) return; // Mark that we've added this control. controls.classList.add('condensed-parsed'); const config = createSettingsButton(); config.classList.add('rounded', 'relative', 'md-btn', 'flex', 'items-center', 'px-3', 'justify-center', 'text-black', 'dark:text-white', 'bg-accent', 'hover:bg-accent-darken', 'active:bg-accent-darken2', 'dark:bg-accent-lighten2', 'dark:hover:bg-accent-lighten', 'dark:active:bg-accent', 'px-0'); config.style.minHeight = '48px'; config.style.minWidth = '48px'; controls.append(config); } if (current_page_observers.length === 0) { style(); observer(); addConfig(); } } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// // This is our loader. //debugger; const pageContentSelector = '#__layout'; const bootstrap_loader = function(mutationsList, observer) { console.log('[MDC] Bootstrap loader.'); observer.disconnect(); observer.takeRecords(); // Detects page changes. const page_transfer_loader = function(mutationsList, observer) { const full_location = location.pathname + location.search; if (previous_pathname === full_location) return; previous_pathname = full_location; if (current_page_observers.length !== 0) { current_page_observers.forEach((x) => { x.disconnect(); x.takeRecords(); }); current_page_observers = []; } // Choose the style function to apply. //debugger; let pageFunction = undefined; if (/\/titles\/feed/.test(full_location)) pageFunction = pageFollows; else if (/\/titles\/latest/.test(full_location)) pageFunction = pageFollows; else if (/\/my\/history/.test(full_location)) pageFunction = pageFollows; else if (/\/title\//.test(full_location)) pageFunction = pageTitle; else if (/\/group\//.test(full_location)) pageFunction = pageTitle; if (pageFunction !== undefined) { console.log(`[MDC] Page detected, calling ${pageFunction.name}.`); pageFunction(); } // observer.observe(content_container, {attributes: false, childList: true, subtree: true}); }; const content_container = document.querySelector(pageContentSelector); const content_observer = new MutationObserver(page_transfer_loader); content_observer.observe(content_container, {attributes: false, childList: true, subtree: true}); // Test for the page already being loaded. This is a race condition that could break the observer. if (content_container.hasChildNodes()) { console.log('[MDC] Page loaded, jumping to page detection.'); page_transfer_loader([], content_observer); } }; // This is the first bootstrap loader. // This will catch the main page being loaded. // At this point, we switch over to our page transfer loader which will detect page changes. window.addEventListener('load', (event) => { // Apply the styles now. They will sit for all future pages. addStyles(); const load_observer = new MutationObserver(bootstrap_loader); load_observer.observe(document.body, {attributes: false, childList: true, subtree: false}); // Test for the page already being loaded. This is a race condition that could break the observer. const content_container = document.querySelector(pageContentSelector); if (content_container != null && content_container.hasChildNodes()) { console.log('[MDC] Page loaded, jumping to bootstrap.'); bootstrap_loader([], load_observer); } }); })();