All files / src/web keyboard-setup.js

93.1% Statements 54/58
84% Branches 21/25
86.36% Functions 19/22
95.45% Lines 42/44

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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102            1x   1x   1x   1x       1x                             25x   118x 378x       25x 25x 25x 25x         150x 166x 83x 83x     25x 25x 29x     7x 9x 8x 30x   25x 225x 175x   25x     172x       25x   150x                 200x 200x   25x 378x 378x   25x 119x 25x 66x 66x 48x 66x        
import { Keyboard } from "./keyboard.js";
import { showNotice } from "./reporting.js";
import { noteEvent } from "./analytics.js";
import { keyCodes } from "../keymap.js";
import { Shortcuts } from "./shortcuts.js";
 
const PasteBoxId = "paste-text";
/** The eight accessibility switches, on the number keys. */
const SwitchKeys = ["K1", "K2", "K3", "K4", "K5", "K6", "K7", "K8"];
 
const TypingTargets = 'input, textarea, select, [contenteditable]:not([contenteditable="false"])';
// Where keys are for the page, not the machine: the paste box, and the media window's controls.
const KeyboardSinks = `#${PasteBoxId}, #media-panel`;
// The media window's own shortcuts have to answer from inside it, or it cannot be re-aimed from
// the keyboard at all. The paste box keeps every key, because on a Mac Alt is Option and types
// an accented character rather than nothing.
const ShortcutSinks = `#${PasteBoxId}`;
 
/**
 * Builds the emulated keyboard and wires the browser's shortcuts around it,
 * exposing it as `keyboard` for whoever needs the machine's keys.
 */
export class KeyboardSetup {
    /**
     * @param {object} opts
     * @param {object} opts.actions what each shortcut does, supplied late-bound:
     *   toggleDebugger, toggleFast, openRewind, openPrinter, openMedia,
     *   pause, resume, paste, onAnyKeyDown
     * @param {import("./accessibility-switches.js").AccessibilitySwitches} opts.accessibilitySwitches
     */
    constructor({ actions, accessibilitySwitches, processor, dbgr, keyLayout }) {
        const keyboard = (this.keyboard = new Keyboard({
            processor,
            inputEnabledFunction: () => !!document.activeElement?.closest(KeyboardSinks),
            shortcutsBlockedFunction: () => !!document.activeElement?.closest(ShortcutSinks),
            keyLayout,
            dbgr,
        }));
        keyboard.addEventListener("notice", showNotice);
        keyboard.addEventListener("pause", () => actions.pause());
        keyboard.addEventListener("resume", () => actions.resume());
        keyboard.addEventListener("break", (e) => {
            // F12/Break: Reset processor
            if (e.detail) noteEvent("keyboard", "press", "break");
        });
 
        const onDown = (note, action) => (down) => {
            if (down) {
                if (note) noteEvent("keyboard", "press", note);
                action();
            }
        };
        const alt = { alt: true, ctrl: false };
        const runners = {
            toggleDebugger: () => actions.toggleDebugger(),
            // `running` is pushed in from the loop, so it stays true whatever stopped the machine.
            togglePause: () => (keyboard.running ? keyboard.pauseEmulation() : keyboard.resumeEmulation()),
            toggleFast: () => actions.toggleFast(),
            openPrinter: () => actions.openPrinter(),
            openRewind: () => actions.openRewind(),
            openMediaTape: () => actions.openMedia("tape"),
        };
        for (const shortcut of Shortcuts) {
            if (!shortcut.key) continue;
            if (shortcut.run === "openMediaDrive") {
                // The only shortcut whose shift state changes what it does, rather than which key it is.
                keyboard.registerKeyHandler(
                    keyCodes[shortcut.key],
                    (down, _code, shift) => {
                        if (down) actions.openMedia(shift ? 1 : 0);
                    },
                    alt,
                );
                continue;
            }
            keyboard.registerKeyHandler(
                keyCodes[shortcut.key],
                onDown(shortcut.note ?? null, runners[shortcut.run]),
                alt,
            );
        }
 
        // Alt means the underlying key is never forwarded to the BBC Micro (keyboard.js bails
        // out early when a handler fires), so typing numbers works normally.
        const handleSwitch = (index) => (down) => accessibilitySwitches.setSwitch(index, down);
        SwitchKeys.forEach((name, index) => keyboard.registerKeyHandler(keyCodes[name], handleSwitch(index), alt));
 
        document.addEventListener("keydown", (evt) => {
            actions.onAnyKeyDown();
            keyboard.keyDown(evt);
        });
        document.addEventListener("keypress", (evt) => keyboard.keyPress(evt));
        document.addEventListener("keyup", (evt) => keyboard.keyUp(evt));
        document.addEventListener("paste", (evt) => {
            const target = document.activeElement;
            if (target && target.id !== PasteBoxId && target.matches(TypingTargets)) return;
            const text = evt.clipboardData?.getData("text/plain");
            if (text) actions.paste(text);
        });
    }
}