TedCart / BeastSaberFilterHelper

// ==UserScript==
// @namespace    https://openuserjs.org/users/TedCart
// @name         BeastSaberFilterHelper
// @copyright    2020, TedCart (https://openuserjs.org/users/TedCart)
// @version      1.0
// @license      MIT
// @description  Help filter by difficulty or author while browsing songs on bsaber.com
// @author       You
// @match        https://bsaber.com/newest/*
// @match        https://bsaber.com/curator-recommended/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

// ==OpenUserJS==
// @author       TedCart
// ==/OpenUserJS==

// link to source:
// https://openuserjs.org/scripts/TedCart/BeastSaberFilterHelper/source

(function () {
  'use strict';

  // import { mainBack, mainFront, specialBack, specialFront } from "./colors.js"

  const addCustomStyleTag = () => {
    const newStyle = document.createElement("style");
    newStyle.setAttribute('type', 'text/css');
    // classToColor background, color, padding, margin
    newStyle.innerHTML = `
    /* DO NOT EDIT BELOW THIS POINT */
    /* BEGIN CUSTOM CSS */
body {
  margin-top: 0;
}

.post-content > .row.row-fluid {
  display: flex;
  flex-direction: column;
  align-items: center;
}

#restore-mapper-button-list {
  max-width: 200px;
}

#restore-mapper-button-list button {
  display: block;
  margin: 4px 0 4px 28px;
}

/* END CUSTOM CSS */
  `;
    document.head.appendChild(newStyle);
  };

  const selectors
    = { singlePost:     '.mapper_id.vcard'
      , postContentSelector: 'table tr:nth-of-type(2) td:nth-of-type(2)'
      , parentNodeCount: 5
      };

  function elementIsHidden(el) {
    return (el.offsetParent === null)
  }

  function getCurrentElement (elementList) {
    if (!elementList) return
    const positionArray = [];
    elementList.forEach(e => positionArray.push(e.getBoundingClientRect().top));

    let targetIndex = positionArray.findIndex(e => e > -10);
    if (targetIndex === -1) return
    return elementList[targetIndex]
  } // end of scrollToPreviousElement

  function screenIsScrolledToTop () {
    const body = document.querySelector('body');

    return body
      ? (body.getBoundingClientRect().top > -10) // within 10 pixels of the top
      : undefined
  }

  function screenIsScrolledToBottom () {
    const html = document.querySelector('html');
    const body = document.querySelector('body');

    // html.clientHeight is the height of the visible window
    // body.clientHeight is the height of all elements combined

    return (html && body)
      ? ((body.clientHeight + body.getBoundingClientRect().top) < (html.clientHeight + 5))
      : undefined
  }

  function elementScrollJump (ev) {
    if (document.activeElement.tagName === "INPUT") return
    if (document.activeElement.tagName === "TEXTAREA") return

    const upList
      = [ 38 // up arrow
        , 87 // w key
        ];
    const downList
      = [ 40 // down arrow
        , 83 // s key
        ];
    const clickList
      = [ 39 // right arrow
        , 68 // d key
        ];

    const activeKeyCode = ev.keyCode;

    if  (!activeKeyCode
        || [...upList, ...downList, ...clickList].indexOf(activeKeyCode) === -1
        ) { return }

    const postList = [];
    const postElements = document.querySelectorAll(selectors.singlePost);
    postElements.forEach(e => {if (!elementIsHidden(e)) {postList.push(e);}});

    if (clickList.indexOf(activeKeyCode) !== -1 && selectors.linkSelector) {
      const targetEl = getCurrentElement(postList);
      let linkElement = targetEl.querySelector(selectors.linkSelector).parentNode;
      if (!linkElement) return
      linkElement.setAttribute('target', '_blank');
      linkElement.click();
      return
    }

    ev.preventDefault();

    if (upList.indexOf(activeKeyCode) !== -1) {
      // console.log("Scrolling to previous element...")
      scrollToPreviousElement(postList);
    } else if (downList.indexOf(activeKeyCode) !== -1) {
      // console.log("Scrolling to next element...")
      scrollToNextElement(postList);
    }

  } // end of elementScrollJump

  function scrollToPreviousElement (elementList) {
    if (!elementList) return
    const positionArray = [];
    let targetIndex = elementList.length - 1;

    // if you're at the very top (within 10 pixels), go the very bottom
    if (screenIsScrolledToTop()) {
      window.scrollTo(0,document.body.scrollHeight);
    } else if (screenIsScrolledToBottom()) {
      // if you're at the very bottom (within 5 pixels), go to last element (or the very top)
      elementList[elementList.length - 1]
        ? elementList[elementList.length - 1].scrollIntoView()
        : window.scrollTo(0,0);
    } else {
      elementList.forEach(e => positionArray.push(e.getBoundingClientRect().top));
      targetIndex = positionArray.findIndex(e => e > -10);

      if (targetIndex === 0) {
        window.scrollTo(0,0);
      } else if (targetIndex >= 1) {
        elementList[targetIndex - 1].scrollIntoView();
      }
    } // end else

    return elementList[targetIndex]
  } // end of scrollToPreviousElement

  function scrollToNextElement (elementList) {
    if (!elementList) return

    const positionArray = [];
    let targetIndex = 0;

    // if you're at the very bottom (within 5 pixels), go the very top
    if (screenIsScrolledToBottom()) {
      window.scrollTo(0,0);
    } else if (screenIsScrolledToTop()) {
      // if you're at the very top (within 10 pixels), go the first element (or the very bottom)
      elementList[0]
        ? elementList[0].scrollIntoView()
        : window.scrollTo(0,document.body.scrollHeight);
    } else {
      elementList.forEach(e => positionArray.push(e.getBoundingClientRect().top));
      targetIndex = positionArray.findIndex(e => e > 10);
      if (targetIndex === -1) {
        window.scrollTo(0,document.body.scrollHeight);
      } else {
        elementList[targetIndex].scrollIntoView();
      }

    } // end else

    return elementList[targetIndex]
  } // end of scrollToNextElement

  // import { addCustomModalStyleTag } from "./_styles.js"

  const modalBlockId = "draggable-modal-block";

  const collapsingArrowSvg
    = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" width="17px" height="17px">
      <g fill="none" fill-rule="evenodd">
        <path d="M3.29175 4.793c-.389.392-.389 1.027 0 1.419l2.939 2.965c.218.215.5.322.779.322s.556-.107.769-.322l2.93-2.955c.388-.392.388-1.027 0-1.419-.389-.392-1.018-.392-1.406 0l-2.298 2.317-2.307-2.327c-.194-.195-.449-.293-.703-.293-.255 0-.51.098-.703.293z" fill="#FFFFFF">
        </path>
      </g>
    </svg>`;

  function getMainModalElement () {
    return document.querySelector(`#${modalBlockId}`)
  }

  const maxListHeight
    = Math.max( Math.floor(window.innerHeight / 2)
              , 200
              );

  function createModalBlock (startPositionOptions) {

    addCustomModalStyleTag(startPositionOptions);
    const modalBlock = document.createElement('div');
    modalBlock.setAttribute('id', modalBlockId);
    if (startPositionOptions.top) {
      modalBlock.style.top = `${startPositionOptions.top}`;
    }
    if (startPositionOptions.left) {
      modalBlock.style.left = `${startPositionOptions.left}`;
    }
    // modalBlock.setAttribute('title', "Click and hold to drag...")

    document.querySelector('body').prepend(modalBlock);

    modalBlock.onmousedown = moveModalContainer;
    modalBlock.ondragstart = () => false;

    // This function came from https://javascript.info/mouse-drag-and-drop
    function moveModalContainer (event) {

      if (event.target.tagName === "INPUT") return
      if (event.target.tagName === "BUTTON") return
      if (event.target.tagName === "SPAN") return
      if (event.target.tagName === "SECTION") return
      if (event.target.tagName === "LABEL") return
      if (event.target.tagName === "SVG") return
      if (event.target.tagName === "G") return
      if (event.target.tagName === "PATH") return
      // if (event.target.tagName === "LI") return

      // const modalBlock = document.querySelector(`#${modalBlockId}`)
      // if (!modalBlock) return

      const top = document.querySelector('html').scrollTop;

      let shiftX = event.clientX - modalBlock.getBoundingClientRect().left;
      let shiftY = event.clientY - modalBlock.getBoundingClientRect().top;
      modalBlock.style.position = 'fixed';
      modalBlock.style.zIndex = 1000;
      document.body.append(modalBlock);

      moveAt(event.pageX, event.pageY);

      // moves the modalBlock at (pageX, pageY) coordinates
      // taking initial shifts into account
      function moveAt(pageX, pageY) {
        modalBlock.style.left = pageX - shiftX + 'px';
        modalBlock.style.top = (pageY - top) - shiftY + 'px';
      }

      function onMouseMove(event) {
        moveAt(event.pageX, event.pageY);
      }

      // move the modalBlock on mousemove
      document.addEventListener('mousemove', onMouseMove);

      // drop the modalBlock, remove unneeded handlers
      modalBlock.onmouseup = function() {
        document.removeEventListener('mousemove', onMouseMove);
        modalBlock.onmouseup = null;
      };

    }; // end moveModalContainer

    // return modalBlock
  }

  function createCollapsibleSectionContainer (sectionTitle, idPrefix) {
    const el = getMainModalElement();
    if (!el) return

    const firstAttempt = el.querySelector(`#${idPrefix}-filter-container`);
    if (firstAttempt) return firstAttempt

    function toggleSectionVisibility (ev) {
      const clickableDiv = ev.target.closest(`#${idPrefix}-filter-container > .section-header`);
      if (!clickableDiv) return;
      ev.preventDefault();
      const containerDiv = document.querySelector(`#${idPrefix}-filter-container`);
      const isOpen = /open/.test(containerDiv.className);

      if (isOpen) {
        containerDiv.classList.remove('open');
        localStorage.setItem(`${idPrefix}IsHidden`, 'true');
      } else {
        containerDiv.classList.add('open');
        localStorage.setItem(`${idPrefix}IsHidden`, 'false');
      }
    } // end toggleSectionVisibility

    // Putting this click listener on the document is (ironically) better for performance
    // https://gomakethings.com/detecting-click-events-on-svgs-with-vanilla-js-event-delegation/
    document.addEventListener('click', toggleSectionVisibility);

    const sectionContainerDiv = document.createElement('section');
    sectionContainerDiv.setAttribute('id',`${idPrefix}-filter-container`);
    if (!JSON.parse(localStorage.getItem(`${idPrefix}IsHidden`) || 'false')) {
      sectionContainerDiv.setAttribute('class','open');
    }

    const sectionHeaderDiv = document.createElement('section');
    sectionHeaderDiv.setAttribute('class','section-header');

    const toggleArrowDiv = document.createElement('section');
    toggleArrowDiv.setAttribute('class','toggle-arrow');
    toggleArrowDiv.innerHTML = collapsingArrowSvg;
    sectionHeaderDiv.append(toggleArrowDiv);

    const sectionHeaderText = document.createElement('span');
    sectionHeaderText.setAttribute('class','modal-section-header');
    sectionHeaderText.innerText = sectionTitle;
    sectionHeaderDiv.append(sectionHeaderText);

    sectionContainerDiv.append(sectionHeaderDiv);
    el.append(sectionContainerDiv);

    return sectionContainerDiv
  } // end createCollapsibleSectionContainer

  function addCustomModalStyleTag (startPositionOptions={}) {
    const newStyle = document.createElement("style");
    newStyle.setAttribute('type', 'text/css');
    // classToColor background, color, padding, margin
    if (  !startPositionOptions.top
      &&  !startPositionOptions.left) {
        startPositionOptions.top = "40px";
        startPositionOptions.left = "10px";
    }
    newStyle.innerHTML = `
    svg { pointer-events: none; }
    .modal-button {
      text-align: center;
      margin:auto;
      display: block;
      min-width: 7em;
      border: 1px solid #777;
      padding: 3px 5px;
      border-radius: 5px;
    }
    .modal-section-header {
      display: inline-block;
      cursor: pointer;
      font-size: 1.2em;
      user-select: none;
    }
    .hide-button {
      margin-bottom:5px;
      /* background: #DDD; */
      /* color:#222; */
      border: 1px solid #777;
      border-radius: 2px;
      padding: 3px;
    }
    .live-button {
      margin-bottom:5px;
      font-weight: 800;
      font-size: 107%;
      border: 1px solid #777;
      border-radius: 3px;
      padding: 3px;
      color: #EEE;
      background: #2762a6;
    }
    .modal-input-list-button {
      background: #000;
      color: #DDD;
    }
    #draggable-modal-block {
      position: fixed;
      ${startPositionOptions.top ? "top: " + startPositionOptions.top + ";" : ''}
      ${startPositionOptions.left ? "left: " + startPositionOptions.left + ";" : ''}
      padding: 15px 5px;
      min-height: 20px;
      min-width: 20px;
      background: #333333BB;
      color: #DDD;
      z-index: 1000;
      border: solid transparent 1px;
      border-radius: 8px;
      transition: 500ms;
      opacity: .2;
    }
    #draggable-modal-block:hover {
      background: #333333BB;
      transition: 0ms;
      opacity: 1;
    }
    .deactivated-filters#draggable-modal-block button,
    .deactivated-filters#draggable-modal-block section {
      display: none;
    }
    .deactivated-filters#draggable-modal-block button:nth-child(1) {
      display: block;
    }
    .input-modal-checkbox {
      display:inline-block;
      margin-left: 25px;
    }

    .modal-input-list label {
      display: inline-block;
      margin: 0 0 0 8px;
      font-size: 1em;
      transition: 250ms;
      user-select: none;
    }
    .modal-input-list label:hover,
    .modal-input-list li:hover label {
      color: #FFF;
      transition: 50ms;
    }
    .modal-input-list li { margin: 0; }

    section.section-header {
      display: flex;
    }

    .toggle-arrow {
      display: flex;
      justify-content: space-around;
      align-items: center;
      height: 25px;
      width:  25px;
      border-radius: 2px;
      margin: 0;
      transition: 300 ms;
      cursor: pointer;
      /* transform: rotate(-90deg); */
      opacity: .8;
    }
    .toggle-arrow:hover {
      opacity: 1;
      background: black;
    }

    .toggle-arrow svg       { transform: rotate(-90deg); transition: 300ms; }
    .open .toggle-arrow svg { transform: rotate(0deg); }

    .modal-input-list {
      opacity: 0;
      display: none;
      visibility: hidden;
      transition: visibility 0s lineaer 0.1s, opacity 0.3s ease;
      padding: 0;
      margin: 0;
      max-height: ${maxListHeight}px;
      overflow-y: auto;
    }
    .open .modal-input-list {
      display: block;
      visibility: visible;
      opacity: 1;
      transition-delay: 0s;
    }
    .modal-input-list li {
      display: block;
    }
  `;
    document.head.appendChild(newStyle);
  }

  function createCheckboxWithLabel (options) {
    /*  options example:
         { id: "modal-checkbox-example-id"
         , label: "Example Checkbox"
         , checked: true }
    */
    options = options || {};
    const modalBlock = getMainModalElement();
    if (!modalBlock) return []

    let newCheckbox;
    if (options && options.id) newCheckbox = modalBlock.querySelector(`#${options.id}`);
    if (newCheckbox) {
      console.log("not creating duplicate input", options.id);
      return [] // don't create duplicates
    }
    newCheckbox = document.createElement('input');
    newCheckbox.setAttribute('type',`checkbox`);
    newCheckbox.setAttribute('class',`input-modal-checkbox`);
    newCheckbox.oninput = function(e) {
      this.setAttribute('value', newCheckbox.checked);
      this.blur();
    };

    for (const key in options) {
      if (key === 'label') continue
      if (key === 'checked') {
        newCheckbox.checked = !!options[key];
      } else {
        newCheckbox.setAttribute(key, options[key]);
      }
    } // end for loop

    const newLabel = document.createElement('label');
    newLabel.innerText = options.label;
    // newLabel.onclick = function () { newCheckbox.checked = !newCheckbox.checked  }
    newLabel.onclick = function () { newCheckbox.click();  };
    return [newCheckbox, newLabel]
  }

  function createNewCheckboxListItem (el, options) {
    const newListItem = document.createElement('li');
    const newCheckboxElements = createCheckboxWithLabel(options);
    if (newCheckboxElements.length > 0) {
      newCheckboxElements.forEach(el => newListItem.append(el));
      el.append(newListItem);
    }
  }

  function createSpan (options) {
    /*  options example:
         { id: "modal-checkbox-example-id"
         , label: "Example Checkbox"
         , checked: true }
    */
    options = options || {};
    const modalBlock = getMainModalElement();
    if (!modalBlock) return

    let newSpan;
    if (options && options.id) newSpan = modalBlock.querySelector(`#${options.id}`);
    if (newSpan) {
      console.log("not creating duplicate span", options.id);
      return // don't create duplicates
    }
    newSpan = document.createElement('span');

    for (const key in options) {
      newSpan.setAttribute(key, options[key]);
    } // end for loop
    return newSpan
  }

  function createNewCheckboxListItemWithCount (el, countOptions, checkboxOptions) {
    const newListItem = document.createElement('li');
    const newCheckboxElements = createCheckboxWithLabel(checkboxOptions);
    if (newCheckboxElements.length > 0) {
      const countElement = createSpan(countOptions);
      if (countElement) newCheckboxElements.unshift(countElement);
      newCheckboxElements.forEach(el => newListItem.append(el));
      el.append(newListItem);
    }
  }

  // import * as foo from './src/foo.js'

  const activeIntervals = {};
  const intervalCounts = {};

  // console.log('Doing all the stuff!')

  const noDisplayString = "display:none;";

  const hideButtonAttributes
    = { specialId: "hide-posts-special-button"
      , hideLabel: "Deactivate Filters"
      , showLabel: "Activate Filters"
      };

  // Options: (provide checkbox for each)
  // - Hide duplicate songs by same mapper
  // - Hide songs by any mapper you choose
  // - Hide songs without selected difficulty
  // - Hide songs with less than x upvotes (user input)

  const elementSelector
    = { mapperName:             selectors.singlePost
      , mapperParentNodeCount:  selectors.parentNodeCount
      , postContent:    '.post-content'
      , restoreContainerId: 'restore-mappers-container'
      , difficultyContainerId: 'difficulty-selector-container'
      };

  const diffSelector
    = { easy:           'a.post-difficulty[href="/songs/?difficulty=easy"]'
      , normal:         'a.post-difficulty[href="/songs/?difficulty=normal"]'
      , hard:           'a.post-difficulty[href="/songs/?difficulty=hard"]'
      , expert:         'a.post-difficulty[href="/songs/?difficulty=expert"]'
      , expertPlus:     'a.post-difficulty[href="/songs/?difficulty=expert-plus"]'
      };

  const hideMapperButtonAttributes
    = { specialId: "hide-mapper-button"
      , label: "Hide<br>Mapper"
      };

  addCustomStyleTag();
  createModalBlock({top: '150px', left: '45px'});
  createHideButton();
  createDifficultyCheckboxes(diffSelector);
  createRestoreMapperButtons(getHiddenMappers());
  const bod = document.querySelector('body');
  bod.addEventListener("keydown", elementScrollJump);

  putMutationObserverOnMainElement(bod, hidePosts);

  // ===========================================================================
  // Begin support functions
  // Nothing invoked beneath this point
  // ===========================================================================

  function getHiddenMappers() {
    const rawJSON = localStorage.getItem('hiddenMappers') || '[]';
    return JSON.parse(rawJSON)
  }

  function getRequiredDifficulties() {
    const rawJSON = localStorage.getItem('requiredDifficulties') || '[]';
    return JSON.parse(rawJSON)
  }

  function hasRequiredDifficulty (el, key) {
    return !!el.querySelector(diffSelector[key])
  }

  function getHideStatus() {
    const rawJSON = localStorage.getItem('isHidingPosts') || 'false';
    return JSON.parse(rawJSON)
  }

  function updateRequiredDifficulties () {
    const inputList = document.querySelectorAll('.modal-input-list.difficulties .input-modal-checkbox');
    if (!inputList) return
    const curRequiredDifficulties = [];
    for (let i = 0; i < inputList.length; i++) {
      const curInput = inputList[i];
      if (curInput.checked) curRequiredDifficulties.push(curInput.id.replace(/^modal-checkbox-/, ''));
    } // end for loop
    localStorage.setItem('requiredDifficulties'
                        , JSON.stringify(curRequiredDifficulties)
                        );

    return curRequiredDifficulties
  } // end updateRequiredDifficulties

  function hidePosts () {
    const postElements = document.querySelectorAll(selectors.singlePost);
    if (!postElements) return

    const hideBadPosts = getHideStatus();

    console.log(`${hideBadPosts ? 'Hiding' : 'Revealing'} those posts...`);

    createHideButton();
    updateRequiredDifficulties();

    const counts
      = { hiddenMappers: {}
        , songList: []
        };
    getHiddenMappers().forEach(e => counts.hiddenMappers[e] = 0);

    postElements.forEach((el, curIndex) => {
      let targetEl = el;
      for (let i = 0; i < selectors.parentNodeCount; i++) {
        targetEl = targetEl.parentNode;
      } // end for loop
      counts.curIndex = curIndex;
      const isHidden = !isValidElement(targetEl, counts);
      if (isHidden && hideBadPosts) {
        targetEl.style = noDisplayString;
      } else {
        targetEl.style = "";
        // (targetEl.style || '').replace("display:none;", "")
      }
    }); // end postElements forEach

    // updatePostCounts(counts.hiddenMappers)
    createHideMapperButtons();
    createRestoreMapperButtons(getHiddenMappers(), counts.hiddenMappers);
  } // end hidePosts function

  function createHideFunction() {
    return () => {
      localStorage.setItem('isHidingPosts', JSON.stringify(!getHideStatus()));
      hidePosts();
    } // end callback function
  } // end createHideFunction

  function isValidElement (el, counts) {
    const mapperNameEl  = el.querySelector(selectors.singlePost);
    const curMapperName
      = mapperNameEl
        ? mapperNameEl.innerText.trim()
        : 'NO MAPPER NAME';

    // return FALSE for duplicates
    const entryTitleEl = el.querySelector('.entry-title');
    const curSongName
      = entryTitleEl
        ? entryTitleEl.innerText.trim()
        : 'NO SONG TITLE';
    const curSongByMapper = `${curSongName} by mapper ${curMapperName}`;
    counts.songList.push(curSongByMapper);
    if (counts.songList.indexOf(curSongByMapper) !== counts.curIndex) {
      // console.log(`Hiding duplicate map: ${curSongByMapper}`)
      return false
    }

    // return FALSE for a hidden mapper (and adjust their count)
    if (getHiddenMappers().indexOf(curMapperName) !== -1) {
      counts.hiddenMappers[curMapperName]++;
      // console.log(`Hiding map by ${curMapperName}...`)
      return false
    }

    // return FALSE for missing required difficulty
    const requiredDifficulties = getRequiredDifficulties();
    if (requiredDifficulties.length > 0 && requiredDifficulties.every(diff => !hasRequiredDifficulty(el, diff))) {
      // console.log(`Hiding "${curSongByMapper}"`)
      // console.log(`Hiding song with missing difficulties: ${curSongByMapper}`)
      return false
    }

    return true
  } // end of isValidElement

  function createHideButton () {
    const buttonContainerElement = getMainModalElement();
    if (!buttonContainerElement) {
      console.log('well shit');
      return
    }

    let hideButtonElement = buttonContainerElement.querySelector(`#${hideButtonAttributes.specialId}`);
    const buttonJustCreated = !hideButtonElement;

    if (!hideButtonElement) {
      console.log("creating hide button...");
      hideButtonElement     = document.createElement('button');
      hideButtonElement.id  = hideButtonAttributes.specialId;
      hideButtonElement.addEventListener('click', createHideFunction());
    }

    hideButtonElement.innerText
      = getHideStatus()
        ? hideButtonAttributes.hideLabel
        : hideButtonAttributes.showLabel;
    hideButtonElement.className
      = getHideStatus()
        ? 'modal-button hide-button'
        : 'modal-button live-button';

    if (buttonJustCreated) buttonContainerElement.prepend(hideButtonElement);

  } // end createHideButton


  function createDifficultyCheckboxes (difficultyList) {
    const difficultyContainerDiv = createCollapsibleSectionContainer("Required Difficulties", "difficulty");

    const inputListContainer = document.createElement('ul');
    inputListContainer.setAttribute('class','modal-input-list difficulties');
    inputListContainer.onclick="event.stopPropagation()";
    inputListContainer.onmousedown="event.stopPropagation()";

    const requiredDifficulties = getRequiredDifficulties();

    for (const key in difficultyList) {
      const checkboxOptions
        = { id: `modal-checkbox-${key}`
          , label: key
          , checked: !!(requiredDifficulties.indexOf(key) !== -1) };
      createNewCheckboxListItem(inputListContainer, checkboxOptions);
    } // end for loop

    putMutationObserverOnInputList(inputListContainer, hidePosts);
    difficultyContainerDiv.append(inputListContainer);
  } // end createDifficultyCheckboxes

  function putMutationObserverOnInputList (el, callback) {
    let inputEl = el;
    if (!inputEl) return

    // configuration of the observer:
    const inputElConfig
      = { attributes: true
        , characterData: true
        , subtree: true
        };

    // create an observer instance
    const mainElementObserver
      = new MutationObserver(callback); // end MutationObserver

    // pass in the target node, as well as the observer options
    mainElementObserver.observe(inputEl, inputElConfig);
  } // end putMutationObserverOnInputList

  function createHideMapperButtons () {
    const mapperElements = document.querySelectorAll(selectors.singlePost);
    mapperElements.forEach(el => {
      const buttonContainerDiv = el.parentNode;
      // If you already have the button, just stop
      if (buttonContainerDiv.querySelector(`#${hideMapperButtonAttributes.specialId}`)) return

      const mapperName = el.innerText.trim();

      const newButton     = document.createElement('button');
      newButton.id        = hideMapperButtonAttributes.specialId;
      newButton.innerHTML = hideMapperButtonAttributes.label;
      newButton.className = 'hide-button';
      newButton.addEventListener('click', createHideMapperFunction(mapperName));

      buttonContainerDiv.prepend(newButton);
    }); // end mapperElements forEach
  } // end createHideMapperButtons

  function createHideMapperFunction(mapperName) {
    return () => {
      const hiddenMappers = getHiddenMappers();
      if (hiddenMappers.indexOf(mapperName) === -1) {
        hiddenMappers.push(mapperName);
        localStorage.setItem('hiddenMappers', JSON.stringify(hiddenMappers));
      } // end if mapperName
      hidePosts();
    } // end callback function
  } // end createHideMapperFunction

  function createRestoreMapperButtons (hiddenMappers, counts={}) {

    const restoreMapperContainerDiv = createCollapsibleSectionContainer("Restore Mappers", "restore-mapper");

    let listContainerDiv = document.querySelector(`#restore-mapper-button-list`);
    if (!listContainerDiv) {
      listContainerDiv    = document.createElement('section');
      listContainerDiv.id = `restore-mapper-button-list`;
      listContainerDiv.className  = 'modal-input-list';
      restoreMapperContainerDiv.append(listContainerDiv);
    }

    // const noHiddenMappersMessage = "No mappers are hidden"
    // // const noHiddenMappersStyle = "margin-left: 25px; font-style: italic;"
    // if (hiddenMappers && hiddenMappers.length === 0) {
    //   listContainerDiv.innerHTML = noHiddenMappersMessage
    //   // listContainerDiv.style = noHiddenMappersStyle
    // } else {
    //   listContainerDiv.innerHTML = listContainerDiv.innerHTML.replace(noHiddenMappersMessage, '')
    //   // listContainerDiv.style = listContainerDiv.style.replace(noHiddenMappersStyle, '')
    // }

    hiddenMappers.forEach(mapperName => {
      const specialButtonId = `restore-mapper-${mapperName.replace(/[^0-9a-zA-Z]/g,'')}`;
      // don't make the same button twice
      if (listContainerDiv.querySelector(`#${specialButtonId}`)) return

      const newRestoreButton      = document.createElement('button');
      newRestoreButton.id         = specialButtonId;
      // newRestoreButton.innerText  = `${counts[mapperName] || 0} - ${mapperName}`
      newRestoreButton.innerText  = mapperName;
      newRestoreButton.className  = 'hide-button';
      newRestoreButton.addEventListener('click', createRestoreFunction(mapperName, specialButtonId));

      listContainerDiv.append(newRestoreButton);
    }); // end hiddenMappers forEach

  } // end createRestoreMapperButtons

  function createRestoreFunction(mapperName, specialButtonId) {
    return () => {
      const hiddenMappers = getHiddenMappers();
      const mapperIndex = hiddenMappers.indexOf(mapperName);
      if (mapperIndex !== -1) {
        hiddenMappers.splice(mapperIndex, 1);
        localStorage.setItem('hiddenMappers', JSON.stringify(hiddenMappers));
      } // end if mapperName
      const restoreMapperButton = document.querySelector(`#${specialButtonId}`);
      if (restoreMapperButton) restoreMapperButton.remove();
      hidePosts();
    } // end callback function
  } // end createRestoreFunction


  function startMOInterval (sectionName, callback, options={}) {
    activeIntervals[sectionName]
      = setInterval(() => {
          putObserverOnEl(sectionName, callback, options);
        }, 700);
  } // end startMOInterval

  function putObserverOnEl (key, callback, options) {
    intervalCounts[key] = intervalCounts[key] || 0;
    intervalCounts[key]++;
    if (intervalCounts[key] > 8) {
      console.log(`Did not find element for "${key}" after ${intervalCounts[key]} attempts`);
      clearInterval(activeIntervals[key]);
      activeIntervals[key] = undefined;
      return
    }
    let el  = document.querySelector(elementSelector[key]);
    if (!el) return
    for (let i = 0; i < (elementSelector.mapperParentNodeCount + 1); i++)
      el = el.parentNode;
    if (!el) return

    callback(el);
    const elConfig = options.config || { childList: true };
    const elObserver = options.observer || new MutationObserver((mutations) => {
          if (options.msg) console.log(options.msg);
          callback(el);
        }); // end MutationObserver
    elObserver.observe(el, elConfig);
    console.log(`Successfully put observer on "${key}"`);
    clearInterval(activeIntervals[key]);
    activeIntervals[key] = undefined;
  } // end function putObserverOnEl

  function putMutationObserverOnMainElement (el, callback) {
    startMOInterval("mapperName"
    , callback
    , { config: { childList: true
                // , subtree: true
                }
      // , msg: "processing ..."
      }
    );
  } // end putMutationObserverOnMainElement

}());