keymap.ts (4634B)
1 import {base, keyName} from "w3c-keyname" 2 import {Plugin, Command} from "prosemirror-state" 3 import {EditorView} from "prosemirror-view" 4 5 const mac = typeof navigator != "undefined" && /Mac|iP(hone|[oa]d)/.test(navigator.platform) 6 const windows = typeof navigator != "undefined" && /Win/.test(navigator.platform) 7 8 function normalizeKeyName(name: string) { 9 let parts = name.split(/-(?!$)/), result = parts[parts.length - 1] 10 if (result == "Space") result = " " 11 let alt, ctrl, shift, meta 12 for (let i = 0; i < parts.length - 1; i++) { 13 let mod = parts[i] 14 if (/^(cmd|meta|m)$/i.test(mod)) meta = true 15 else if (/^a(lt)?$/i.test(mod)) alt = true 16 else if (/^(c|ctrl|control)$/i.test(mod)) ctrl = true 17 else if (/^s(hift)?$/i.test(mod)) shift = true 18 else if (/^mod$/i.test(mod)) { if (mac) meta = true; else ctrl = true } 19 else throw new Error("Unrecognized modifier name: " + mod) 20 } 21 if (alt) result = "Alt-" + result 22 if (ctrl) result = "Ctrl-" + result 23 if (meta) result = "Meta-" + result 24 if (shift) result = "Shift-" + result 25 return result 26 } 27 28 function normalize(map: {[key: string]: Command}) { 29 let copy: {[key: string]: Command} = Object.create(null) 30 for (let prop in map) copy[normalizeKeyName(prop)] = map[prop] 31 return copy 32 } 33 34 function modifiers(name: string, event: KeyboardEvent, shift = true) { 35 if (event.altKey) name = "Alt-" + name 36 if (event.ctrlKey) name = "Ctrl-" + name 37 if (event.metaKey) name = "Meta-" + name 38 if (shift && event.shiftKey) name = "Shift-" + name 39 return name 40 } 41 42 /// Create a keymap plugin for the given set of bindings. 43 /// 44 /// Bindings should map key names to [command](#commands)-style 45 /// functions, which will be called with `(EditorState, dispatch, 46 /// EditorView)` arguments, and should return true when they've handled 47 /// the key. Note that the view argument isn't part of the command 48 /// protocol, but can be used as an escape hatch if a binding needs to 49 /// directly interact with the UI. 50 /// 51 /// Key names may be strings like `"Shift-Ctrl-Enter"`—a key 52 /// identifier prefixed with zero or more modifiers. Key identifiers 53 /// are based on the strings that can appear in 54 /// [`KeyEvent.key`](https:///developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key). 55 /// Use lowercase letters to refer to letter keys (or uppercase letters 56 /// if you want shift to be held). You may use `"Space"` as an alias 57 /// for the `" "` name. 58 /// 59 /// Modifiers can be given in any order. `Shift-` (or `s-`), `Alt-` (or 60 /// `a-`), `Ctrl-` (or `c-` or `Control-`) and `Cmd-` (or `m-` or 61 /// `Meta-`) are recognized. For characters that are created by holding 62 /// shift, the `Shift-` prefix is implied, and should not be added 63 /// explicitly. 64 /// 65 /// You can use `Mod-` as a shorthand for `Cmd-` on Mac and `Ctrl-` on 66 /// other platforms. 67 /// 68 /// You can add multiple keymap plugins to an editor. The order in 69 /// which they appear determines their precedence (the ones early in 70 /// the array get to dispatch first). 71 export function keymap(bindings: {[key: string]: Command}): Plugin { 72 return new Plugin({props: {handleKeyDown: keydownHandler(bindings)}}) 73 } 74 75 /// Given a set of bindings (using the same format as 76 /// [`keymap`](#keymap.keymap)), return a [keydown 77 /// handler](#view.EditorProps.handleKeyDown) that handles them. 78 export function keydownHandler(bindings: {[key: string]: Command}): (view: EditorView, event: KeyboardEvent) => boolean { 79 let map = normalize(bindings) 80 return function(view, event) { 81 let name = keyName(event), baseName, direct = map[modifiers(name, event)] 82 if (direct && direct(view.state, view.dispatch, view)) return true 83 // A character key 84 if (name.length == 1 && name != " ") { 85 if (event.shiftKey) { 86 // In case the name was already modified by shift, try looking 87 // it up without its shift modifier 88 let noShift = map[modifiers(name, event, false)] 89 if (noShift && noShift(view.state, view.dispatch, view)) return true 90 } 91 if ((event.altKey || event.metaKey || event.ctrlKey) && 92 // Ctrl-Alt may be used for AltGr on Windows 93 !(windows && event.ctrlKey && event.altKey) && 94 (baseName = base[event.keyCode]) && baseName != name) { 95 // Try falling back to the keyCode when there's a modifier 96 // active or the character produced isn't ASCII, and our table 97 // produces a different name from the the keyCode. See #668, 98 // #1060, #1529. 99 let fromCode = map[modifiers(baseName, event)] 100 if (fromCode && fromCode(view.state, view.dispatch, view)) return true 101 } 102 } 103 return false 104 } 105 }