import { Utils } from "../../Utils" import { UIEventSource } from "../../Logic/UIEventSource" import { Translation } from "../i18n/Translation" import Translations from "../i18n/Translations" import MarkdownUtils from "../../Utils/MarkdownUtils" import Locale from "../i18n/Locale" export default class Hotkeys { public static readonly _docs: UIEventSource< { key: { ctrl?: string; shift?: string; alt?: string; nomod?: string; onUp?: boolean } documentation: string | Translation alsoTriggeredBy: Translation[] }[] > = new UIEventSource([]) private static readonly seenKeys: Set = new Set() /** * Register a hotkey * @param key * @param documentation * @param action the function to run. It might return 'false', indicating that it didn't do anything and gives control back to the default flow * @constructor */ public static RegisterHotkey( key: ( | { ctrl: string } | { shift: string } | { alt: string } | { nomod: string } ) & { onUp?: boolean }, documentation: string | Translation, action: () => void | false, alsoTriggeredBy?: Translation[] ) { const type = key["onUp"] ? "keyup" : "keypress" let keycode: string = key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"] if (keycode.length == 1) { keycode = keycode.toLowerCase() if (key["shift"] !== undefined) { keycode = keycode.toUpperCase() } } const keyString = JSON.stringify(key) this.seenKeys.add(keyString) this._docs.data.push({ key, documentation, alsoTriggeredBy }) this._docs.ping() if (Utils.runningFromConsole) { return } if (key["ctrl"] !== undefined) { document.addEventListener("keydown", function (event) { if (event.ctrlKey && event.key === keycode) { if (action() !== false) { event.preventDefault() } } }) } else if (key["shift"] !== undefined) { document.addEventListener(type, function (event) { if (Hotkeys.textElementSelected(event)) { // A text element is selected, we don't do anything special return } if (event.shiftKey && event.key === keycode) { if (action() !== false) { event.preventDefault() } } }) } else if (key["alt"] !== undefined) { document.addEventListener(type, function (event) { if (event.altKey && event.key === keycode) { if (action() !== false) { event.preventDefault() } } }) } else if (key["nomod"] !== undefined) { document.addEventListener(type, function (event) { if (Hotkeys.textElementSelected(event) && keycode !== "Escape") { // A text element is selected, we don't do anything special return } if (event.key === keycode) { const result = action() if (result !== false) { event.preventDefault() } } }) } } static prepareDocumentation( docs: { key: { ctrl?: string; shift?: string; alt?: string; nomod?: string; onUp?: boolean } documentation: string | Translation alsoTriggeredBy: Translation[] }[] ) { let byKey: [string, string | Translation, Translation[] | undefined][] = docs .map(({ key, documentation, alsoTriggeredBy }) => { const modifiers = Object.keys(key).filter((k) => k !== "nomod" && k !== "onUp") let keycode: string = key["ctrl"] ?? key["shift"] ?? key["alt"] ?? key["nomod"] if (keycode.length == 1) { keycode = keycode.toUpperCase() } if (keycode === " ") { keycode = "Spacebar" } modifiers.push(keycode) return <[string, string | Translation, Translation[] | undefined]>[ modifiers.join("+"), documentation, alsoTriggeredBy, ] }) .sort() byKey = Utils.NoNull(byKey) for (let i = byKey.length - 1; i > 0; i--) { if (byKey[i - 1][0] === byKey[i][0]) { byKey.splice(i, 1) } } return byKey } static generateDocumentationFor( docs: { key: { ctrl?: string; shift?: string; alt?: string; nomod?: string; onUp?: boolean } documentation: string | Translation alsoTriggeredBy: Translation[] }[], language: string ): string { const tr = Translations.t.hotkeyDocumentation function t(t: Translation | string) { if (typeof t === "string") { return t } return t.textFor(language) } const contents: string[][] = this.prepareDocumentation(docs).map( ([key, doc, alsoTriggeredBy]) => { let keyEl: string = [key, ...(alsoTriggeredBy ?? [])] .map((k) => "`" + t(k) + "`") .join(" ") return [keyEl, t(doc)] } ) return [ "# " + t(tr.title), t(tr.intro), MarkdownUtils.table([t(tr.key), t(tr.action)], contents), ].join("\n") } public static generateDocumentation(language?: string) { return Hotkeys.generateDocumentationFor( Hotkeys._docs.data, language ?? Locale.language.data ) } private static textElementSelected(event: KeyboardEvent): boolean { if (event.ctrlKey || event.altKey) { // This is an event with a modifier-key, lets not ignore it return false } if (event.key === "Escape") { return false // Another not-printable character that should not be ignored } return ["input", "textarea"].includes(document?.activeElement?.tagName?.toLowerCase()) } }