MediaWiki:Common.js

From Titans of Time
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.
/* Any JavaScript here will be loaded for all users on every page load. */
/* Any JavaScript here will be loaded for all users on every page load. */
/* Page previews: hovering a link in a page shows that page's infobox in a card. */
(function () {
    const API = "/wiki/api.php";
    const DELAY = 300;
    const OTHER = /^(Category|File|Help|MediaWiki|Special|Talk|Template|Titans of Time|User|Data):/;

    function start() {
        const content = document.getElementById("mw-content-text");
        if (window.totPagePreviews === true || content === null || !window.matchMedia("(hover: hover)").matches) return;
        window.totPagePreviews = true;

        const known = new Map();
        let timer;
        let shown = null;
        // The browser's own tooltip would cover the card, so the link gives up its title while hovered.
        let titled = null;
        let linkTitle = "";

        const restoreTitle = () => {
            if (titled !== null) titled.setAttribute("title", linkTitle);
            titled = null;
        };

        const card = document.createElement("div");
        card.style.cssText = "position:fixed; z-index:9999; display:none; pointer-events:none; max-width:260px; " +
            "background:#fff; border:1px solid #a2a9b1; border-radius:6px; box-shadow:0 4px 16px rgba(0,0,0,0.3); padding:6px; font-size:13px;";
        document.body.appendChild(card);

        const titleOf = (link) => {
            if (link.classList.contains("new") || link.classList.contains("selflink")) return null;
            const url = new URL(link.href, document.baseURI);
            const title = url.searchParams.get("title");
            if (!/\/wiki\/index\.php$/.test(url.pathname) || title === null || url.searchParams.has("action")) return null;
            const plain = title.replace(/_/g, " ");
            return OTHER.test(plain) ? null : plain;
        };

        const infobox = (title) => {
            if (known.has(title)) return Promise.resolve(known.get(title));
            const ask = `?action=parse&format=json&prop=text&disablelimitreport=1&page=${encodeURIComponent(`Data:${title}/Infobox`)}`;
            return fetch(API + ask).then((response) => response.json()).then((json) => {
                const holder = document.createElement("div");
                holder.innerHTML = json.parse ? json.parse.text["*"] : "";
                const table = holder.querySelector("table.wikitable");
                if (table !== null) {
                    table.style.cssText = "float:none; margin:0; width:100%; max-width:246px;";
                    // The page's own navigation row and an empty picture row say nothing in a card.
                    table.querySelectorAll("tr").forEach((row) => {
                        const empty = row.textContent.trim() === "" && row.querySelector("img") === null;
                        if (empty || row.querySelector('a[title="Overview"]') !== null) row.remove();
                    });
                    table.querySelectorAll("img").forEach((image) => {
                        image.removeAttribute("srcset");
                        image.removeAttribute("width");
                        image.removeAttribute("height");
                        image.style.cssText = "max-width:100%; max-height:170px; width:auto; height:auto;";
                    });
                }
                known.set(title, table);
                return table;
            });
        };

        const place = (x, y) => {
            const box = card.getBoundingClientRect();
            const left = x + 16 + box.width > window.innerWidth ? x - box.width - 16 : x + 16;
            const top = Math.min(y + 16, window.innerHeight - box.height - 8);
            card.style.left = `${Math.max(8, left)}px`;
            card.style.top = `${Math.max(8, top)}px`;
        };

        const hide = () => {
            clearTimeout(timer);
            restoreTitle();
            shown = null;
            card.style.display = "none";
        };

        content.addEventListener("mouseover", (event) => {
            const link = event.target.closest("a");
            if (link === null || link === shown) return;
            const title = titleOf(link);
            if (title === null) return;
            hide();
            shown = link;
            if (link.hasAttribute("title")) {
                titled = link;
                linkTitle = link.title;
                link.removeAttribute("title");
            }
            const {clientX: x, clientY: y} = event;
            timer = setTimeout(() => {
                infobox(title).then((table) => {
                    if (shown !== link) return;
                    if (table === null) {
                        restoreTitle();
                        return;
                    }
                    card.textContent = "";
                    card.appendChild(table);
                    card.style.display = "block";
                    place(x, y);
                    // A picture still loading makes the card taller once it arrives, so place it again then.
                    card.querySelectorAll("img").forEach((image) => {
                        if (!image.complete) image.addEventListener("load", () => {
                            if (shown === link) place(x, y);
                        }, {once: true});
                    });
                }).catch(() => undefined);
            }, DELAY);
        });

        content.addEventListener("mouseout", (event) => {
            const link = event.target.closest("a");
            if (link !== null && !link.contains(event.relatedTarget)) hide();
        });
        window.addEventListener("scroll", hide, {passive: true});
    }

    if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start);
    else start();
})();