/*\ title: $:/core/modules/keyboard.js type: application/javascript module-type: global Keyboard handling utilities \*/ (function(){ /*jslint node: true, browser: true */ /*global $tw: false */ "use strict"; var namedKeys = { "cancel": 3, "help": 6, "backspace": 8, "tab": 9, "clear": 12, "return": 13, "enter": 13, "pause": 19, "escape": 27, "space": 32, "page_up": 33, "page_down": 34, "end": 35, "home": 36, "left": 37, "up": 38, "right": 39, "down": 40, "printscreen": 44, "insert": 45, "delete": 46, "0": 48, "1": 49, "2": 50, "3": 51, "4": 52, "5": 53, "6": 54, "7": 55, "8": 56, "9": 57, "firefoxsemicolon": 59, "firefoxequals": 61, "a": 65, "b": 66, "c": 67, "d": 68, "e": 69, "f": 70, "g": 71, "h": 72, "i": 73, "j": 74, "k": 75, "l": 76, "m": 77, "n": 78, "o": 79, "p": 80, "q": 81, "r": 82, "s": 83, "t": 84, "u": 85, "v": 86, "w": 87, "x": 88, "y": 89, "z": 90, "numpad0": 96, "numpad1": 97, "numpad2": 98, "numpad3": 99, "numpad4": 100, "numpad5": 101, "numpad6": 102, "numpad7": 103, "numpad8": 104, "numpad9": 105, "multiply": 106, "add": 107, "separator": 108, "subtract": 109, "decimal": 110, "divide": 111, "f1": 112, "f2": 113, "f3": 114, "f4": 115, "f5": 116, "f6": 117, "f7": 118, "f8": 119, "f9": 120, "f10": 121, "f11": 122, "f12": 123, "f13": 124, "f14": 125, "f15": 126, "f16": 127, "f17": 128, "f18": 129, "f19": 130, "f20": 131, "f21": 132, "f22": 133, "f23": 134, "f24": 135, "firefoxminus": 173, "semicolon": 186, "equals": 187, "comma": 188, "dash": 189, "period": 190, "slash": 191, "backquote": 192, "openbracket": 219, "backslash": 220, "closebracket": 221, "quote": 222 }; function KeyboardManager(options) { var self = this; options = options || ""; // Save the named key hashmap this.namedKeys = namedKeys; // Create a reverse mapping of code to keyname this.keyNames = []; $tw.utils.each(namedKeys,function(keyCode,name) { self.keyNames[keyCode] = name.substr(0,1).toUpperCase() + name.substr(1); }); // Save the platform-specific name of the "meta" key this.metaKeyName = $tw.platform.isMac ? "cmd-" : "win-"; this.shortcutKeysList = [], // Stores the shortcut-key descriptors this.shortcutActionList = [], // Stores the corresponding action strings this.shortcutParsedList = []; // Stores the parsed key descriptors this.shortcutPriorityList = []; // Stores the parsed shortcut priority this.lookupNames = ["shortcuts"]; this.lookupNames.push($tw.platform.isMac ? "shortcuts-mac" : "shortcuts-not-mac") this.lookupNames.push($tw.platform.isWindows ? "shortcuts-windows" : "shortcuts-not-windows"); this.lookupNames.push($tw.platform.isLinux ? "shortcuts-linux" : "shortcuts-not-linux"); this.updateShortcutLists(this.getShortcutTiddlerList()); $tw.wiki.addEventListener("change",function(changes) { self.handleShortcutChanges(changes); }); } /* Return an array of keycodes for the modifier keys ctrl, shift, alt, meta */ KeyboardManager.prototype.getModifierKeys = function() { return [ 16, // Shift 17, // Ctrl 18, // Alt 20, // CAPS LOCK 91, // Meta (left) 93, // Meta (right) 224 // Meta (Firefox) ] }; /* Parses a key descriptor into the structure: { keyCode: numeric keycode shiftKey: boolean altKey: boolean ctrlKey: boolean metaKey: boolean } Key descriptors have the following format: ctrl+enter ctrl+shift+alt+A */ KeyboardManager.prototype.parseKeyDescriptor = function(keyDescriptor,options) { var components = keyDescriptor.split(/\+|\-/), info = { keyCode: 0, shiftKey: false, altKey: false, ctrlKey: false, metaKey: false }; for(var t=0; t<components.length; t++) { var s = components[t].toLowerCase(), c = s.charCodeAt(0); // Look for modifier keys if(s === "ctrl") { info.ctrlKey = true; } else if(s === "shift") { info.shiftKey = true; } else if(s === "alt") { info.altKey = true; } else if(s === "meta" || s === "cmd" || s === "win") { info.metaKey = true; } // Replace named keys with their code if(this.namedKeys[s]) { info.keyCode = this.namedKeys[s]; } } if(options.keyDescriptor) { info.keyDescriptor = options.keyDescriptor; } if(info.keyCode) { return info; } else { return null; } }; /* Parse a list of key descriptors into an array of keyInfo objects. The key descriptors can be passed as an array of strings or a space separated string */ KeyboardManager.prototype.parseKeyDescriptors = function(keyDescriptors,options) { var self = this; options = options || {}; options.stack = options.stack || []; var wiki = options.wiki || $tw.wiki; if(typeof keyDescriptors === "string" && keyDescriptors === "") { return []; } if(!$tw.utils.isArray(keyDescriptors)) { keyDescriptors = keyDescriptors.split(" "); } var result = []; $tw.utils.each(keyDescriptors,function(keyDescriptor) { // Look for a named shortcut if(keyDescriptor.substr(0,2) === "((" && keyDescriptor.substr(-2,2) === "))") { if(options.stack.indexOf(keyDescriptor) === -1) { options.stack.push(keyDescriptor); var name = keyDescriptor.substring(2,keyDescriptor.length - 2), lookupName = function(configName) { var keyDescriptors = wiki.getTiddlerText("$:/config/" + configName + "/" + name); if(keyDescriptors) { options.keyDescriptor = keyDescriptor; result.push.apply(result,self.parseKeyDescriptors(keyDescriptors,options)); } }; $tw.utils.each(self.lookupNames,function(platformDescriptor) { lookupName(platformDescriptor); }); } } else { result.push(self.parseKeyDescriptor(keyDescriptor,options)); } }); return result; }; KeyboardManager.prototype.getPrintableShortcuts = function(keyInfoArray) { var self = this, result = []; $tw.utils.each(keyInfoArray,function(keyInfo) { if(keyInfo) { result.push((keyInfo.ctrlKey ? "ctrl-" : "") + (keyInfo.shiftKey ? "shift-" : "") + (keyInfo.altKey ? "alt-" : "") + (keyInfo.metaKey ? self.metaKeyName : "") + (self.keyNames[keyInfo.keyCode])); } }); return result; } KeyboardManager.prototype.checkKeyDescriptor = function(event,keyInfo) { return keyInfo && event.keyCode === keyInfo.keyCode && event.shiftKey === keyInfo.shiftKey && event.altKey === keyInfo.altKey && event.ctrlKey === keyInfo.ctrlKey && event.metaKey === keyInfo.metaKey; }; KeyboardManager.prototype.checkKeyDescriptors = function(event,keyInfoArray) { return (this.getMatchingKeyDescriptor(event,keyInfoArray) !== null); }; KeyboardManager.prototype.getMatchingKeyDescriptor = function(event,keyInfoArray) { for(var t=0; t<keyInfoArray.length; t++) { if(this.checkKeyDescriptor(event,keyInfoArray[t])) { return keyInfoArray[t]; } } return null; }; KeyboardManager.prototype.getEventModifierKeyDescriptor = function(event) { return event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey ? "ctrl" : event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey ? "shift" : event.ctrlKey && event.shiftKey && !event.altKey && !event.metaKey ? "ctrl-shift" : event.altKey && !event.shiftKey && !event.ctrlKey && !event.metaKey ? "alt" : event.altKey && event.shiftKey && !event.ctrlKey && !event.metaKey ? "alt-shift" : event.altKey && event.ctrlKey && !event.shiftKey && !event.metaKey ? "ctrl-alt" : event.altKey && event.shiftKey && event.ctrlKey && !event.metaKey ? "ctrl-alt-shift" : event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey ? "meta" : event.metaKey && event.ctrlKey && !event.shiftKey && !event.altKey ? "meta-ctrl" : event.metaKey && event.ctrlKey && event.shiftKey && !event.altKey ? "meta-ctrl-shift" : event.metaKey && event.ctrlKey && event.shiftKey && event.altKey ? "meta-ctrl-alt-shift" : "normal"; }; KeyboardManager.prototype.getShortcutTiddlerList = function() { return $tw.wiki.getTiddlersWithTag("$:/tags/KeyboardShortcut"); }; KeyboardManager.prototype.updateShortcutLists = function(tiddlerList) { this.shortcutTiddlers = tiddlerList; for(var i=0; i<tiddlerList.length; i++) { var title = tiddlerList[i], tiddlerFields = $tw.wiki.getTiddler(title).fields; this.shortcutKeysList[i] = tiddlerFields.key !== undefined ? tiddlerFields.key : undefined; this.shortcutActionList[i] = tiddlerFields.text; this.shortcutParsedList[i] = this.shortcutKeysList[i] !== undefined ? this.parseKeyDescriptors(this.shortcutKeysList[i]) : undefined; this.shortcutPriorityList[i] = tiddlerFields.priority === "yes" ? true : false; } }; /* event: the keyboard event object options: onlyPriority: true if only priority global shortcuts should be invoked */ KeyboardManager.prototype.handleKeydownEvent = function(event, options) { options = options || {}; var key, action; for(var i=0; i<this.shortcutTiddlers.length; i++) { if(options.onlyPriority && this.shortcutPriorityList[i] !== true) { continue; } if(this.shortcutParsedList[i] !== undefined && this.checkKeyDescriptors(event,this.shortcutParsedList[i])) { key = this.shortcutParsedList[i]; action = this.shortcutActionList[i]; } } if(key !== undefined) { event.preventDefault(); event.stopPropagation(); $tw.rootWidget.invokeActionString(action,$tw.rootWidget,event); return true; } return false; }; KeyboardManager.prototype.detectNewShortcuts = function(changedTiddlers) { var shortcutConfigTiddlers = [], handled = false; $tw.utils.each(this.lookupNames,function(platformDescriptor) { var descriptorString = "$:/config/" + platformDescriptor + "/"; Object.keys(changedTiddlers).forEach(function(configTiddler) { var configString = configTiddler.substr(0, configTiddler.lastIndexOf("/") + 1); if(configString === descriptorString) { shortcutConfigTiddlers.push(configTiddler); handled = true; } }); }); if(handled) { return $tw.utils.hopArray(changedTiddlers,shortcutConfigTiddlers); } else { return false; } }; KeyboardManager.prototype.handleShortcutChanges = function(changedTiddlers) { var newList = this.getShortcutTiddlerList(); var hasChanged = $tw.utils.hopArray(changedTiddlers,this.shortcutTiddlers) ? true : ($tw.utils.hopArray(changedTiddlers,newList) ? true : (this.detectNewShortcuts(changedTiddlers)) ); // Re-cache shortcuts if something changed if(hasChanged) { this.updateShortcutLists(newList); } }; exports.KeyboardManager = KeyboardManager; })();