Fanatical bundles carousel enhancer

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.

As of 2024-07-08. See the latest version.

// ==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
  */
})();
长期地址
遇到问题?请前往 GitHub 提 Issues。