MediaWiki:MonkeyTTracker.js

From CoraTO Wiki - Official Wiki
Jump to navigation Jump to search

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
(function () {
  'use strict';

  const STORAGE_KEY = 'cora.questTracker.monkeyT.v1';
  const SELECTORS = {
    questCheckbox: '.quest-checkbox',
    locationSection: '.location-section',
    trackerRootId: 'monkeytab-tracker',
    totalProgress: '#total-progress',
    completionPercentage: '#completion-percentage',
    progressFill: '#progress-fill',
    resetButton: '#reset-progress',
    statsContainer: '.progress-stats'
  };

  function safeQuerySelector(selector, context) {
    try {
      return (context || document).querySelector(selector);
    } catch (_) {
      return null;
    }
  }

  function safeQuerySelectorAll(selector, context) {
    try {
      return Array.from((context || document).querySelectorAll(selector));
    } catch (_) {
      return [];
    }
  }

  function safeJsonParse(value) {
    try {
      return JSON.parse(value);
    } catch (_) {
      return null;
    }
  }

  function safeGetLocalStorage(key) {
    try {
      return localStorage.getItem(key);
    } catch (_) {
      return null;
    }
  }

  function safeSetLocalStorage(key, value) {
    try {
      localStorage.setItem(key, value);
    } catch (_) {}
  }

  function safeRemoveLocalStorage(key) {
    try {
      localStorage.removeItem(key);
    } catch (_) {}
  }

  function getQuestId(checkbox, index) {
    const dataQuest = checkbox.getAttribute('data-quest');
    if (dataQuest) return dataQuest;
    const name = checkbox.getAttribute('name');
    if (name) return name;
    const id = checkbox.getAttribute('id');
    if (id) return id;
    return `quest-${index}`;
  }

  function parseRequiredCountForCheckbox(checkbox) {
    const label = checkbox.closest('label');
    const nameEl = label ? safeQuerySelector('.quest-name', label) : null;
    const text = (nameEl ? nameEl.textContent : '') || '';
    const match = text.match(/(\d+)/);
    if (!match) return 1;
    const n = Number(match[1]);
    return Number.isFinite(n) && n > 0 ? n : 1;
  }

  function collectRegions(root) {
    const sections = safeQuerySelectorAll(SELECTORS.locationSection, root);
    return sections
      .map((section) => ({
        section,
        checkboxes: safeQuerySelectorAll(SELECTORS.questCheckbox, section)
      }))
      .filter((x) => x.checkboxes.length > 0);
  }

  function applySingleSelectionPerRegion(regions) {
    regions.forEach(({ checkboxes }) => {
      const checked = checkboxes.filter((cb) => cb.checked);
      const anyChecked = checked.length > 0;

      if (!anyChecked) {
        checkboxes.forEach((cb) => {
          cb.disabled = false;
        });
        return;
      }

      const chosen = checked[0];
      if (checked.length > 1) {
        checked.slice(1).forEach((cb) => {
          cb.checked = false;
        });
      }

      checkboxes.forEach((cb) => {
        cb.disabled = cb !== chosen;
      });
    });
  }

  function loadState(checkboxes) {
    const raw = safeGetLocalStorage(STORAGE_KEY);
    const parsed = raw ? safeJsonParse(raw) : null;
    const state = parsed && typeof parsed === 'object' ? parsed : {};

    checkboxes.forEach((cb, index) => {
      const key = getQuestId(cb, index);
      cb.checked = !!state[key];
    });
  }

  function saveState(checkboxes) {
    const out = {};
    checkboxes.forEach((cb, index) => {
      const key = getQuestId(cb, index);
      out[key] = !!cb.checked;
    });
    safeSetLocalStorage(STORAGE_KEY, JSON.stringify(out));
  }

  function ensureItemsStatElement(root) {
    const container = safeQuerySelector(SELECTORS.statsContainer, root);
    if (!container) return null;

    let el = container.querySelector('[data-stat="items-progress"]');
    if (el) return el;

    const item = document.createElement('div');
    item.className = 'stat-item';
    item.setAttribute('data-stat', 'items-progress');

    const label = document.createElement('span');
    label.className = 'stat-label';
    label.textContent = 'Items:';

    const value = document.createElement('span');
    value.className = 'stat-value';
    value.setAttribute('data-stat-value', 'items-progress');
    value.textContent = '0/0';

    item.appendChild(label);
    item.appendChild(value);
    container.appendChild(item);

    return item;
  }

  function updateProgress(regions, allCheckboxes, root) {
    const totalRegions = regions.length;
    const completedRegions = regions.filter(({ checkboxes }) => checkboxes.some((cb) => cb.checked)).length;
    const percentage = totalRegions > 0 ? Math.round((completedRegions / totalRegions) * 100) : 0;

    const totalProgressEl = safeQuerySelector(SELECTORS.totalProgress, root);
    const completionEl = safeQuerySelector(SELECTORS.completionPercentage, root);
    const fillEl = safeQuerySelector(SELECTORS.progressFill, root);

    if (totalProgressEl) totalProgressEl.textContent = `${completedRegions}/${totalRegions}`;
    if (completionEl) completionEl.textContent = `${percentage}%`;
    if (fillEl) fillEl.style.width = `${percentage}%`;

    const itemsStat = ensureItemsStatElement(root);
    const itemsValue = itemsStat ? itemsStat.querySelector('[data-stat-value="items-progress"]') : null;
    if (itemsValue) {
      const totalItems = allCheckboxes.reduce((sum, cb) => sum + parseRequiredCountForCheckbox(cb), 0);
      const completedItems = allCheckboxes.reduce(
        (sum, cb) => sum + (cb.checked ? parseRequiredCountForCheckbox(cb) : 0),
        0
      );
      itemsValue.textContent = `${completedItems}/${totalItems}`;
    }
  }

  function wireEvents(regions, checkboxes, root) {
    checkboxes.forEach((cb) => {
      cb.addEventListener('change', () => {
        applySingleSelectionPerRegion(regions);
        saveState(checkboxes);
        updateProgress(regions, checkboxes, root);
      });
    });

    const resetBtn = safeQuerySelector(SELECTORS.resetButton, root);
    if (resetBtn) {
      resetBtn.addEventListener('click', () => {
        checkboxes.forEach((cb) => {
          cb.checked = false;
          cb.disabled = false;
        });
        safeRemoveLocalStorage(STORAGE_KEY);
        updateProgress(regions, checkboxes, root);
      });
    }
  }

  function init() {
    const root = document.getElementById(SELECTORS.trackerRootId);
    if (!root) return;

    const checkboxes = safeQuerySelectorAll(SELECTORS.questCheckbox, root);
    if (checkboxes.length === 0) return;

    const regions = collectRegions(root);
    if (regions.length === 0) return;

    loadState(checkboxes);
    applySingleSelectionPerRegion(regions);
    saveState(checkboxes);
    updateProgress(regions, checkboxes, root);
    wireEvents(regions, checkboxes, root);
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();