Fanatical Build Your Own Bundles with games, has "Add to bundle" button on the carousel. This script adds this to all BYO bundles. Also moves carousel on top. It is handy to have arrows to browse products and button to add to cart close by.
目前為
// ==UserScript==
// @name Fanatical bundles carousel enhancer
// @version 2024.7.8
// @namespace Jakub Marcinkowski
// @description Fanatical Build Your Own Bundles with games, has "Add to bundle" button on the carousel. This script adds this to all BYO bundles. Also moves carousel on top. It is handy to have arrows to browse products and button to add to cart close by.
// @author Jakub Marcinkowski <kuba.marcinkowski on g mail>
// @copyright 2024+, Jakub Marcinkowski <kuba.marcinkowski on g mail>
// @license Zlib
// @homepageURL https://gist.github.com/JakubMarcinkowski
// @homepageURL https://github.com/JakubMarcinkowski
// @match https://*.fanatical.com/*
// @icon https://cdn.fanatical.com/production/icons/favicon.ico
// @run-at document-body
// ==/UserScript==
(function() {
'use strict';
let listening = new Set();
let carousel, tileCard, tileTarget, carouselTarget, button, buttonDummy, tilesTitles, carouselTitle, observerAddRem;
const observerInitial = new MutationObserver(function(mutationsList) {
const contentElem = document.getElementsByClassName('content')[0];
if (!contentElem) return;
if (contentElem.parentElement.parentElement.id !== 'root') return;
observerInitial.disconnect();
// carousel = document.querySelector('section.bundle-carousel');
// if (carousel) handleCarousel();
observePageChange(contentElem);
});
observerInitial.observe(document.body, {childList: true, subtree: true});
function observePageChange(elem) {
const observerPage = new MutationObserver(function(mutationsList) {
const addedBundle = mutationsList.find(
mutation =>
[...mutation.addedNodes].find(checkIfBundle)
);
if (addedBundle) {
carousel = document.querySelector('section.bundle-carousel');
handleCarousel();
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 handleCarousel() {
if (!carousel) return;
// Carousel on top
unwrap(carousel);
unwrap(carousel.parentElement);
const bgContrast = document.querySelector('[class$="backgroundContrast"]');
if (bgContrast) carousel.parentElement.before(bgContrast);
if (document.querySelector('main.bundle-page')) return; // Not a BYOB
if (document.querySelector('.bundle-carousel .pnm-add-btn')) return; // Add buttons exists in game bundles by default
if (document.querySelector('main.PickAndMixProductPage')) addButton();
}
function unwrap(relElem) {
while (relElem.previousElementSibling) {
carousel.after(relElem.previousElementSibling);
}
}
function addButton() {
tilesTitles = [...document.querySelectorAll('h2.card-product-name')];
carouselTitle = carousel.querySelector('.product-name').firstChild; // text node
moveTheButton();
const observerTitle = new MutationObserver(moveTheButton);
observerTitle.observe(carouselTitle, {characterData: true});
observerAddRem = new MutationObserver(function(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() {
carouselTarget = document.querySelector('h4.mb-3'); // On top of right pane in carousel.
if (tileCard) { // Not on first run
moveToTile();
removeListeners(tileCard);
}
tileCard = tilesTitles
.find(x => x.textContent === carouselTitle.nodeValue)
.closest('article');
tileTarget = tileCard.querySelector('.PickAndMixCard__addToBundle > div');
button = tileTarget.querySelector('button');
moveToCarousel();
addListeners(tileCard);
}
function moveToTile(event) {
tileTarget.append(button);
if (buttonDummy) buttonDummy.remove();
buttonDummy = button.cloneNode(true);
carouselTarget.prepend(buttonDummy);
}
function moveToCarousel(event) {
carouselTarget.prepend(button);
if (buttonDummy) buttonDummy.remove();
if (!event) buttonDummy = button.cloneNode(true);
tileTarget.append(buttonDummy);
}
function removeListeners(node) {
listening.delete(node);
node.removeEventListener('mouseenter', moveToTile);
node.removeEventListener('mouseleave', moveToCarousel);
}
function addListeners(node) {
listening.add(node);
node.addEventListener('mouseenter', moveToTile);
node.addEventListener('mouseleave', moveToCarousel);
}
const styleSheet = document.head.appendChild(document.createElement('style')).sheet;
function addStyleRules(rules) {
rules.forEach(rule => styleSheet.insertRule(rule));
}
addStyleRules([
`h4.mb-3 > button {
float: right;
padding: 6px;
margin-left: 0.3rem;
margin-bottom: 1rem;
}`
,`h4.mb-3 + .overview-container {
clear: both;
}`
,`section.bundle-carousel {
padding-top: 1px;
}`
,`#carousel-content {
padding: 1rem;
}`
,`.PickAndMixProductPage__content {
padding-top: 0 !important;
}`
,`:root {
margin-left: -1.1rem;
}` // Fix. Sometimes fanatical have unnecesary horizontal scrollbar
]);
/* 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
*/
})();