MapComplete/src/UI/Base/Hotkeys.ts

189 lines
6.6 KiB
TypeScript

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<string> = 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())
}
}