All files / src/web panel-keys.js

100% Statements 34/34
100% Branches 6/6
100% Functions 17/17
100% Lines 25/25

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71                    332x 332x                       835x               3377x 321x 298x 297x 1158x 295x       3022x       535x 1393x 1393x           29x         49x 49x 49x 68x 68x 68x 68x   49x 49x 49x 49x    
/**
 * The keys that windows answer while the machine is stopped. Each window declares a table of its
 * keys, in the shape of the Shortcuts table: a row with a `key` runs the action its `run` names;
 * a row with a `description` documents the keys its `combo` names, space separated, for Help, and
 * a `label` puts them on the window's own line of keys. A row with no description is documented
 * by another row naming its key.
 */
export class PanelKeys {
    /** @param {import("./floating-panel.js").PanelArrangement} arrangement whose stacking decides between windows */
    constructor(arrangement) {
        this._arrangement = arrangement;
        this._owners = [];
    }
 
    /**
     * @param {object} owner
     * @param {object[]} owner.table
     * @param {Object<string, function(): void>} owner.actions what each row's `run` names
     * @param {function(): boolean} owner.isLive whether its keys answer now
     * @param {import("./floating-panel.js").FloatingPanel | null} owner.panel the window offering them, or null
     * for keys that yield to any window's
     */
    register(owner) {
        this._owners.push(owner);
    }
 
    /**
     * Runs what `key` is bound to by the live owner offering it whose window is in front.
     * @returns {boolean} whether anything was bound to it
     */
    press(key) {
        const offering = this._owners.filter((owner) => owner.isLive() && owner.table.some((row) => row.key === key));
        if (offering.length === 0) return false;
        const front = this._arrangement.frontmost(offering.map((owner) => owner.panel));
        const owner = offering.find((candidate) => candidate.panel === front);
        owner.actions[owner.table.find((row) => row.key === key).run]();
        return true;
    }
}
 
const documented = (table) => table.filter((row) => row.description);
 
/** A window's line of keys, as "j/k move · b back". */
export function keyLine(table) {
    return documented(table)
        .filter((row) => row.label)
        .map((row) => `${row.combo.replaceAll(" ", "/")} ${row.label}`)
        .join(" · ");
}
 
/** Help's rows for `table`: each documented combo, its keys as key caps, and what they do. */
export function helpRows(table) {
    return documented(table).map(({ combo, description }) => helpRow(combo.split(" "), description));
}
 
/** A row of Help: `keys` as key caps, and what they do. */
export function helpRow(keys, description) {
    const row = document.createElement("tr");
    const caps = document.createElement("td");
    keys.forEach((key, index) => {
        const cap = document.createElement("span");
        cap.className = "key";
        cap.textContent = key;
        caps.append(...(index ? [" ", cap] : [cap]));
    });
    const what = document.createElement("td");
    what.textContent = description;
    row.append(caps, what);
    return row;
}