From f1af0e5efff0f8f7955115f2dc591e0c0d519dc4 Mon Sep 17 00:00:00 2001 From: "Alex A. Naanou" Date: Thu, 12 Jan 2017 02:43:17 +0300 Subject: [PATCH] migrated to gen2... Signed-off-by: Alex A. Naanou --- keyboard.js | 1619 +++++++++++++++++++++++---------------------------- 1 file changed, 735 insertions(+), 884 deletions(-) diff --git a/keyboard.js b/keyboard.js index eff5318..cfcfd72 100755 --- a/keyboard.js +++ b/keyboard.js @@ -7,23 +7,21 @@ (function(require){ var module={} // make module AMD/node compatible... /*********************************************************************/ +var object = require('lib/object') + /*********************************************************************/ -// Attributes to be ignored my the key handler... -// -// These are used for system tasks. -var KEYBOARD_SYSTEM_ATTRS = -module.KEYBOARD_SYSTEM_ATTRS = [ - 'doc', - 'title', - 'ignore', - 'pattern' -] +var MODIFIERS = +module.MODIFIERS = [ 'ctrl', 'meta', 'alt', 'shift' ] -// Neither _SPECIAL_KEYS nor _KEY_CODES are meant for direct access, use +var KEY_SEPARATORS = +module.KEY_SEPARATORS = ['+', '-', '_'] + + +// Neither SPECIAL_KEYS nor KEY_CODES are meant for direct access, use // toKeyName() and toKeyCode() for a more uniform access. // // NOTE: these are un-shifted ASCII key names rather than actual key @@ -34,8 +32,8 @@ module.KEYBOARD_SYSTEM_ATTRS = [ // NOTE: the lower case letters are accessible by adding 32 to the // capital key code. // NOTE: don't understand why am I the one who has to write this... -var _SPECIAL_KEYS = -module._SPECIAL_KEYS = { +var SPECIAL_KEYS = +module.SPECIAL_KEYS = { // Special Keys... 9: 'Tab', 33: 'PgUp', 45: 'Ins', 13: 'Enter', 34: 'PgDown', 46: 'Del', @@ -77,8 +75,8 @@ module._SPECIAL_KEYS = { } -var _SHIFT_KEYS = -module._SHIFT_KEYS = { +var SHIFT_KEYS = +module.SHIFT_KEYS = { '`': '~', '-': '_', '=':'+', '#1': '!', '#2': '@', '#3': '#', '#4': '$', '#5': '%', @@ -90,44 +88,25 @@ module._SHIFT_KEYS = { } -// build a reverse map of _SPECIAL_KEYS -var _KEY_CODES = -module._KEY_CODES = {} -for(var k in _SPECIAL_KEYS){ - _KEY_CODES[_SPECIAL_KEYS[k]] = k +var UNSHIFT_KEYS = +module.UNSHIFT_KEYS = {} +for(var k in SHIFT_KEYS){ + UNSHIFT_KEYS[SHIFT_KEYS[k]] = k } -// XXX some keys look really wrong... -var toKeyName = -module.toKeyName = -function toKeyName(code){ - // check for special keys... - var k = _SPECIAL_KEYS[code] - if(k != null){ - return k - } - // chars... - k = String.fromCharCode(code) - if(k != ''){ - //return k.toLowerCase() - return k - } - return null +// build a reverse map of SPECIAL_KEYS +var KEY_CODES = +module.KEY_CODES = {} +for(var k in SPECIAL_KEYS){ + KEY_CODES[SPECIAL_KEYS[k]] = k } -var toKeyCode = -module.toKeyCode = -function toKeyCode(c){ - if(c in _KEY_CODES){ - return _KEY_CODES[c] - } - return c.charCodeAt(0) -} +/*********************************************************************/ -// documentation wrapper... +// Documentation wrapper... var doc = module.doc = function doc(text, func){ @@ -137,80 +116,9 @@ function doc(text, func){ } -// Get list of applicable modes... -// -// XXX elem is not used... -var getApplicableModes = -module.getApplicableModes = -function getApplicableModes(keybindings, modes, elem){ - return Object.keys(keybindings) - .filter(function(title){ - - if(keybindings[title].pattern != null){ - var mode = keybindings[title].pattern - } else { - var mode = title - } - - // check if we need to skip this mode... - return modes == 'all' - // explicit mode match... - || modes == mode - // 'any' means we need to check the mode... - || (modes == 'any' - // '*' always matches... - && mode == '*' - // match the mode... - // XXX is this too global??? - || $(mode).length != 0) - }) -} - - -// Build or normalize a modifier string. -// -// Acceptable argument sets: -// - none -> "" -// - true, false, true -> "ctrl+shift" -// - true, false -> "ctrl" -// - [true, false] -> "ctrl" -// - 'alt+shift' -> "alt+shift" -// - 'shift - alt' -> "alt+shift" -// -// Bool flag order: -// ctrl, meta, alt, shift -// -// NOTE: 'meta' is the OSX "Command" key... -var normalizeModifiers = -module.normalizeModifiers = -function normalizeModifiers(c, m, a, s){ - if(c != null && c.constructor === Array){ - m = c[1] - a = c[2] - s = c[3] - c = c[0] - } - if(typeof(c) == typeof('str')){ - var modifiers = c - } else { - var modifiers = (c ? 'ctrl' : '') - + (m ? ' meta' : '') - + (a ? ' alt' : '') - + (s ? ' shift' : '') - } - - // build the dormalized modifier string... - var res = /ctrl/i.test(modifiers) ? 'ctrl' : '' - res += /meta/i.test(modifiers) ? (res != '' ? '+meta' : 'meta') : '' - res += /alt/i.test(modifiers) ? (res != '' ? '+alt' : 'alt') : '' - res += /shift/i.test(modifiers) ? (res != '' ? '+shift' : 'shift') : '' - - return res -} - - - -// supported action format: +// Parse action call format... +// +// supported format: // [!][: ][-- ] // // can contain space seporated: @@ -218,7 +126,10 @@ function normalizeModifiers(c, m, a, s){ // - strings // - non-nested arrays or objects // -// XXX add support for suffix to return false... +// XXX should this be here??? +// XXX add support for suffix to return false / stop_propagation... +// XXX should this handle calls??? +// i.e. have .call(..) / .apply(..) methods??? var parseActionCall = module.parseActionCall = function parseActionCall(txt){ @@ -244,14 +155,717 @@ function parseActionCall(txt){ action: action, arguments: args, doc: doc, - 'no-default': no_default, + no_default: no_default, + stop_propagation: false, } } +//--------------------------------------------------------------------- +// Helpers and utility functions... + +// Form standard key string from keyboard event... +// +// Format: +// "[ctrl+][meta+][alt+][shift+]" +// +// - string returned by code2key(..) +// +var event2key = +module.event2key = +function event2key(evt){ + evt = evt || event + + var key = [] + evt.ctrlKey && key.push('ctrl') + evt.altKey && key.push('alt') + evt.metaKey && key.push('meta') + evt.shiftKey && key.push('shift') + key.push(code2key(evt.keyCode)) + + return key +} + + +// Get key code from key name... +var key2code = +module.key2code = +function key2code(key){ + return key in KEY_CODES ? KEY_CODES[key] + : key.length > 1 ? null + : key.charCodeAt(0) } + + +// Get key name from key code... +var code2key = +module.code2key = +function code2key(code){ + var name = String.fromCharCode(code) + return code in SPECIAL_KEYS ? SPECIAL_KEYS[code] + : name != '' ? name + : null } + + +// Check if string is a standard key string... +var isKey = +module.isKey = +function isKey(key){ + var modifiers = MODIFIERS + + var mod = normalizeKey(splitKey(key)) + var k = mod.pop() + + // key is either a key code or a valid key name... + return (!!parseInt(k) || key2code(k) != null) + // mod must be a subset of modifiers... + && mod.filter(function(m){ return modifiers.indexOf(m) < 0 }).length == 0 +} + + +// Split key... +// +// NOTE: if this gets an array, it will get returned as-is... +// NOTE: no checks are made on the key, use isKey(..) in conjunction +// with normalizeKey(..) for checking... +var splitKey = +module.splitKey = +function splitKey(key){ + var sep = KEY_SEPARATORS + return key instanceof Array ? key + : typeof(key) == typeof(123) ? [key] + : key + .split(RegExp('[' + +sep.join('\\') + +']')) + .concat(sep.indexOf(key.slice(-1)) >= 0 ? key.slice(-1) : []) + .filter(function(c){ return c != '' }) } + + +// Normalize key string/array... +// +// NOTE: this will not check if a key is a key use isKey(..) for that. +var normalizeKey = +module.normalizeKey = +function normalizeKey(key){ + var output = key instanceof Array ? 'array' : 'string' + var modifiers = MODIFIERS + + // special case: got a number... + if(typeof(key) == typeof(123)){ + return code2key(key) + } + + // sort modifiers via .modifiers and keep the key last... + key = splitKey(key) + .slice() + .sort(function(a, b){ + a = modifiers.indexOf(a.toLowerCase()) + b = modifiers.indexOf(b.toLowerCase()) + return a >= 0 && b >= 0 ? a - b + : a < 0 ? 1 + : -1 }) + + var k = key.pop() + k = parseInt(k) ? code2key(parseInt(k)) : k + k = modifiers.indexOf(k.toLowerCase()) >= 0 ? + k.toLowerCase() + : k.capitalize() + key.push(k) + key = key.unique() + + return output == 'array' ? + key + : key.join(KEY_SEPARATORS[0] || '+') +} + + +// Get shifted key if available... +// +// Examples: +// - '{' -> 'shift+[' +// - ')' -> 'shift+#0' +var shifted = +module.shifted = +function shifted(key){ + var output = key instanceof Array ? 'array' : 'string' + key = normalizeKey(splitKey(key)).slice() + var k = key.pop() + + var s = (key.indexOf('shift') >= 0 ? + SHIFT_KEYS[k] + : UNSHIFT_KEYS[k]) + || null + + var res = s == null ? key + : (key.indexOf('shift') >= 0 ? + key.filter(function(k){ return k != 'shift' }) + : key.concat(['shift'])) + res.push(s) + + return s == null ? null + : output == 'string' ? + res.join(KEY_SEPARATORS[0] || '+') + : res +} + + + + +/*********************************************************************/ +// Generic keyboard handler... + +var KeyboardClassPrototype = { + service_fields: ['doc', 'drop'], + + event2key: event2key, + key2code: key2code, + code2key: code2key, + isKey: isKey, + splitKey: splitKey, + normalizeKey: normalizeKey, + shifted: shifted +} + +var KeyboardPrototype = { + //service_fields: ['doc', 'drop'], + special_handlers: { + DROP: 'drop key', + NEXT_SECTION: 'handle key in next section', + }, + + // Format: + // { + // : { + // doc: , + // drop: [ , ... ] | '*', + // + // : , + // + // : , + // : , + // }, + // ... + // } + // + // Reserved special handlers: + // - DROP - drop checking of key + // NOTE: this will prevent handling next sections + // for this key. + // - NEXT_SECTION - force check next section, this has priority + // over .drop + // + __keyboard: null, + get keyboard(){ + return this.__keyboard instanceof Function ? + this.__keyboard() + : this.__keyboard }, + set keyboard(value){ + this.__keyboard = value }, + + // XXX is this needed??? + //context: null, + + // utils... + event2key: KeyboardClassPrototype.event2key, + key2code: KeyboardClassPrototype.key2code, + code2key: KeyboardClassPrototype.code2key, + shifted: KeyboardClassPrototype.shifted, + splitKey: KeyboardClassPrototype.splitKey, + normalizeKey: KeyboardClassPrototype.normalizeKey, + isKey: KeyboardClassPrototype.isKey, + + //isModeApplicable: function(mode, keyboard, context){ return true }, + + // XXX merge configs... + // - need to match and order groups (use 1'st as reference)... + // - need to create new set w/o changing the originals... + merge: function(){ + }, + + // Get keys for handler... + // + // List all handlers... + // .keys() + // .keys('*') + // -> keys + // + // List keys for handler... + // .keys(handler) + // -> keys + // + // List keys for given handlers... + // .keys(handler, ...) + // .keys([handler, ...]) + // -> keys + // + // List keys for handlers that pass the func predicate... + // .keys(func) + // -> keys + // + // + // Format: + // { + // : { + // : [ , ... ], + // ... + // }, + // ... + // } + // + // + // NOTE: this will also return non-key aliases... + // NOTE: to match several compatible handlers, pass a list of handlers, + // the result for each will be merged into one common list. + keys: function(handler){ + var that = this + var res = {} + var keyboard = this.keyboard + var key_separators = KEY_SEPARATORS + var service_fields = this.service_fields + || this.constructor.service_fields + + handler = arguments.length > 1 ? [].slice.call(arguments) + : handler == null ? '*' + : handler == '*' || handler instanceof Function ? handler + : handler instanceof Array ? handler + : [handler] + + var walkAliases = function(res, rev, bindings, key, mod){ + mod = mod || [] + if(key in rev){ + rev[key].forEach(function(k){ + k = that.normalizeKey(mod + .concat(that.splitKey(k)) + .unique() + .join(key_separators[0])) + res.indexOf(k) < 0 + && res.push(k) + && walkAliases(res, rev, bindings, k, mod) + }) + } + } + + Object.keys(keyboard).forEach(function(mode){ + var bindings = keyboard[mode] + + // build a reverse index... + var rev = {} + // NOTE: this will not work for handlers that are not strings + // and have no .doc... + Object.keys(bindings) + .filter(function(key){ + return service_fields.indexOf(key) < 0 }) + .forEach(function(key){ + var h = bindings[key] + h = typeof(h) == typeof('str') ? h + : (h.doc || h.name) + rev[h] = (rev[h] || []) + .concat((rev[bindings[key]] || []).concat([key])) + .unique() + }) + + var seen = [] + var handlers = handler == '*' ? Object.keys(rev) + : handler instanceof Function ? + Object.keys(rev) + .filter(handler) + : handler + + handlers + .forEach(function(h){ + var keys = (rev[h] || []).map(that.normalizeKey.bind(that)) + + // find all reachable keys from the ones we just found in reverse... + keys.slice() + .filter(function(key){ return seen.indexOf(key) < 0 }) + .forEach(function(key){ + // direct aliases... + walkAliases(keys, rev, bindings, key) + + var mod = that.splitKey(key) + var k = mod.pop() + + // aliases with modifiers... + k != key + && walkAliases(keys, rev, bindings, k, mod) + + seen.push(seen) + }) + + if(keys.length > 0){ + var m = res[mode] = res[mode] || {} + m[h] = keys + } + }) + }) + + return res + }, + + // Get/set/unset handler for key... + // + // In general if handler is not passed this will get the handlers, + // if a handler is given this will set the handler, if the passed + // handler is either null or '' then it will be unbound. + // + // Get handler for key in all modes... + // .handler(key) + // .handler('*', key) + // -> key-spec + // + // Get handlers for key in applicable modes... + // .handler('?', key) + // .handler('test', key) + // -> key-spec + // + // Get handler for key in a specific mode... + // .handler(mode, key) + // -> key-spec + // + // Get handler for key in a specific list of modes... + // .handler([mode, ..], key) + // -> key-spec + // + // + // Bind handler to key in specific mode... + // .handler(mode, key, handler) + // -> this + // + // Bind handler to key in all modes... + // .handler('*', key, handler) + // -> this + // + // Bind handler to key in applicable modes... + // .handler('?', key, handler) + // .handler('test', key, handler) + // -> this + // + // Bind handler to key in a specific list of modes... + // .handler([mode, ..], key, handler) + // -> this + // + // + // Unbind handler from key in specific mode... + // .handler(mode, key, null) + // .handler(mode, key, '') + // -> this + // + // Unbind handler from key in all modes... + // .handler('*', key, null) + // .handler('*', key, '') + // -> this + // + // Unbind handler from key in applicable modes... + // .handler('?', key, null) + // .handler('?', key, '') + // .handler('test', key, null) + // .handler('test', key, '') + // -> this + // + // Unbind handler from key in a specific list of modes... + // .handler([mode, ..], key, null) + // .handler([mode, ..], key, '') + // -> this + // + // + // Format: + // { + // : , + // ... + // } + // + // + // Search order: + // - search for full key + // - search for shifted key if applicable + // - search for key without modifiers + // - if an alias is found it is first checked with and then + // without modifiers + // - search for key code without modifiers + // - if an alias is found it is first checked with and then + // without modifiers + handler: function(mode, key, handler){ + var that = this + var keyboard = this.keyboard + var key_separators = KEY_SEPARATORS + + if(mode == null){ + return null + } + if(key == null && this.isKey(mode)){ + key = mode + mode = '*' + } + + + var genKeys = function(key, shift_key){ + // match candidates... + return key_separators + // full key... + .map(function(s){ return key.join(s) }) + // full shift key... + .concat(shift_key ? + key_separators + .map(function(s){ return shift_key.join(s) }) + : []) + .unique() } + var walkAliases = function(bindings, handler, modifiers){ + var seen = [] + var modifiers = modifiers || [] + + while(handler in bindings){ + handler = bindings[handler] + + handler = modifiers + .filter(function(m){ + return handler.indexOf(m) < 0 + && seen.indexOf(m+handler) < 0 + && m+handler in bindings }) + .map(function(m){ return m+handler })[0] + || handler + + // check for loops... + if(seen.indexOf(handler) >= 0){ + handler = null + break + } + seen.push(handler) + } + + return handler + } + + key = this.normalizeKey(this.splitKey(key)) + var shift_key = this.shifted(key) + + // match candidates... + var keys = genKeys(key, shift_key).unique() + + // get modes... + var modes = mode == '*' ? Object.keys(keyboard) + : mode == 'test' || mode == '?' ? this.modes() + : mode instanceof Array ? mode + : [mode] + + // get... + if(handler === undefined){ + var res = {} + var k = key.slice(-1)[0] + var c = this.key2code(k) + + var mod = genKeys(key.slice(0, -1).concat('')) + + var drop = mode == 'test' || mode == '?' + for(var i=0; i < modes.length; i++){ + var m = modes[i] + + var bindings = keyboard[m] + + // stage 1: check key aliases with modifiers... + handler = walkAliases( + bindings, + keys.filter(function(k){ return bindings[k] })[0]) + + // stage 2: check raw key aliases with and without modifiers... + if(!handler){ + handler = walkAliases( + bindings, + [k, c].filter(function(k){ return bindings[k] })[0], + mod) + } + + // explicit DROP -- ignore next sections... + if(drop && handler == 'DROP'){ + break + } + + // we got a match... + if(handler){ + res[m] = handler + } + + // if key in .drop then ignore the rest... + if(drop + // explicit go to next section... + && handler != 'NEXT_SECTION' + && (bindings.drop == '*' + // XXX should this be more flexible by adding a + // specific key combo? + // ... if yes, we'll need to differentiate + // between X meaning drop only X and drop + // all combos with X... + || (bindings.drop || []).indexOf(k) >= 0)){ + break + } + } + + return (typeof(mode) == typeof('str') + && ['*', 'test', '?'].indexOf(mode) < 0) ? + res[mode] + : res + + // set / remove... + } else { + modes.forEach(function(m){ + var bindings = keyboard[m] + + // remove all matching keys... + // NOTE: if key with modifiers, then this will remove only + // the full matched keys and shifted matches but will + // leave the key without modifiers as-is... + keys.forEach(function(k){ delete bindings[k] }) + + // set handler if given... + if(handler && handler != ''){ + keyboard[mode][key] = handler + } + }) + } + + return this + }, + + // get applicable modes... + // + modes: function(context){ + var that = this + return that.isModeApplicable ? + Object.keys(this.keyboard) + .filter(function(mode){ + return that.isModeApplicable( + mode, + that.keyboard, + context || that.context) }) + : Object.keys(this.keyboard) }, + + + // init base data... + __init__: function(keyboard, is_mode_applicable){ + this.keyboard = keyboard + + if(is_mode_applicable instanceof Function){ + this.isModeApplicable = is_mode_applicable + } + }, +} + +var Keyboard = +module.Keyboard = +object.makeConstructor('Keyboard', + KeyboardClassPrototype, + KeyboardPrototype) + + +//--------------------------------------------------------------------- +// Keyboard handler with modes identified by CSS selectors... + +var KeyboardWithCSSModesPrototype = { + service_fields: ['doc', 'drop', 'pattern'], + + isModeApplicable: function(mode, keyboard, context){ + var pattern = keyboard[mode].pattern || mode + context = context || this.context + return !pattern + || pattern == '*' + // XXX can we join these into one search??? + || context.is(pattern) + || context.find(pattern).length > 0 + }, + + __init__: function(keyboard, context){ + object.superMethod(KeyboardWithCSSModes, '__init__').call(this, keyboard) + + if(context instanceof Function){ + Object.defineProperty(this, 'context', { + get: context, + }) + + } else { + this.context = context + } + }, +} + +var KeyboardWithCSSModes = +module.KeyboardWithCSSModes = +object.makeConstructor('KeyboardWithCSSModes', + KeyboardClassPrototype, + KeyboardWithCSSModesPrototype) +// inherit from Keyboard... +KeyboardWithCSSModes.prototype.__proto__ = Keyboard.prototype + + + + +/*********************************************************************/ + +// Base event handler wrapper of Keyboard... +// +var makeKeyboardHandler = +module.makeKeyboardHandler = +function makeKeyboardHandler(keyboard, unhandled, actions){ + + var kb = keyboard instanceof Keyboard ? + keyboard + //: Keyboard(keyboard, checkGlobalMode) + : Keyboard(keyboard) + + return function(evt){ + var res = undefined + var did_handling = false + + var key = kb.event2key(evt) + var handlers = kb.handler('test', key) + + Object.keys(handlers).forEach(function(mode){ + if(res === false){ + return + } + + var handler = handlers[mode] + + // raw function handler... + if(handler instanceof Function){ + res = handler.call(actions) + + // action call syntax... + } else { + var h = parseActionCall(handler) + + if(h && h.action in actions){ + did_handling = true + + h.no_default + && evt.preventDefault() + + // call the handler... + res = actions[h.action].apply(actions, h.args) + + if(h.stop_propagation){ + res = false + evt.stopPropagation() + } + } + } + }) + + unhandled + && !did_handling + && unhandled.call(actions, evt) + + return res + } +} + + + +//--------------------------------------------------------------------- +// handler wrappers... + // Event handler wrapper to stop handling keys if check callback does // not pass (returns false)... +// var stoppableKeyboardRepeat = module.stoppableKeyboardRepeat = function(handler, check){ @@ -306,769 +920,6 @@ function dropRepeatingkeys(handler, max_rate){ -/* Key handler getter - * - * For doc on format see makeKeyboardHandler(...) - * - * modifiers can be: - * - '' (default) - No modifiers - * - '?' - Return list of applicable modifiers per mode - * - - Any of 'ctrl', 'alt' or 'shift' alone or in - * combination. - * Combinations MUST be ordered as shown above. - * Combination elements are separated by '+' - * Ex: - * 'ctrl+shift' - * NOTE: 'shift+ctrl' is wrong. - * NOTE: normalizeModifiers(...) can be used as - * a reference, if in doubt. - * - * modes can be: - * - 'any' (default) - Get list of all applicable handlers up until - * the first applicable ignore. - * - 'all' - Get ALL handlers, including ignores - * - - Get handlers for an explicit mode - * - * - * This will also resolve several shifted keys by name, for example: - * 'shift-/' is the same as '?', and either can be used, but the shorter - * direct notation has priority (see _SHIFT_KEYS for supported keys). - * - * When resolving handler aliases this will look in: - * 1) the current mode - * 2) the keybinding object - * 3) the actions object if given - * - * NOTE: if a handler is found in the action object it will be called in - * the action object context rather than the keybinding context as - * as with normal handlers. - * NOTE: if and action alias ends with '!' then event.preventDefault() will - * get called before the action. - * - * - * Returns: - * { - * : , - * ... - * } - * - * - * can be: - * - - handler - * - [, ] - * - lisp-style handler - * - 'IGNORE' - if mode is 'all' and key is in .ignore - * - [, 'IGNORE NEXT'] - * - if mode is 'all' and the key is both in .ignore - * and a handler is defined in the current section - * NOTE: in this case if this mode matches, all - * the subsequent handlers will get ignored - * in normal modes... - * - * - * NOTE: adding a key to the ignore list has the same effect as returning - * false form it's handler in the same context. - * NOTE: it is not possible to do a shift-? as it is already shifted. - * NOTE: if a key is not handled in a mode, that mode will not be - * present in the resulting object. - * NOTE: this will not unwrap lisp-style (see below) handlers. - * NOTE: modes are prioritized by order of occurrence. - * NOTE: modifiers can be a list of three bools... - * (see: normalizeModifiers(...) for further information) - * - * XXX check do we need did_handling here... - * XXX BUG explicitly given modes do not yield results if the pattern - * does not match... - * XXX should action handler support event.stoppropagation()??? - * ...at this point I'm not sure it is needed as it will not affect - * the keyboard handlers, it will preven further JS event handlers... - * XXX this also supports the experimental 'ALL' key name that matches - * all the keys, needs more work, not for production use... - */ -var getKeyHandlers = -module.getKeyHandlers = -function getKeyHandlers(key, modifiers, keybindings, modes, shifted_keys, actions){ - var chr = null - var s_chr = null - // XXX I do not understand why this is here... - var did_handling = false - var did_ignore = false - modifiers = modifiers || '' - modifiers = modifiers != '?' ? normalizeModifiers(modifiers) : modifiers - modes = modes || 'any' - shifted_keys = shifted_keys || _SHIFT_KEYS - actions = actions || {} - - // XXX too global -- need to pass some context... - var applicable_modes = getApplicableModes(keybindings, modes) - - if(typeof(key) == typeof(123)){ - key = key - chr = toKeyName(key) - } else { - chr = key - key = toKeyCode(key) - } - - // XXX this is not done yet... - if(shifted_keys != false && /shift/i.test(modifiers)){ - var s_chr = shifted_keys[chr] - } - - res = {} - - for(var title in keybindings){ - // skip functions... - if(typeof(keybindings[title]) == 'function'){ - continue - } - - // If a key is ignored then look no further... - if(did_ignore){ - if(modes != 'all'){ - break - } else { - did_ignore = false - if(modifiers != '?' && res[mode] != 'IGNORE'){ - res[mode] = [ res[mode], 'IGNORE NEXT'] - } - } - } - - // older version compatibility... - var mode = keybindings[title].pattern || title - - // check if we need to skip this mode... - if(applicable_modes.indexOf(title) < 0){ - continue - } - - var bindings = keybindings[title] - - if(s_chr != null && s_chr in bindings){ - var handler = bindings[s_chr] - chr = s_chr - modifiers = modifiers.replace(/\+?shift/i, '') - } else if(chr in bindings){ - var handler = bindings[chr] - } else if(key in bindings) { - var handler = bindings[key] - // XXX experimental... - } else { - var handler = bindings['ALL'] - } - - // alias... - // XXX should this be before after or combined with ignore handling... - while(handler != null && typeof(handler) != 'function'){ - - // do the complex handler aliases... - if(handler != null && handler.constructor == Object){ - // build modifier list... - if(modifiers == '?'){ - break - } - if(modifiers in handler){ - if(typeof(handler[modifiers]) == typeof('str')){ - handler = handler[modifiers] - } else { - break - } - } else if(typeof(handler['default']) == typeof('str')){ - handler = handler['default'] - } else { - break - } - } - - // simple handlers... - if(handler in bindings){ - // XXX need to take care of that we can always be a number or a string... - handler = bindings[handler] - - // global alias... - } else if(handler in keybindings){ - handler = keybindings[handler] - - // actions... - } else if(handler in actions - || handler.split(/!?\s*:\s*|!|--/)[0].trim() in actions){ - - var c = parseActionCall(handler) - - handler = function(n, no_default, args, doc){ - if(no_default){ - var f = function(){ - event.preventDefault() - return actions[n].apply(actions, args) - } - } else { - var f = function(){ - return actions[n].apply(actions, args) - } - } - // make this doc-generator compatible -- inherit all - // the docs from the actual action... - f.__proto__ = actions[n] - // tell the doc generator about the action stuff... - f.action = n - f.args = args - f.no_default = no_default - - // use the inherited doc if no specific doc is defined... - if(doc && doc.length > 0){ - f.doc = doc - } - - return f - }(c.action, c['no-default'], c.arguments, c.doc) - - // ignore... - } else if(handler == 'IGNORE'){ - break - - // key code... - } else if(typeof(handler) == typeof(1)) { - handler = bindings[toKeyName(handler)] - - // key name... - } else { - handler = bindings[toKeyCode(handler)] - } - } - - // if something is ignored then just breakout and stop handling... - if(handler == 'IGNORE' - || bindings.ignore == '*' - || bindings.ignore != null - && (bindings.ignore.indexOf(key) != -1 - || bindings.ignore.indexOf(chr) != -1)){ - did_handling = true - // ignoring a key will stop processing it... - if(modes == 'all' || mode == modes){ - // NOTE: if a handler is defined in this section, this - // will be overwritten... - // XXX need to add the handler to this if it's defined... - res[mode] = 'IGNORE' - } - did_ignore = true - } - - // no handler... - if(handler == null){ - continue - } - - // complex handler... - if(handler != null && handler.constructor === Object){ - // build modifier list... - if(modifiers == '?'){ - res[mode] = Object.keys(handler) - did_handling = true - continue - } - - var callback = handler[modifiers] - if(callback == null){ - callback = handler['default'] - } - - if(callback != null){ - res[mode] = callback - - did_handling = true - continue - } - - // simple callback... - } else { - // build modifier list... - if(modifiers == '?'){ - res[mode] = 'none' - } else { - res[mode] = handler - } - - did_handling = true - continue - } - - if(modes != 'all' && did_handling){ - break - } - } - - return res -} - - -/* Make a keyboard handler function... - * - * Basic key binding format: - * - * { - * // alias... - * : | , - * - * // section... - * : { - * doc: <text>, - * pattern: <css-selector>, - * - * // this defines the list of keys to ignore by the handler. - * // NOTE: use "*" to ignore all keys other than explicitly - * // defined in the current section. - * // NOTE: ignoring a key will stop processing it in other - * // compatible modes. - * ignore: <ignored-keys> - * - * // alias... - * <alias> : <handler> | <callback> | 'IGNORE', - * - * // ignore handling... - * <key-def> : 'IGNORE', - * - * // NOTE: a callback can have a .doc attr containing - * // documentation... - * <key-def> : <callback>, - * - * <key-def> : [ - * // this can be any type of handler except for an alias... - * <handler>, - * <doc> - * ], - * - * <key-def> : { - * // modifiers can either have a callback or an alias as - * // a value... - * // NOTE: when the alias is resolved, the same modifiers - * // will be applied to the final resolved handler. - * default: <callback> | <alias> | 'IGNORE', - * - * // a modifier can be any single modifier, like shift or a - * // combination of modifiers like 'ctrl+shift', in order - * // of priority. - * // supported modifiers, ordered by priority, are: - * // - ctrl - * // - alt - * // - shift - * // NOTE: if in doubt use normalizeModifiers(..) as a - * // reference... - * <modifer>: <callback> | <alias> | 'IGNORE', - * ... - * }, - * - * // alias... - * <key-def> : <alias>, - * - * ... - * }, - * - * // legacy format, still supported... (deprecated) - * <css-selector>: { - * // meta-data used to generate user docs/help/config - * title: <text>, - * ... - * }, - * - * ... - * } - * - * - * <key-def> can be: - * - explicit key code, e.g. 65 - * - key name, if present in _SPECIAL_KEYS, e.g. Enter - * - key char (uppercase), as is returned by String.fromCharCode(...) e.g. A - * - action/alias -- any arbitrary string that is not in the above - * categories. - * - * <alias> will be matched to key definitions in sections and in the key - * binding object itself and in an actions object if given. - * - * <alias> syntax examples: - * actionName - simple alias / action name - * actionName! - same as above but calls event.preventDefault() - * actionNmae: 1 "2" [3, 4] {5:6, 7:8} - * - action with arguments. - * arguments can be: - * - numbers - * - strings - * - non-nested arrays and/or objects - * actionNmae!: 1 "2" [3, 4] {5:6, 7:8} - * - same as above but calls event.preventDefault() - * - * XXX add support for suffix to return false... - * - * - * NOTE: The handler will be called with keybindings as context (this). - * NOTE: handlers found in actions will be called with the actions as context. - * NOTE: only for handlers found in actions, if the alias ends with '!' - * then event.preventDefault() will be called before the handler. - * NOTE: adding a key to the ignore list has the same effect as returning - * false form it's handler in the same context. - * NOTE: actions,the last case, are used for alias referencing, they will - * never match a real key, but will get resolved in alias searches. - * NOTE: to test what to use as <key-def> use toKeyCode(..) / toKeyName(..). - * NOTE: all fields are optional. - * NOTE: if a handler explicitly returns false then that will break the - * event propagation chain and exit the handler. - * i.e. no other matching handlers will be called. - * NOTE: if more than one match is found all matching handlers will be - * called in sequence until one returns false explicitly. - * NOTE: a <css-selector> is used as a predicate to select a section to - * use. if multiple selectors match something then multiple sections - * will be resolved in order of occurrence. - * NOTE: the number keys are named with a leading hash '#' (e.g. '#8') - * to avoid conflicsts with keys that have the code with the same - * value (e.g. 'backspace' (8)). - * NOTE: one can use a doc(<doc-string>, <callback>) as a shorthand to - * assign a docstring to a handler. - * it will only assign .doc attr and return the original function. - * - * XXX need an explicit way to prioritize modes... - * XXX will aliases get resolved if they are in a different mode?? - */ -var makeKeyboardHandler = -module.makeKeyboardHandler = -function makeKeyboardHandler(keybindings, unhandled, actions){ - if(unhandled == null){ - unhandled = function(){} - } - - return function(evt){ - var did_handling = false - var res = null - - var _keybindings = typeof(keybindings) == typeof(function(){}) ? - keybindings.call(this) - : keybindings - - // key data... - var key = evt.keyCode - - // get modifiers... - var modifiers = [evt.ctrlKey, evt.metaKey, evt.altKey, evt.shiftKey] - - //window.DEBUG && console.log('KEY:', key, chr, modifiers) - - var handlers = getKeyHandlers(key, modifiers, _keybindings, null, null, actions) - - for(var mode in handlers){ - var handler = handlers[mode] - if(handler != null){ - - // Array, lisp style with docs... - if(typeof(handler) == typeof([]) && handler.constructor == Array){ - // we do not care about docs here, so just get the handler... - handler = handler[0] - } - - // stop handling explicitly ignored keys... - // XXX - if(handler == 'IGNORE'){ - break - } - - did_handling = true - //res = handler(evt) - res = handler.call(_keybindings) - - if(res === false){ - break - } - } - } - if(!did_handling){ - return unhandled(key) - } - return res - } -} - - -/* Build structure ready for conversion to HTML help. -* -* Structure: -* { -* <section-title>: { -* doc: ... -* -* <handler-doc>: <keys-spec> -* ... -* } -* } -* -* <keys-spec> - list of key names. -* -* -* NOTE: this will not add keys (key names) that are not explicit key names. -*/ -// XXX do we need to normalize/pre-process keybindings??? -// - might be a good idea to normalize the <modifiers>... -// XXX do we show ignored keys??? -// ...at this point we show explicitly ignored keys only... -// XXX do we show overloaded keys??? -var buildKeybindingsHelp = -module.buildKeybindingsHelp = -function buildKeybindingsHelp(keybindings, shifted_keys, actions){ - shifted_keys = shifted_keys == null ? _SHIFT_KEYS : shifted_keys - var res = {} - var mode, title - - for(var title in keybindings){ - mode = keybindings[title] - - // older version compatibility... - if(keybindings[title].pattern != null){ - var pattern = keybindings[title].pattern - } else { - var pattern = title - // titles and docs... - var title = mode.title == null ? pattern : mode.title - } - - res[title] = { - doc: mode.doc == null ? '' : mode.doc - } - section = res[title] - - // handlers... - for(var key in mode){ - if(KEYBOARD_SYSTEM_ATTRS.indexOf(key) >= 0){ - continue - } - var modifiers = getKeyHandlers(key, '?', keybindings, 'all', null, actions)[pattern] - modifiers = modifiers == 'none' || modifiers == undefined ? [''] : modifiers - - for(var i=0; i < modifiers.length; i++){ - var mod = modifiers[i] - - var handler = getKeyHandlers(key, mod, keybindings, 'all', null, actions)[pattern] - - // no handler... - // NOTE: handler is present in config but not present - // in actions... - if(handler == null){ - continue - } - - if(handler.constructor === Array && handler[1] == 'IGNORE NEXT'){ - handler = handler[0] - } - - if(handler == 'IGNORE'){ - // XXX do we show ignored keys??? - var doc = 'Ignored' - //continue - - // standard object doc... - } else if('doc' in handler){ - var doc = handler.doc - - // lisp style... - } else if(handler.constructor === Array){ - var doc = handler[1] - - // no doc... - } else { - if('name' in handler && handler.name != ''){ - var doc = handler.name - } else { - // XXX is this the right way to do this? - var doc = handler - } - } - - // populate the section... - // NOTE: we need a list of keys per action... - if(doc in section){ - var keys = section[doc] - } else { - var keys = [] - section[doc] = keys - } - - // translate shifted keys... - if(shifted_keys != false && mod == 'shift' && key in shifted_keys){ - mod = '' - key = shifted_keys[key] - } - - // skip anything that is not a key... - if(key.length > 1 && !(key in _KEY_CODES)){ - continue - } - - keys.push((mod == '' || mod == 'default') ? key : (mod +'+'+ key)) - } - - } - } - - return res -} - - -// Get a list of keys associated with a given doc... -// -// The second argument must be a structure formated as returned by -// buildKeybindingsHelp(...) -// -// Returned format: -// { -// <section-name> : <key-spec> -// ... -// } -// -// NOTE: <key-spec> is the same as generated by buildKeybindingsHelp(..) -var getKeysByDoc = -module.getKeysByDoc = -function getKeysByDoc(doc, help){ - var res = {} - for(var mode in help){ - var name = mode - var section = help[mode] - if(doc in section){ - res[mode] = section[doc] - } - } - return res -} - - -// Build a basic HTML table with keyboard help... -// -// The table will look like this: -// -// <table class="keyboard-help"> -// -// <!-- section head --> -// <tr class="section-title"> -// <th colspan=2> SECTION TITLE <th> -// </tr> -// <tr class="section-doc"> -// <td colspan=2> SECTION DESCRIPTION <td> -// </tr> -// -// <!-- section keys --> -// <tr> -// <td> KEYS <td> -// <td> ACTION DESCRIPTION <td> -// </tr> -// -// ... -// -// </table> -// -// NOTE: section are not separated in any way other than the <th> element. -// NOTE: the actual HTML is created by jQuery, so the table may get -// slightly structurally changed, i.e. a <tbody> element will be -// added etc. -// -var buildKeybindingsHelpHTML = -module.buildKeybindingsHelpHTML = -function buildKeybindingsHelpHTML(keybindings, actions){ - var doc = buildKeybindingsHelp(keybindings, null, actions) - - var res = '<table class="keyboard-help">' - for(var mode in doc){ - if(mode == 'doc'){ - continue - } - // section head... - res += ' <tr class="section-title"><th colspan=2>' + mode + '</th></tr>\n' - mode = doc[mode] - res += ' <tr class="section-doc"><td colspan=2>'+ mode.doc + '</td></tr>\n' - - // keys... - for(var action in mode){ - if(action == 'doc'){ - continue - } - res += ' <tr><td>' + mode[action].join(', ') +'</td><td>'+ action + '</td></tr>\n' - } - } - res += '</table>' - - return $(res) -} - - -// Build HTML for a single key definition... -// -// Format if combining sections (default): -// <span class="key-doc"> -// <span class="doc"> DOC </span> -// <span class="keys"> KEYS </span> -// </span> -// -// Format if not combining sections: -// <span class="key-doc"> -// <span class="doc"> DOC </span> -// <span class="section"> -// <span class="name"> MODE NAME </span> -// <span class="keys"> KEYS </span> -// </span> -// ... -// </span> -// -// XXX not yet sure if we are handling the sections correctly... -var getKeysByDocHTML = -module.getKeysByDocHTML = -function getKeysByDocHTML(doc, help, combine_sections){ - combine_sections = combine_sections == null ? true : combine_sections - - var spec = getKeysByDoc(doc, help) - var res = '<span class="key-doc">' - - res += '<span class="doc">'+ doc +'</span>' - - var keys = [] - - for(var section in spec){ - if(!combine_sections){ - keys = spec[section].join(', ') - res += '<span class="section">' - +'<span class="name">'+ section +'</span>' - +'<span class="keys">'+ keys +'</span>' - +'</span>' - } else { - keys = keys.concat(spec[section]) - } - } - - if(combine_sections){ - res += '<span class="keys">'+ keys.join(', ') +'</span>' - } - - return res + '</span>' -} - - -// Update key definitions... -// -// NOTE: this does not support multiple sections... -var updateHTMLKeyDoc = -module.updateHTMLKeyDoc = -function updateHTMLKeyDoc(help, root){ - root = root == null ? $('body') : root - return root.find('.key-doc').each(function(i, e){ - e = $(e) - var doc = e.find('.doc') - var keys = $(getKeysByDocHTML(doc.html(), help)).find('.keys') - e.find('.keys').html(keys.html()) - }) -} - - - -/********************************************************************** -* Key binding editor... -*/ - -// XXX - - /********************************************************************** * vim:set ts=4 sw=4 : */ return module })