software/hoardy-web/./extension/page/help.js

Passively capture, archive, and hoard your web browsing history, including the contents of the pages you visit, for later offline viewing, replay, mirroring, data scraping, and/or indexing. Your own personal private Wayback Machine that can also archive HTTP POST requests and responses, as well as most other HTTP-level data.

Files

Raw Source

Contents

/*
 * Copyright (c) 2023-2026 Jan Malakhovski <oxij@oxij.org>
 *
 * This file is a part of `hoardy-web` project.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * The "Help" page.
 */

"use strict";

let dbody = document.body;
let pbody;
let iframe;
let minWidth = 1355; // see ./help.template
let columns = true;

// Resize elements to window. This switches between `columns` and `linear` layouts depending on
// width. This part is not done via `CSS` because we use the `columns` value above too.
function resize() {
    let w = window.innerWidth;
    let h = window.innerHeight;

    console.log("current viewport:", w, h);
    columns = w >= minWidth;

    setConditionalClass(dbody, "columns", columns);
    setConditionalClass(dbody, "linear", !columns);

    // Prevent independent scroll in `columns` layout.
    let h1 = columns ? `${h - 5}px` : null;
    pbody.style["min-height"] = pbody.style["max-height"] = pbody.style["height"] = h1;

    let ib = iframe.contentDocument.body;
    if (ib === null) {
        // not yet loaded
        return;
    }

    // Prevent in-iframe scroll in `linear` layout.
    let h2 = columns ? h1 : `${ib.scrollHeight + 20}px`;
    iframe.style["min-height"] = iframe.style["max-height"] = iframe.style["height"] = h2;
}

function updateLinks(node) {
    classifyDocumentLinks(
        node,
        [
            ["/page/help.html", "internal"],
            ["/page/popup.html", "popup"],
            ["/", "local"],
        ],
        (link, info) => {
            let klass = info.klass;
            if (klass !== undefined) {
                link.classList.add(klass);
            }

            switch (info.klass) {
                case "internal":
                    link.onclick = (event) => {
                        event.stopPropagation();
                        event.preventDefault();
                        historyFromTo({ id: info.id }, { id: info.target });
                        focusNode(info.target);
                    };
                    link.onmouseover = (_event) => {
                        if (columns) {
                            broadcastToPopup("highlightNode", null);
                        }
                    };
                    break;
                case "popup":
                    link.onclick = (event) => {
                        event.stopPropagation();
                        event.preventDefault();
                        if (!columns) {
                            historyFromTo({ id: info.id }, info.href);
                        }
                        broadcastToPopup("focusNode", info.target);
                    };
                    link.onmouseover = (_event) => {
                        if (columns) {
                            broadcastToPopup("focusNode", info.target);
                        }
                    };
                    break;
                default:
                    link.onclick = (event) => {
                        event.stopPropagation();
                        historyFromTo({ id: info.id });
                    };
                    link.onmouseover = (_event) => {
                        if (columns) {
                            broadcastToPopup("highlightNode", null);
                        }
                    };
            }
        },
    );
}

// no corresponding popup UI elements
let noPopup = new Set([
    "_execute_browser_action",
    "showEveryLog",
    "showWindowLog",
    "showTabLog",
    "closeTabThenDiscardInLimbo",
]);

// only set on Chromium
let firefoxCommands;

async function updatePage(initial) {
    // generate shortcuts table
    let rows = [];

    let shortcuts = await getShortcuts(firefoxCommands);
    for (let [name, shortcut] of Object.entries(shortcuts)) {
        let pointer = name.startsWith("toggleTabConfig")
            ? mapShortcutName(
                  (name, children) => "genui-tabconfig." + (children ? "children." : "") + name,
                  name,
              )
            : name;

        let desc = shortcut.description;
        let [sdesc, ldesc] = desc.split(" : ");
        shortcut.sdesc = sdesc;
        shortcut.ldesc = ldesc;

        let cur = shortcut.shortcut || "unbound";
        shortcut.default = shortcut.suggested_key
            ? shortcut.suggested_key.default || "unbound"
            : "unbound";
        let def = shortcut.default;

        let tr = document.createElement("tr");
        tr.id = `shortcut-${name}`;
        let s = document.createElement("span");

        if (noPopup.has(name)) {
            if (ldesc !== undefined) {
                s.innerHTML = sdesc + ": " + microMarkdownToHTML(ldesc);
            } else {
                s.innerHTML = microMarkdownToHTML(desc);
            }
            appendElements(tr, "td", s);
        } else {
            s.innerHTML = ": " + microMarkdownToHTML(ldesc);
            appendElements(tr, "td", [
                [
                    (e) => {
                        e.href = `./popup.html#${pointer}`;
                        return e;
                    },
                    "a",
                    sdesc,
                ],
                [s],
            ]);
        }
        appendElements(tr, "td", cur);
        appendElements(
            tr,
            "td",
            "div",
            cur === def
                ? "ditto"
                : browser.commands !== undefined && browser.commands.update !== undefined
                  ? createButton(def, "Reset to default", async () => {
                        await browser.commands.update({ name, shortcut: def });
                        await updatePage(false);
                    })
                  : def,
        );

        rows.push(tr);
    }

    let tbody = document.getElementById("tbody-sk");
    tbody.replaceChildren(...rows);

    if (!initial) {
        updateLinks(tbody);
    }

    // expand shortcut macros
    function mapShortcuts(func, nodes) {
        for (let node of nodes) {
            let sname = node.getAttribute("data-macro-shortcut");
            if (sname === null) {
                console.error("bad macro", node);
                continue;
            }
            let shortcut = shortcuts[sname];
            if (!shortcut) {
                console.error("missing shortcut", sname);
                continue;
            }
            func(node, shortcut);
        }
    }

    mapShortcuts((node, shortcut) => {
        node.children[0].innerText = shortcut.shortcut || "unbound";
    }, document.getElementsByName("shortcutKey"));

    mapShortcuts((node, shortcut) => {
        node.children[0].innerText = shortcut.sdesc;
    }, document.getElementsByName("shortcutShort"));
}

async function helpMain() {
    setPageLoading();

    pbody = document.getElementById("body");
    iframe = document.getElementById("iframe");

    // allow to un-highlight currently highlighted node
    pbody.addEventListener("click", (_event) => {
        highlightNode(null);
        broadcastToPopup("highlightNode", null);
    });

    setupHistoryPopState();

    if (useDebugger) {
        firefoxCommands = await fetch(
            browser.runtime.getURL("/manifest-commands-firefox.json"),
        ).then((result) => result.json());
    }

    if (browser.commands !== undefined && browser.commands.onChanged !== undefined) {
        browser.commands.onChanged.addListener(() => {
            resetSingletonTimeout(scheduledUI, "updatePage", 300, () => updatePage(false));
        });
    }
    await updatePage(true);

    updateLinks(document);

    async function processUpdate(update) {
        let [what, data] = update;
        switch (what) {
            case "updateConfig":
                setRootClasses(data);
                break;
            case "popupResized":
                resize();
                break;
            default:
                await webextRPCHandleMessageDefault(update);
        }
    }

    // add default handlers
    await subscribeToExtensionSimple("help", 3, processUpdate);

    {
        let config = await browser.runtime.sendMessage(["getConfig"]);
        setRootClasses(config);

        if (config.debugRuntime) {
            setTimeout(
                () =>
                    verifyLinks(document, console.error, true, undefined, (hashlessHref, id) => {
                        if (hashlessHref === popupPageURL && id.startsWith("genui-")) {
                            return id.substr(6);
                        }
                        return id;
                    }),
                100,
            );
        }
    }

    window.onresize = catchAll(resize);
    catchAll(resize)();

    // give it a chance to re-compute the layout
    await sleep(1);

    // show UI
    pbody.style["visibility"] = null;
    document.getElementById("container").style["visibility"] = null;

    // finish
    setPageDone();

    // highlight current target
    focusHashNode();
}

document.addEventListener("DOMContentLoaded", () => helpMain().catch(setPageError), setPageError);