NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Fanatical bundles carousel enhancer // @version 2025-04-06 // @namespace Jakub Marcinkowski // @description In all Fanatical Build Your Own bundles, adds "Add to bundle" button to the carousel - like in game bundles. It is handy to have arrows, to browse products, and button to add to cart close by. Also marks a tile of the product, currently shown in the carousel. In bundles with tiers, adds tier info to the carousel. All bundles: move carousel on top and use mouse wheel horizontal scroll to switch next/prev product. // @author Jakub Marcinkowski <kuba.marcinkowski on g mail> // @copyright 2023+, Jakub Marcinkowski <kuba.marcinkowski on g mail> // @license Zlib // @homepageURL https://gist.github.com/JakubMarcinkowski // @homepageURL https://github.com/JakubMarcinkowski // @updateURL https://openuserjs.org/meta/JakubMarcinkowski/Fanatical_bundles_carousel_enhancer.meta.js // @downloadURL https://openuserjs.org/install/JakubMarcinkowski/Fanatical_bundles_carousel_enhancer.user.js // @match https://*.fanatical.com/* // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii01MCAtNTAgMTAwIDEwMCIgY2xpcC1wYXRoPSJjaXJjbGUoMTAwJSkiPjxjaXJjbGUgcj0iNTAiIGZpbGw9IiNmOTAiLz48cGF0aCBkPSJNLTUwLC0yNUgyOUwyMywtOUgtNTBNMjAsLTNILTE5VjM2TDAsMTJIMTMiIGZpbGw9IiNmZmYiLz48L3N2Zz4 // @run-at document-body // ==/UserScript== (function() { 'use strict'; let carousel, tileCard, tileTarget, carouselTarget, button, buttonDummy, tilesTitles, carouselTitle, observerAddRem, markOnly; const observerInitial = new MutationObserver(() => { const contentElem = document.getElementsByClassName('content')[0]; if (!contentElem) return; if (contentElem.parentElement.parentElement.id !== 'root') return; observerInitial.disconnect(); observePageChange(contentElem); }); observerInitial.observe(document.body, {childList: true, subtree: true}); function observePageChange(elem) { const observerPage = new MutationObserver((mutationsList) => { const addedBundle = mutationsList .find((mutation) => [...mutation.addedNodes].find(checkIfBundle)); if (addedBundle) { carousel = document.querySelector('section.bundle-carousel'); if (carousel) { carouselOnTop(); carousel.addEventListener('wheel', wheelSwitch, {passive: false}); carouselTitle = carousel.getElementsByClassName('product-name')[0].firstChild; // text node carouselTarget = carousel.getElementsByClassName('right-column')[0]; if (document.querySelector('main.PickAndMixProductPage')) { markOnly = !!document.querySelector('.bundle-carousel .pnm-add-btn'); // Add button exists in game bundles by default tilesTitles = [...document.querySelectorAll('h2.card-product-name')]; startButtonCopy(); } else if (document.getElementsByClassName('tier-title').length !== 0) { // Bundle with tiers tilesTitles = [...document.querySelectorAll('h3.card-product-name')]; startTierCopy(); } } return; } const removedBundle = mutationsList .find((mutation) => [...mutation.removedNodes].find(checkIfBundle)); if (removedBundle) { if (observerAddRem) observerAddRem.disconnect(); if (tileCard) removeListeners(tileCard); } }); observerPage.observe(elem, {childList: true}); } function checkIfBundle(node) { return node.tagName && node.tagName === 'MAIN' && (node.classList.contains('PickAndMixProductPage') || node.classList.contains('bundle-page')); } function carouselOnTop() { unwrapCarousel(carousel); unwrapCarousel(carousel.parentElement); const bgContrast = document.querySelector('[class$="backgroundContrast"]'); if (bgContrast) carousel.parentElement.before(bgContrast); } function unwrapCarousel(relElem) { while (relElem.previousElementSibling) { carousel.after(relElem.previousElementSibling); } } function startButtonCopy() { moveTheButton(); const observerTitle = new MutationObserver(moveTheButton); observerTitle.observe(carouselTitle, {characterData: true}); if (markOnly) return; observerAddRem = new MutationObserver((mutationsList) => { if (mutationsList[0].target && mutationsList[0].target.tagName === 'A' || !buttonDummy ) return; mutationsList.forEach((mutation) => { if (mutation.target.tagName !== 'BUTTON') return; const buttonDummy2 = button.cloneNode(true); buttonDummy.replaceWith(buttonDummy2); buttonDummy = buttonDummy2; }); }); observerAddRem.observe( document.querySelector('div.PickAndMixProductPage__content.container > section'), {subtree: true, attributeFilter: ["class"]} ); } function moveTheButton() { if (tileCard) { // Not on the first run moveToTile(); if (!markOnly) removeListeners(tileCard); } tileCard = tilesTitles .find((tile) => tile.textContent === carouselTitle.nodeValue) .closest('article'); tileTarget = tileCard.querySelector('.PickAndMixCard__addToBundle > div'); button = tileTarget.getElementsByTagName('button')[0]; moveToCarousel(); if (!markOnly) addListeners(tileCard); } function moveToTile(event) { tileCard.parentElement.classList.remove('fbce-in-carousel'); if (markOnly) return; tileTarget.append(button); if (buttonDummy) buttonDummy.remove(); buttonDummy = button.cloneNode(true); carouselTarget.prepend(buttonDummy); } function moveToCarousel(event) { tileCard.parentElement.classList.add('fbce-in-carousel'); if (markOnly) return; carouselTarget.prepend(button); if (buttonDummy) buttonDummy.remove(); if (!event) buttonDummy = button.cloneNode(true); tileTarget.append(buttonDummy); } function removeListeners(node) { node.removeEventListener('mouseenter', moveToTile); node.removeEventListener('mouseleave', moveToCarousel); } function addListeners(node) { node.addEventListener('mouseenter', moveToTile); node.addEventListener('mouseleave', moveToCarousel); } function startTierCopy() { const container = document.createElement('div'); container.className = 'fbce-tierInfo'; carouselTarget.prepend(container); carouselTarget = container; copyTierInfo(); const observerTitle = new MutationObserver(copyTierInfo); observerTitle.observe(carouselTitle, {characterData: true}); } function copyTierInfo() { const tileCard = tilesTitles .find((tile) => tile.textContent === carouselTitle.nodeValue) .closest('article') .closest('div'); const tierElem = tileCard.closest('.tier'); const tierTiles = tierElem.querySelectorAll(':scope > div > div > div'); const tierCount = tierTiles.length; const tierIndex = [...tierTiles] .findIndex((tile) => tile === tileCard); let string = [...tierElem.children[0].childNodes] .reduce((str,node) => { return str += node.nodeType === Node.TEXT_NODE ? node.textContent : '' }, ''); string += tierElem.children[0].firstElementChild.textContent; carouselTarget.replaceChildren( string, document.createElement('br'), `${tierIndex + 1}/${tierCount}` ); } function wheelSwitch(e) { if (!e.cancelable || e.deltaY !== 0) return; if (e.deltaX > 0) { document.querySelector('button.carousel-button[aria-label="Next"]').click(); } else if (e.deltaX < 0) { document.querySelector('button.carousel-button[aria-label="Previous"]').click(); } } const styleSheet = new CSSStyleSheet(); document.adoptedStyleSheets.push(styleSheet); styleSheet.replaceSync(` .right-column > button { float: right; padding: 6px; margin-left: 0.3rem; margin-bottom: 1rem; } .right-column > div.fbce-tierInfo { text-align: right; margin-bottom: .5rem; } h4.mb-3 + .overview-container {clear: both;} section.bundle-carousel {padding-top: 1px;} #carousel-content {padding: 1rem;} .PickAndMixProductPage__content {padding-top: 0 !important;} .fbce-in-carousel {scale: 1.1;} .fbce-in-carousel > article {background-color: dimgrey;} .fbce-in-carousel .PickAndMixCard__bottomRowIcons * {color: bisque !important;} article.left-column > div.product-details { /* Fix. BYO Fantasy Game Assets Bundle had Pixelart Fonts Asset packs. */ /* Description contained "supported characters", which swelled container. */ word-break: break-word; } :root { /* Fix. Sometimes fanatical have unnecesary horizontal scrollbar. */ margin-left: -1.1rem; } `); /* Tested: https://www.fanatical.com/en/pick-and-mix/essential-game-music-build-your-own-bundle - audio https://www.fanatical.com/en/pick-and-mix/ultimate-machine-learning-and-ai-build-your-own-bundle - ebook https://www.fanatical.com/en/pick-and-mix/build-your-own-tabletop-wargame-bundle - games, already has Add https://www.fanatical.com/en/pick-and-mix/new-skills-new-you-build-your-own-bundle - elearning https://www.fanatical.com/en/pick-and-mix/build-your-own-fantasy-game-assets-bundle - mixed audio + graphics */ })();