forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			189 lines
		
	
	
	
		
			6.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			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())
 | |
|     }
 | |
| }
 |