|
// ==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://gist.github.com/JakubMarcinkowski/098b8913527a41f1d842b10b38f7a58b/raw/fanatical-bundles-carousel-enhancer.user.meta.js |
|
// @downloadURL https://gist.github.com/JakubMarcinkowski/098b8913527a41f1d842b10b38f7a58b/raw/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 |
|
*/ |
|
})(); |
Would it be possible to add direct link to gg.deals website? :)