import {base, keyName} from "w3c-keyname" import {Plugin} from "../../state/src" import {EditorView} from "../../view/src" export type Command = (view: EditorView) => boolean const mac = typeof navigator != "undefined" ? /Mac/.test(navigator.platform) : false function normalizeKeyName(name: string): string { const parts = name.split(/-(?!$)/) let result = parts[parts.length - 1] if (result == "Space") result = " " let alt, ctrl, shift, meta for (let i = 0; i < parts.length - 1; ++i) { const mod = parts[i] if (/^(cmd|meta|m)$/i.test(mod)) meta = true else if (/^a(lt)?$/i.test(mod)) alt = true else if (/^(c|ctrl|control)$/i.test(mod)) ctrl = true else if (/^s(hift)?$/i.test(mod)) shift = true else if (/^mod$/i.test(mod)) { if (mac) meta = true; else ctrl = true } else throw new Error("Unrecognized modifier name: " + mod) } if (alt) result = "Alt-" + result if (ctrl) result = "Ctrl-" + result if (meta) result = "Meta-" + result if (shift) result = "Shift-" + result return result } function normalize(map: {[key: string]: Command}): {[key: string]: Command} { const copy = Object.create(null) for (const prop in map) copy[normalizeKeyName(prop)] = map[prop] return copy } function modifiers(name: string, event: KeyboardEvent, shift: boolean) { if (event.altKey) name = "Alt-" + name if (event.ctrlKey) name = "Ctrl-" + name if (event.metaKey) name = "Meta-" + name if (shift !== false && event.shiftKey) name = "Shift-" + name return name } // :: (Object) → Plugin // Create a keymap plugin for the given set of bindings. // // Bindings should map key names to [command](#commands)-style // functions, which will be called with `(EditorState, dispatch, // EditorView)` arguments, and should return true when they've handled // the key. Note that the view argument isn't part of the command // protocol, but can be used as an escape hatch if a binding needs to // directly interact with the UI. // // Key names may be strings like `"Shift-Ctrl-Enter"`—a key // identifier prefixed with zero or more modifiers. Key identifiers // are based on the strings that can appear in // [`KeyEvent.key`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key). // Use lowercase letters to refer to letter keys (or uppercase letters // if you want shift to be held). You may use `"Space"` as an alias // for the `" "` name. // // Modifiers can be given in any order. `Shift-` (or `s-`), `Alt-` (or // `a-`), `Ctrl-` (or `c-` or `Control-`) and `Cmd-` (or `m-` or // `Meta-`) are recognized. For characters that are created by holding // shift, the `Shift-` prefix is implied, and should not be added // explicitly. // // You can use `Mod-` as a shorthand for `Cmd-` on Mac and `Ctrl-` on // other platforms. // // You can add multiple keymap plugins to an editor. The order in // which they appear determines their precedence (the ones early in // the array get to dispatch first). export function keymap(bindings: {[key: string]: Command}): Plugin { let keydown = keydownHandler(bindings) return new Plugin({ view() { return {handleDOMEvents: {keydown}} } }) } // :: (Object) → (view: EditorView, event: dom.Event) → bool // Given a set of bindings (using the same format as // [`keymap`](#keymap.keymap), return a [keydown // handler](#view.EditorProps.handleKeyDown) handles them. export function keydownHandler(bindings: {[key: string]: Command}): (view: EditorView, event: KeyboardEvent) => boolean { const map = normalize(bindings) return function(view, event) { const name = keyName(event), isChar = name.length == 1 && name != " " const direct = map[modifiers(name, event, !isChar)] let baseName if (direct && direct(view)) return true if (isChar && (event.shiftKey || event.altKey || event.metaKey) && (baseName = base[event.keyCode]) && baseName != name) { const fromCode = map[modifiers(baseName, event, true)] if (fromCode && fromCode(view)) return true } return false } }