Tokopedia Select/Unselect All Wishlist

Select/unselect all Tokopedia wishlist items of a page

// ==UserScript==
// @name            Tokopedia Select/Unselect All Wishlist
// @name:id         Tokopedia Pilih Semua/Hapus Semua Pilihan  Wishlist
// @description     Select/unselect all Tokopedia wishlist items of a page
// @description:id  Pilih semua/hapus semua pilihan item wishlist Tokopedia dalam satu halaman
// @icon            https://ecs7.tokopedia.net/assets-tokopedia-lite/prod/icon512.png
// @match           https://www.tokopedia.com/*
// @grant           none
// @version         0.18
// @license         GPL-3.0-only
// @namespace       https://gitlab.com/undrilled
// @author          undrilled
// ==/UserScript==

(function() {
  'use strict';

  // Injection retry duration
  const RUN_INTERVAL = 250;
  // Browser path check
  const PATH_CHECK_REGEX = '^/wishlist/(all|collection/.+)';
  // Select all button properties
  const SELECT_ALL_BUTTON_CLASS_NAME = 'undrilled_select_all_btn';
  const UNSELECT_ALL_BUTTON_CLASS_NAME = 'undrilled_unselect_all_btn';
  const SELECT_ALL_BUTTON_LABEL = 'Centang semua';
  const UNSELECT_ALL_BUTTON_LABEL = 'Hapus centang';
  // Selector targets
  const MANAGE_LABEL = 'Atur';
  const CANCEL_LABEL = 'Batal';
  const MANAGE_SELECTOR = '.manage';
  const PRODUCTS_SELECTOR = '.content__grid .product';
  const PRODUCT_TARGET_SELECTOR = '.target';

  const NOT_READY_MESSAGE = 'Manage button is not yet loaded. Retrying Select All Wishlist injection in ' + RUN_INTERVAL + 'ms.';

  let documentUrl = '';
  let observer = null;

  const isCorrectPage = () => {
    return !! window.location.pathname.match(PATH_CHECK_REGEX);
  }
  const showIsNotReadyMessage = () => {
    console.info(NOT_READY_MESSAGE);
  }
  const getManageButtonContainer = () => {
    return document.querySelector(MANAGE_SELECTOR) ?? null;
  }
  const getProducts = () => {
    return document.querySelectorAll(PRODUCTS_SELECTOR) ?? null;
  }
  const isProductSelected = (product) => {
    return product.querySelector('input').checked;
  }
  const getProductTarget = (product) => {
    return product.querySelector(PRODUCT_TARGET_SELECTOR) ?? null;
  }
  const getAnyManageButton = () => {
    const btnEl = getManageButtonContainer()?.querySelectorAll('button');
    if (!btnEl) {
      return null;
    }
    return Array
            .from(btnEl)
            .find(el => (el.innerText === MANAGE_LABEL || el.innerText === CANCEL_LABEL)) ?? null;
  }
  const getManageButton = () => {
    const el = getAnyManageButton();
    return el?.innerText === MANAGE_LABEL ? el : null;
  }
  const getCancelButton = () => {
    const el = getAnyManageButton();
    return el?.innerText === CANCEL_LABEL ? el : null;
  }
  const getSelectAllButton = () => {
    return document.querySelector('.' + SELECT_ALL_BUTTON_CLASS_NAME);
  }
  const getUnselectAllButton = () => {
    return document.querySelector('.' + UNSELECT_ALL_BUTTON_CLASS_NAME);
  }
  const isAnyManageButtonReady = () => {
    return !! getAnyManageButton();
  }
  const isManageButtonReady = () => {
    return !! getManageButton();
  }
  const isCancelButtonReady = () => {
    return !! getCancelButton();
  }
  const isInjected = () => {
    return !! observer;
  }

  const handleSelectAll = () => {
    getProducts().forEach(el => {
      if (isProductSelected(el)) {
        return;
      }
      getProductTarget(el).click();
    });
  }
  const handleClearSelection = () => {
    getProducts().forEach(el => {
      if (!isProductSelected(el)) {
        return;
      }
      getProductTarget(el).click();
    })
  }
  const inject = () => {
    const manageEl = getAnyManageButton();
    const newButtons = [
      {
        label: SELECT_ALL_BUTTON_LABEL,
        class: SELECT_ALL_BUTTON_CLASS_NAME,
        event: handleSelectAll
      },
      {
        label: UNSELECT_ALL_BUTTON_LABEL,
        class: UNSELECT_ALL_BUTTON_CLASS_NAME,
        event: handleClearSelection,
      }
    ];
    for (const x of newButtons) {
      const el = manageEl.cloneNode(true);
      el.classList.add(x.class);
      el.innerText = x.label;
      el.addEventListener('click', x.event);
      getManageButtonContainer().insertBefore(el, manageEl);
    }
  }
  const removeSelectAll = () => {
    const selectAllButton = getSelectAllButton();
    if (!selectAllButton) {
      return;
    }
    getManageButtonContainer()?.removeChild(selectAllButton);
  }
  const removeUnselectAll = () => {
    const unselectAllButton = getUnselectAllButton();
    if (!unselectAllButton) {
      return;
    }
    getManageButtonContainer()?.removeChild(unselectAllButton);
  }
  const removeAll = () => {
    removeSelectAll();
    removeUnselectAll();
  }

  const addHookManageChange = (fn) => {
    observer = new MutationObserver(fn);
    observer.observe(getAnyManageButton(), {
      characterData: true,
      subtree: true
    });
    // Run it once
    fn();
  }
  const removeHookManageChange = (fn) => {
    if (!observer) {
      return;
    }
    fn();
    observer.disconnect();
    observer = null;
  }

  const patchPathChange = () => {
    const handlePathChange = () => {
      requestAnimationFrame(() => {
        if (documentUrl !== location.href) {
          documentUrl = location.href;
          removeHookManageChange(() => {
            removeAll();
          });
          if (!isCorrectPage()) {
            return;
          }
          setup();
        }
      });
    }
    document.addEventListener('click', handlePathChange);
    document.addEventListener('keyup', handlePathChange);
  }

  const setup = () => {
    const intervalId = setInterval(() => {
      if (!isAnyManageButtonReady()) {
        showIsNotReadyMessage();
        return;
      }
      if (isInjected()) {
        clearInterval(intervalId);
        return;
      }

      addHookManageChange(() => {
        if (!isCancelButtonReady()) {
          removeAll();
          return;
        }
        inject();
      });

      clearInterval(intervalId);
    }, RUN_INTERVAL);
  }

  setup();
  patchPathChange();
})();