Jump to content

MediaWiki:Common.js: Difference between revisions

From SIGNAL Earth Wiki
No edit summary
No edit summary
Line 25: Line 25:
       .replace(/"/g, """)
       .replace(/"/g, """)
       .replace(/'/g, "'");
       .replace(/'/g, "'");
  }
  function normalizeKey(value) {
    return String(value == null ? "" : value)
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, " ")
      .trim();
  }
  function findStructuredDataTable() {
    var tables = Array.prototype.slice.call(document.querySelectorAll('table'));
    for (var i = 0; i < tables.length; i += 1) {
      var table = tables[i];
      var caption = table.querySelector('caption');
      var captionText = caption ? caption.textContent || '' : '';
      if (/SIGNAL\s+Earth\s+Structured\s+Data/i.test(captionText)) return table;
      var text = (table.textContent || '').slice(0, 250);
      if (/SIGNAL\s+Earth\s+Structured\s+Data/i.test(text)) return table;
    }
    return null;
  }
  function readStructuredDataFromPage() {
    var table = findStructuredDataTable();
    if (!table) return null;
    var data = {};
    Array.prototype.slice.call(table.querySelectorAll('tr')).forEach(function (row) {
      var th = row.querySelector('th');
      var td = row.querySelector('td');
      if (!th || !td) return;
      var key = normalizeKey(th.textContent || '');
      var value = String(td.textContent || '').replace(/\s+/g, ' ').trim();
      if (!key || !value || value === '—') return;
      data[key] = value;
    });
    return Object.keys(data).length ? data : null;
  }
  function payloadFromPageMetadata(fallback) {
    var data = readStructuredDataFromPage();
    if (!data) return null;
    var objectType = data['object type'] || '';
    var id = data['signal earth id'] || fallback.id;
    var subtitle = objectType || (fallback.type === 'DS' ? 'Damage Signal' : fallback.type === 'DSI' ? 'Damage Signal Instance' : 'SIGNAL structured object');
    return {
      term: {
        type: fallback.type,
        id: id,
        label: fallback.label || id,
        subtitle: subtitle,
        observableType: data['observable type'] || null,
        unit: data['unit'] || null,
        temporalStructure: data['temporal structure'] || null,
        monitoringBackbone: data['monitoring backbone'] || null,
        geography: data['geography'] || null,
        appPath: basicAppPath(fallback.type, id),
        wikiUrl: window.location.href.split('#')[0]
      }
    };
   }
   }


Line 143: Line 208:
         .then(function (payload) {
         .then(function (payload) {
           if (activeTarget !== target) return;
           if (activeTarget !== target) return;
           renderTerm(card, fallback, payload);
           renderTerm(card, fallback, payload || payloadFromPageMetadata(fallback));
           placeCard(card, target);
           placeCard(card, target);
         })
         })
         .catch(function () {
         .catch(function () {
           if (activeTarget !== target) return;
           if (activeTarget !== target) return;
           renderBasic(card, label, type, id);
           renderTerm(card, fallback, payloadFromPageMetadata(fallback));
           placeCard(card, target);
           placeCard(card, target);
         });
         });

Revision as of 15:25, 26 May 2026

/* Any JavaScript here will be loaded for all users on every page load. */

window.SIGNAL_EARTH_APP_BASE_URL = "http://localhost:3000";
window.SIGNAL_EARTH_API_BASE_URL = "http://localhost:3000";

/* SIGNAL structured-term hovercards. Add to MediaWiki:Common.js.
 * Optional configuration before this script runs:
 *   window.SIGNAL_EARTH_APP_BASE_URL = "http://localhost:3000";
 *   window.SIGNAL_EARTH_API_BASE_URL = "http://localhost:3000";
 */
(function () {
  function getAppBaseUrl() {
    return (window.SIGNAL_EARTH_APP_BASE_URL || "http://localhost:3000").replace(/\/$/, "");
  }

  function getApiBaseUrl() {
    return (window.SIGNAL_EARTH_API_BASE_URL || getAppBaseUrl()).replace(/\/$/, "");
  }

  function escapeHtml(value) {
    return String(value == null ? "" : value)
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;")
      .replace(/'/g, "&#39;");
  }



  function normalizeKey(value) {
    return String(value == null ? "" : value)
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, " ")
      .trim();
  }

  function findStructuredDataTable() {
    var tables = Array.prototype.slice.call(document.querySelectorAll('table'));
    for (var i = 0; i < tables.length; i += 1) {
      var table = tables[i];
      var caption = table.querySelector('caption');
      var captionText = caption ? caption.textContent || '' : '';
      if (/SIGNAL\s+Earth\s+Structured\s+Data/i.test(captionText)) return table;
      var text = (table.textContent || '').slice(0, 250);
      if (/SIGNAL\s+Earth\s+Structured\s+Data/i.test(text)) return table;
    }
    return null;
  }

  function readStructuredDataFromPage() {
    var table = findStructuredDataTable();
    if (!table) return null;

    var data = {};
    Array.prototype.slice.call(table.querySelectorAll('tr')).forEach(function (row) {
      var th = row.querySelector('th');
      var td = row.querySelector('td');
      if (!th || !td) return;
      var key = normalizeKey(th.textContent || '');
      var value = String(td.textContent || '').replace(/\s+/g, ' ').trim();
      if (!key || !value || value === '—') return;
      data[key] = value;
    });

    return Object.keys(data).length ? data : null;
  }

  function payloadFromPageMetadata(fallback) {
    var data = readStructuredDataFromPage();
    if (!data) return null;

    var objectType = data['object type'] || '';
    var id = data['signal earth id'] || fallback.id;
    var subtitle = objectType || (fallback.type === 'DS' ? 'Damage Signal' : fallback.type === 'DSI' ? 'Damage Signal Instance' : 'SIGNAL structured object');

    return {
      term: {
        type: fallback.type,
        id: id,
        label: fallback.label || id,
        subtitle: subtitle,
        observableType: data['observable type'] || null,
        unit: data['unit'] || null,
        temporalStructure: data['temporal structure'] || null,
        monitoringBackbone: data['monitoring backbone'] || null,
        geography: data['geography'] || null,
        appPath: basicAppPath(fallback.type, id),
        wikiUrl: window.location.href.split('#')[0]
      }
    };
  }

  function basicAppPath(type, id) {
    if (type === "DS") return "/signals/" + encodeURIComponent(id) + "?tab=wiki";
    if (type === "DSI") return "/signals/instances/" + encodeURIComponent(id);
    return null;
  }

  function createCard() {
    var card = document.createElement("div");
    card.className = "signal-term-hovercard";
    card.hidden = true;
    document.body.appendChild(card);
    return card;
  }

  function placeCard(card, target) {
    var rect = target.getBoundingClientRect();
    var margin = 10;
    var top = rect.bottom + margin;
    var left = Math.min(rect.left, window.innerWidth - card.offsetWidth - 16);
    left = Math.max(16, left);

    if (top + card.offsetHeight > window.innerHeight - 16) {
      top = Math.max(16, rect.top - card.offsetHeight - margin);
    }

    card.style.left = left + "px";
    card.style.top = top + "px";
  }

  function renderLoading(card, label, type, id) {
    card.innerHTML =
      '<div class="signal-term-hovercard-title">' + escapeHtml(label || id || "SIGNAL object") + '</div>' +
      '<div class="signal-term-hovercard-subtitle">SIGNAL structured object' + (type || id ? ': ' + escapeHtml([type, id].filter(Boolean).join(' ')) : '') + '</div>' +
      '<div class="signal-term-hovercard-detail">Loading structured object context…</div>';
  }

  function renderBasic(card, label, type, id) {
    var appPath = basicAppPath(type, id);
    var links = [];
    if (appPath) {
      links.push('<a href="' + escapeHtml(getAppBaseUrl() + appPath) + '" target="_blank" rel="noopener noreferrer">Open SIGNAL object</a>');
    }
    links.push('<a href="' + escapeHtml(window.location.href.split('#')[0]) + '" target="_blank" rel="noopener noreferrer">Open wiki article</a>');

    card.innerHTML =
      '<div class="signal-term-hovercard-title">' + escapeHtml(label || id || "SIGNAL object") + '</div>' +
      '<div class="signal-term-hovercard-subtitle">' + escapeHtml([type, id].filter(Boolean).join(' · ') || 'SIGNAL structured object') + '</div>' +
      '<div class="signal-term-hovercard-detail">Structured object metadata is unavailable from the SIGNAL app API.</div>' +
      '<div class="signal-term-hovercard-links">' + links.join('') + '</div>';
  }

  function renderTerm(card, fallback, payload) {
    var term = payload && payload.term ? payload.term : null;
    if (!term) {
      renderBasic(card, fallback.label, fallback.type, fallback.id);
      return;
    }

    var appUrl = term.appPath ? getAppBaseUrl() + term.appPath : null;
    var wikiUrl = term.wikiUrl || window.location.href.split('#')[0];
    var details = [];

    if (term.observableType) details.push(['Observable type', term.observableType]);
    if (term.unit) details.push(['Unit', term.unit]);
    if (term.temporalStructure) details.push(['Temporal structure', term.temporalStructure]);
    if (term.monitoringBackbone) details.push(['Monitoring backbone', term.monitoringBackbone]);
    if (term.geography) details.push(['Geography', term.geography]);

    var detailHtml = details.length
      ? details.map(function (row) {
          return '<div class="signal-term-hovercard-detail"><strong>' + escapeHtml(row[0]) + ':</strong> ' + escapeHtml(row[1]) + '</div>';
        }).join('')
      : '<div class="signal-term-hovercard-detail">' + escapeHtml(term.description || 'Structured SIGNAL object.') + '</div>';

    var links = [];
    if (appUrl) links.push('<a href="' + escapeHtml(appUrl) + '" target="_blank" rel="noopener noreferrer">Open SIGNAL object</a>');
    if (wikiUrl) links.push('<a href="' + escapeHtml(wikiUrl) + '" target="_blank" rel="noopener noreferrer">Open wiki article</a>');

    card.innerHTML =
      '<div class="signal-term-hovercard-title">' + escapeHtml(term.label || fallback.label || fallback.id) + '</div>' +
      '<div class="signal-term-hovercard-subtitle">' + escapeHtml(term.subtitle || [fallback.type, fallback.id].filter(Boolean).join(' · ')) + '</div>' +
      detailHtml +
      '<div class="signal-term-hovercard-links">' + links.join('') + '</div>';
  }

  function init() {
    var card = createCard();
    var hideTimer = null;
    var activeTarget = null;

    function showFor(target) {
      window.clearTimeout(hideTimer);
      activeTarget = target;
      var type = (target.getAttribute('data-signal-type') || '').trim().toUpperCase();
      var id = (target.getAttribute('data-signal-id') || '').trim();
      var label = (target.getAttribute('data-signal-label') || target.textContent || id || '').trim();
      var fallback = { type: type, id: id, label: label };

      renderLoading(card, label, type, id);
      card.hidden = false;
      placeCard(card, target);

      if (!type || !id) {
        renderBasic(card, label, type, id);
        placeCard(card, target);
        return;
      }

      fetch(getApiBaseUrl() + '/api/wiki/signal-term?type=' + encodeURIComponent(type) + '&id=' + encodeURIComponent(id), {
        method: 'GET',
        mode: 'cors',
        credentials: 'omit'
      })
        .then(function (response) { return response.ok ? response.json() : null; })
        .then(function (payload) {
          if (activeTarget !== target) return;
          renderTerm(card, fallback, payload || payloadFromPageMetadata(fallback));
          placeCard(card, target);
        })
        .catch(function () {
          if (activeTarget !== target) return;
          renderTerm(card, fallback, payloadFromPageMetadata(fallback));
          placeCard(card, target);
        });
    }

    function scheduleHide() {
      window.clearTimeout(hideTimer);
      hideTimer = window.setTimeout(function () {
        card.hidden = true;
        activeTarget = null;
      }, 150);
    }

    document.addEventListener('mouseover', function (event) {
      var target = event.target && event.target.closest ? event.target.closest('.signal-term') : null;
      if (target) showFor(target);
    });

    document.addEventListener('focusin', function (event) {
      var target = event.target && event.target.closest ? event.target.closest('.signal-term') : null;
      if (target) showFor(target);
    });

    document.addEventListener('mouseout', function (event) {
      var target = event.target && event.target.closest ? event.target.closest('.signal-term') : null;
      if (target && !target.contains(event.relatedTarget) && !card.contains(event.relatedTarget)) scheduleHide();
    });

    document.addEventListener('focusout', function (event) {
      var target = event.target && event.target.closest ? event.target.closest('.signal-term') : null;
      if (target) scheduleHide();
    });

    card.addEventListener('mouseover', function () { window.clearTimeout(hideTimer); });
    card.addEventListener('mouseout', function (event) {
      if (!card.contains(event.relatedTarget)) scheduleHide();
    });

    window.addEventListener('scroll', function () {
      if (!card.hidden) card.hidden = true;
    }, { passive: true });
  }

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