[MAL] VA Display Settings

Show voice actors only in selected languages.

// ==UserScript==
// @name         [MAL] VA Display Settings
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Show voice actors only in selected languages.
// @author       grin3671
// @match        https://myanimelist.net/*
// @grant        none
// ==/UserScript==

(function() {
  'use strict';

  let t0 = performance.now();

  // return Array
  const getUserPreferense = () => {
    let storage = JSON.parse(localStorage.getItem('myVaSetting'));
    return storage ? storage : ['Japanese'];
  }

  // Initial data
  let supportedCategories = ['anime', 'character'],
      supportedLanguages = ['Japanese', 'English', 'Korean', 'Spanish', 'German', 'French', 'Portuguese (BR)', 'Italian', 'Hungarian', 'Hebrew', 'Mandarin'],
      userPreferense = getUserPreferense(),
      settingsElement = null,
      toggleableElements = null,
      settingsLink = '#va_settings',
      currentPage = null,
      pageSettings = {
        anime: {
          langSelector: '.js-anime-character-language',
          castSelector: '.anime-character-container table table tr',
          headerSelector: '.page-common .border_solid',
        },
        character: {
          langSelector: 'small',
          castSelector: '#content > table > tbody > tr > td:last-child > table',
          headerSelector: 'div.normal_header',
        }
  }

  // check Link
  const checkPageSupport = () => {
    let currentURL = location.pathname.substring(1).split('/');
    currentPage = currentURL[0];
    return (supportedCategories.includes(currentURL[0]) && (currentPage == 'anime' ? currentURL[currentURL.length - 1] == 'characters' : currentURL.length <= 3));
  }

  const saveUserPreferense = (data) => {
    localStorage.setItem('myVaSetting', JSON.stringify(data));
    userPreferense = data;
  }

  const openSettings = () => {
    settingsElement.showModal();
  }

  const closeSettings = () => {
    settingsElement.close();
  }

  // toggle Display
  const updateElementList = (elems) => {
    let t1 = performance.now();
    let langSelector = pageSettings[currentPage].langSelector;
    for (let i = 0; i < elems.length; ++i) {
      let lang = elems[i].querySelector(langSelector);
      elems[i].style.display = (userPreferense.includes(lang.textContent.trim())) ? (currentPage == 'anime' ?'table-row' : 'table') : 'none';
    }
    console.info('"VA Display Settings" update performance: ' + (performance.now() - t1) + ' milliseconds.');
  };

  if (checkPageSupport()) {
    // -----------------------------------
    // MAIN PART: Voice Actor's Visibility
    // -----------------------------------
    // Select all VA's Elements
    toggleableElements = document.querySelectorAll(pageSettings[currentPage].castSelector);
    // Update Visibility of Elements
    updateElementList(toggleableElements);

    // ------------------------------------------
    // SECONDARY PART: Control Buttons and Styles
    // ------------------------------------------
    // Functions --------------------------------
    let createSettingsButton = () => {
      let button = document.createElement('a');
      button.id = 'va_settings_button';
      let icon = document.createElement('i');
      button.tabIndex = '-1';
      icon.className = 'fa fa-cog mr4';
      button.append(icon, 'VA Display Settings');
      button.href = settingsLink;
      return button;
    }
    let createSettingsDialog = () => {
      let dialog = document.createElement('dialog');
      dialog.id = 'voice_actors_settings';

      dialog.addEventListener('close', (event) => {
        if (location.hash == settingsLink) history.back();
      });

      let dialogHeader = document.createElement('h2');
      dialogHeader.textContent = 'Voice Actors Display Settings';

      let formHint = document.createElement('p');
      formHint.textContent = 'Choose which languages you want to see in the list.';

      let dialogForm = document.createElement('form');

      let formFieldset = document.createElement('fieldset');

      let fieldsetLegend = document.createElement('legend');
      fieldsetLegend.textContent = 'Choose preferred languages:';
      formFieldset.append(fieldsetLegend);

      //
      let counts = new Array(supportedLanguages.length).fill(0);
      let langSelector = pageSettings[currentPage].langSelector;

      for (let i = 0; i < supportedLanguages.length; ++i) {
        for (let j = 0; j < toggleableElements.length; ++j) {
          if (toggleableElements[j].querySelector(langSelector).textContent.trim() == supportedLanguages[i]) counts[i] += 1;
        }

        let langLabel = document.createElement('label');
        let langCheckbox = document.createElement('input');
        langCheckbox.type = 'checkbox';
        langCheckbox.name = 'Language';
        langCheckbox.value = supportedLanguages[i];
        if (userPreferense.includes(supportedLanguages[i])) langCheckbox.checked = 'checked';
        let langText = document.createElement('span');
        langText.textContent = supportedLanguages[i] + ' (' + counts[i] + ')';
        langLabel.append(langCheckbox, langText);
        formFieldset.append(langLabel);
      }
      dialogForm.onchange = () => {
        let formData = new FormData(dialogForm);
        let userSettings = [];
        for (var [key, value] of formData.entries()) {
          userSettings.push(value);
        }
        saveUserPreferense(userSettings);
        updateElementList(toggleableElements);
      }
      dialogForm.append(formHint, formFieldset);

      let dialogFooter = document.createElement('div');

      let dialogClose = document.createElement('input');
      dialogClose.className = 'inputButton flat';
      dialogClose.type = 'button';
      dialogClose.value = 'Close';
      dialogClose.onclick = () => {
        closeSettings();
      };

      dialogFooter.append(dialogClose);
      dialog.append(dialogHeader, dialogForm, dialogFooter);
      return dialog;
    }
    let createStyles = () => {
      let style = document.createElement('style');
      style.innerHTML = `
        #va_settings_button {
          display: inline-block;
          padding: 4px 8px;
          margin: -4px -4px -4px 6px;
          font-size: 11px;
          font-weight: normal;
        }

        #voice_actors_settings {
          position: fixed;
          top: 0;
          bottom: 0;
          width: 340px;
          padding: 16px;
          border: 2px solid #1d439b;
          outline: 10px solid #e1e7f557;
          text-align: left;
        }

        #voice_actors_settings::backdrop {
          background: rgba(0,0,0,.24);
        }

        #voice_actors_settings fieldset {
          border: 1px solid #bebebe;
          padding: 4px 8px;
          margin: 10px 0;
        }

        #voice_actors_settings legend {
          padding: 0 8px;
        }

        #voice_actors_settings label {
          display: flex;
          align-items: center;
          height: 29px;
          padding: 0 2px;
          margin: 0 2px;
          border-bottom: 1px solid #ebebeb;
          cursor: pointer;
          user-select: none;
        }

        #voice_actors_settings label:hover {
          background: rgba(0,0,0,.04);
        }

        #voice_actors_settings label:last-child {
          border-bottom: 0;
        }

        #voice_actors_settings label span {
          margin-left: 6px;
        }

        #voice_actors_settings div {
          text-align: right;
        }

        #voice_actors_settings .inputButton {
          padding: 4px 8px;
          font-size: 13px;
        }
      `;
      document.head.appendChild(style);
    }
    // ------------------------------------------
    // Find Header where Button will be placed
    let headerElement = document.querySelectorAll(pageSettings[currentPage].headerSelector);
    switch (currentPage) {
      case 'anime':
        if (headerElement[0].querySelector('h2').textContent == 'Characters & Voice Actors') {
          headerElement[0].querySelector('h2').textContent += ' (' + toggleableElements.length + ')';
          headerElement[0].append(createSettingsButton());
        }
        break;
      case 'character':
        if (headerElement[2].textContent == 'Voice Actors') {
          headerElement[2].textContent += ' (' + toggleableElements.length + ')';
          headerElement[2].append(createSettingsButton());
        }
        break;
    }
    // Additionally adds Dialog to Page
    let dialog = createSettingsDialog();
    document.body.append(dialog);
    settingsElement = dialog;
    // Add Main Styles
    createStyles();

    // ------------------------------------------
    // If Page has Hash, open Dialog
    if (location.hash == settingsLink) openSettings();
    // Open/Close Dialog depending on Hash
    window.onpopstate = function (event) {
      if (location.hash == settingsLink) openSettings();
      if (location.hash == '') closeSettings();
    };

    console.info('"VA Display Settings" initial performance: ' + (performance.now() - t0) + ' milliseconds.');
  }
})();