kekule
Version:
Open source JavaScript toolkit for chemoinformatics
642 lines (621 loc) • 19.5 kB
JavaScript
/**
* @fileoverview
* Util classe and function for keyboard events of widget.
* @author Partridge Jiang
*/
/*
* requires /lan/classes.js
* requires /core/kekule.common.js
* requires /utils/kekule.utils.js
* requires /utils/kekule.domUtils.js
* requires /xbrowsers/kekule.x.js
* requires /widget/kekule.widget.root.js
* requires /widget/kekule.widget.events.js
*/
(function(){
"use strict";
var OU = Kekule.ObjUtils;
/*
* Default options to enable/disable widget shortcut.
* @object
*/
Kekule.globalOptions.add('widget.shortcut', {
enabled: true
});
/**
* A util class containing functions about key events.
* @class
*/
Kekule.Widget.KeyboardUtils = {
DEF_COMBINATION_KEY_DELIMITER: '+',
/** @private */
_initShiftKeyMap: function()
{
var keyPairs = [
['`', '~'],
['1', '!'],
['2', '@'],
['3', '#'],
['4', '$'],
['5', '%'],
['6', '^'],
['7', '&'],
['8', '*'],
['9', '('],
['0', ')'],
['-', '_'],
['=', '+'],
['[', '{'],
[']', '}'],
[';', ':'],
['\'', '\"'],
['\\', '|'],
[',', '<'],
['.', '>'],
['/', '?']
];
var map = {};
for (var i = 0, l = keyPairs.length; i < l; ++i)
{
map[keyPairs[i][0]] = keyPairs[i][1];
map[keyPairs[i][1]] = keyPairs[i][0];
}
return map;
},
/**
* Returns is value is a printable key (e.g., 'a', ' ', '+'), not a virtual or control one (e.g. 'tab', 'F1', 'Shift').
* @param {String} key
* @returns {Bool}
*/
isPrintableKey: function(key)
{
if (key)
return key.length <= 1;
else
return false;
},
/**
* Returns the shifted char of a keyboard key.
* @param {String} key
* @returns {String}
*/
getShiftedKey: function(key)
{
if (key && key.length <= 1)
{
var c = key.charAt(0); // ensure the first char
if (c >= 'a' && c <= 'z')
return c.toUpperCase();
else if (c >= 'A' && c <= 'Z')
return c.toLowerCase();
else
{
var shifted = Kekule.Widget.KeyboardUtils._shiftKeyMap[c];
return shifted || c;
}
}
else
return key;
},
/**
* Returns a key event params hash for a shorcut display label.
* @param {String} label
* @param {String} delimiter Char to combine keys.
* @param {Bool} strict
*/
shortcutLabelToKeyParams: function(label, delimiter, strict)
{
var delimiters = delimiter? [delimiter]: ['+', '-', '_']; // possible delimiters
var activePartCount = -1;
var activeDelimiter;
var activeParts;
// find the delimiter that splits with most parts
for (var i = 0, l = delimiters.length; i < l; ++i)
{
var d = delimiters[i];
var parts = label.split(d);
if (parts.length > activePartCount)
{
activeDelimiter = d;
activePartCount = parts.length;
activeParts = parts;
}
}
var result = {}
if (activePartCount <= 0) // the label is actually a delimiter key
{
result.key = activeDelimiter;
}
else // analysis each parts
{
for (var i = 0, l = activeParts.length; i < l; ++i)
{
var part = activeParts[i];
if (!part && (i === l - 1)) // empty part on tail, should be the delimiter itself
part = activeDelimiter;
var lpart = part.toLowerCase();
if (lpart === 'shift')
result.shiftKey = true;
else if (lpart === 'alt')
result.altKey = true;
else if (lpart === 'ctrl' || lpart === 'control')
result.ctrlKey = true;
else if (lpart === 'meta')
result.metaKey = true;
else // not modifier, should be the main key
{
if (lpart === 'esc')
result.key = 'Escape';
else if (lpart === 'del') // abbr
result.key = 'Delete';
else if (lpart === 'ins')
result.key = 'Insert';
else if (lpart === 'pgup')
result.key = 'PageUp';
else if (lpart === 'pgdown' || lpart === 'pgdn')
result.key = 'PageDown';
else if (lpart === 'space')
result.key = ' ';
else
result.key = part;
}
if (!result.key && i === l - 1) // main key not found, all modifier keys? Tail part should be regarded as the main key
{
if (lpart === 'ctrl')
result.key = 'Control';
else
result.key = part; //lpart.charAt(0).toUpperCase() + lpart.substr(1);
}
}
}
if (!strict && result.key) // capitalize first char on non strict mode
result.key = result.key.charAt(0).toUpperCase() + result.key.substr(1);
return result;
},
/**
* Returns a shortcut display label for key event params.
* @param {Hash} param
* @param {String} delimiter Char to combine keys, default is '+'.
* @param {Bool} strict
*/
keyParamsToShortcutLabel: function(param, delimiter, strict)
{
if (!delimiter)
delimiter = Kekule.Widget.KeyboardUtils.DEF_COMBINATION_KEY_DELIMITER;
var labels = [];
// modifiers
if (param.ctrlKey)
labels.push('Ctrl');
if (param.altKey)
labels.push('Alt');
if (param.metaKey)
labels.push('Meta');
if (param.shiftKey)
labels.push('Shift');
// main key
var mainKey = param.key;
if (mainKey)
{
if (mainKey === 'Escape')
mainKey = 'Esc';
else if (mainKey === 'Insert')
mainKey = 'Ins';
else if (mainKey === 'Delete')
mainKey = 'Del';
else if (mainKey === 'Control')
mainKey = 'Ctrl';
else if (mainKey === ' ')
mainKey = 'Space';
if (!strict)
mainKey = mainKey.charAt(0).toUpperCase() + mainKey.substr(1);
var index = labels.indexOf(mainKey);
if (index >= 0) // main key is modifier, remove the one in labels
labels.splice(index, 1);
labels.push(mainKey);
}
return labels.join(delimiter);
},
/**
* Turn some alias of key values to one standard one.
* @param {String} value
* returns {String}
* @private
*/
_standardizeEventKeyValue: function(value)
{
if (value === 'Esc')
return 'Escape';
else if (value === 'Del')
return 'Delete';
else if (value === 'Spacebar')
return ' ';
else if (value === 'Left')
return 'ArrowLeft';
else if (value === 'Right')
return 'ArrowRight';
else if (value === 'Up')
return 'ArrowUp';
else if (value === 'Down')
return 'ArrowDown';
else if (value === 'OS')
return 'Meta';
else if (value === 'Scroll')
return 'ScrollLock';
else if (value === 'Apps')
return 'ContextMenu';
else if (value === 'Crsel')
return 'CrSel';
else if (value === 'Exsel')
return 'ExSel';
else
return value;
},
/**
* Extract key param values from an event object.
* @param {HTMLEvent} event
* @param {Bool} modifierKeysOnly If true, only modifier key info will be extracted (e.g. for mouse events).
* @returns {Hash}
*/
getKeyParamsFromEvent: function(event, modifierKeysOnly)
{
var result = {
'altKey': event.getAltKey(),
'ctrlKey': event.getCtrlKey(),
'shiftKey': event.getShiftKey(),
'metaKey': event.getMetaKey()
};
if (!modifierKeysOnly)
result = Object.extend(result,
{
'key': Kekule.Widget.KeyboardUtils._standardizeEventKeyValue(event.getKey()),
'code': event.getCode(),
'repeat': event.getRepeat()
});
return result;
},
/**
* Create a modifier key param object from an array containing the name of modifier keys (e.g. ['shift', 'ctrl').
* @param {Array} keyArray
* @returns {Hash}
*/
createModifierKeyParamsFromArray: function(keyArray)
{
var result = {};
for (var i = 0, l = keyArray.length; i < l; ++i)
{
var key = keyArray[i].toLowerCase();
if (key.indexOf('shift') >= 0)
result.shiftKey = true;
else if (key.indexOf('ctrl') >= 0)
result.ctrlKey = true;
else if (key.indexOf('meta') >= 0)
result.metaKey = true;
else if (key.indexOf('alt') >= 0)
result.altKey = true;
}
return result;
},
/**
* Check if two key param hashes are matached.
* @param {Hash} params1
* @param {Hash} params2
* @param {Bool} strictMatch
* @returns {Bool}
*/
matchKeyParams: function(params1, params2, strictMatch)
{
var matchValue = function(value1, value2) { return (value2 === null) || (!value2 === !value1); };
var matchKeyValue = function(key1, key2)
{
var _s = Kekule.Widget.KeyboardUtils._standardizeEventKeyValue;
if (strictMatch)
return _s(key2) === _s(key1);
else
return (_s(key2) === _s(key1)) || (_s(key1) === Kekule.Widget.KeyboardUtils.getShiftedKey(key2));
};
// modifier keys
var result =
matchValue(params1.altKey, params2.altKey) &&
matchValue(params1.ctrlKey, params2.ctrlKey) &&
matchValue(params1.shiftKey, params2.shiftKey) &&
matchValue(params1.metaKey, params2.metaKey) &&
(!params2.key || matchKeyValue(params1.key, params2.key)) &&
(!params2.code || params2.code === params1.code) &&
matchValue(params1.repeat, params2.repeat);
return result;
}
};
/** @ignore */
Kekule.Widget.KeyboardUtils._shiftKeyMap = Kekule.Widget.KeyboardUtils._initShiftKeyMap();
/**
* A special class to handle shorcut keys in widget system.
* Note the modifier properties (shiftKey, ctrlKey, etc) can be set with a special value null.
* If null is set, this modifier key will not be ignored in event matching.
* @class
* @augments Kekule.Widget.HtmlEventMatcher
*
* @property {String} key Test with event's key property.
* @property {String} code Test with event's code property.
* @property {Bool} shiftKey
* @property {Bool} ctrlKey
* @property {Bool} altKey
* @property {Bool} metaKey
* @property {Bool} repeat
* @property {Bool} strictMatch If true, shift+key('a') will be regarded as different to shift+key('A').
*/
Kekule.Widget.HtmlKeyEventMatcher = Class.create(Kekule.Widget.HtmlEventMatcher,
/** @lends Kekule.Widget.HtmlKeyEventMatcher# */
{
/** @private */
CLASS_NAME: 'Kekule.Widget.HtmlKeyEventMatcher',
/** @constructs */
initialize: function(eventParams)
{
var eparams = Object.extend({'strictMatch': true}, eventParams); // default do the strict match
this.tryApplySuper('initialize', [eparams]);
},
/** @private */
initProperties: function()
{
/*
this._defineEventParamProp('key', DataType.STRING);
this._defineEventParamProp('code', DataType.STRING);
this._defineEventParamProp('shiftKey', DataType.BOOL);
this._defineEventParamProp('ctrlKey', DataType.BOOL);
this._defineEventParamProp('altKey', DataType.BOOL);
this._defineEventParamProp('metaKey', DataType.BOOL);
this._defineEventParamProp('repeat', DataType.BOOL);
this._defineEventParamProp('strictMatch', DataType.BOOL);
*/
var propNames = Kekule.Widget.HtmlKeyEventMatcher.KEY_PARAM_PROPS;
for (var i = 0, l = propNames.length; i < l; ++i)
{
var propType = (propNames[i] === 'key' || propNames[i] === 'code')? DataType.STRING: DataType.BOOL;
this._defineEventParamProp(propNames[i], propType);
}
},
/** @ignore */
match: function(htmlEvent)
{
var result = this.tryApplySuper('match', [htmlEvent]);
if (result)
{
result = this._matchKeyEvent(htmlEvent, this.getEventParams());
}
return result;
},
/** @private */
_isKeyIgnored: function(key)
{
return key === null;
},
/** @private */
_matchKeyEvent: function(event, params)
{
var ignored = this._isKeyIgnored;
// modifier keys
var result =
(ignored(params.altKey) || !!event.getAltKey() === !!params.altKey) &&
(ignored(params.ctrlKey) || !!event.getCtrlKey() === !!params.ctrlKey) &&
(ignored(params.shiftKey) || !!event.getShiftKey() === !!params.shiftKey) &&
(ignored(params.metaKey) || !!event.getMetaKey() === !!params.metaKey) &&
(!params.key || this._matchKey(event, params.key)) &&
(!params.code || event.getCode() === params.code) &&
(OU.isUnset(params.repeat) || !!event.getRepeat() === !!params.repeat);
return result;
},
/** @private */
_matchKey: function(event, key)
{
var _s = Kekule.Widget.KeyboardUtils._standardizeEventKeyValue;
var evKey = event.getKey();
// if key is a printable char, combined with shift may lead to another char
if (/*!event.getShiftKey() ||*/ this.getStrictMatch())
return _s(evKey) === _s(key);
else
return (_s(evKey) === _s(key)) || (evKey === Kekule.Widget.KeyboardUtils.getShiftedKey(key));
}
});
/** @ignore */
Kekule.Widget.HtmlKeyEventMatcher.KEY_PARAM_PROPS = ['key', 'code', 'shiftKey', 'ctrlKey', 'altKey', 'metaKey', 'repeat', 'strictMatch'];
/**
* Keyboard shortcut in widget system.
* @class
* @augments ObjectEx
*
* @property {String} key Text represents a combination key, e.g. 'Ctrl+A'.
* @property {Bool} strictMatch If true, shift+key('a') will be regarded as different to shift+key('A').
*/
/**
* Invoked when the shortcut is pressed (and execTarget should be executed).
* event param of it has fields: {htmlEvent, execTarget}.
* Note this event will still be invoked even if execTarget is not set.
* @name Kekule.Widget.Shortcut#execute
* @event
*/
Kekule.Widget.Shortcut = Class.create(ObjectEx,
/** @lends Kekule.Widget.Shortcut# */
{
/** @private */
CLASS_NAME: 'Kekule.Widget.Shortcut',
/** @constructs */
initialize: function(execTarget, exclusive)
{
//this.setPropStoreFieldValue('eventMatcher', eventMatcher);
//this.setPropStoreFieldValue('execTarget', execTarget);
var self = this;
var r = new Kekule.Widget.HtmlEventResponser(new Kekule.Widget.HtmlKeyEventMatcher({eventType: this._getKeyEventType(), strictMatch: false}), this);
/*
r.addEventListener('execute', function(e){
var execTarget = self.getExecTarget();
self.invokeEvent({'htmlEvent': e.htmlEvent, 'execTarget': execTarget});
});
*/
this.setPropStoreFieldValue('eventResponser', r);
if (Kekule.ObjUtils.notUnset(exclusive))
r.setExclusive(exclusive);
else
r.setExclusive(true); // shortcut default exclusive
if (execTarget)
this.setPropStoreFieldValue('execTarget', execTarget);
this.tryApplySuper('initialize', []);
this._registeredDocs = [];
},
/** @private */
initProperties: function()
{
this.defineProp('strictMatch', {'dataType': DataType.BOOL,
'getter': function() { return this.getEventResponser().getEventMatcher().getStrictMatch(); },
'setter': function(value) { this.getEventResponser().getEventMatcher().setStrictMatch(value); }
});
this.defineProp('exclusive', {'dataType': DataType.BOOL,
'getter': function() { return this.getEventResponser().getEventMatcher().getExclusive(); },
'setter': function(value) { this.getEventResponser().getEventMatcher().setExclusive(value); }
});
this.defineProp('key', {'dataType': DataType.STRING,
'getter': function()
{
var params = this.getEventResponser().getEventParams();
return Kekule.Widget.KeyboardUtils.keyParamsToShortcutLabel(params, null, this.getStrictMatch());
},
'setter': function(value)
{
if (value !== this.getKey())
{
var params = Kekule.Widget.KeyboardUtils.shortcutLabelToKeyParams(value, null, this.getStrictMatch());
/*
params.eventType = this._getKeyEventType();
params.strictMatch = this.getStrictMatch();
this.getEventResponser().setEventParams(params);
*/
var oldParams = this.getEventResponser().getEventParams();
var keyProps = Kekule.Widget.HtmlKeyEventMatcher.KEY_PARAM_PROPS;
var notUnset = Kekule.ObjUtils.notUnset;
for (var i = 0, l = keyProps.length; i < l; ++i)
{
var propName = keyProps[i];
if (notUnset(params[propName]))
oldParams[propName] = params[propName];
}
}
}
});
this.defineProp('execTarget', {'dataType': DataType.VARIANT, 'serializable': false, 'setter': null});
this.defineProp('targetWidget', {'dataType': 'Kekule.Widget.BaseWidget',
'setter': null, 'serializable': false,
'getter': function()
{
var execTarget = this.getExecTarget();
return (execTarget && (execTarget instanceof Kekule.Widget.BaseWidget))? execTarget: null;
}
});
this._defineEventResponserRelatedProp('eventParams', DataType.HASH);
//this._defineEventResponserRelatedProp('execTarget', DataType.VARIANT);
// private
this.defineProp('eventResponser', {'dataType': 'Kekule.Widget.HtmlEventResponser', 'serializable': false, 'setter': null});
},
/** @ignore */
doFinalize: function()
{
this.unregisterFromAll();
var r = this.getEventResponser();
this.setPropStoreFieldValue('eventResponser', null);
r.finalize();
this.tryApplySuper('doFinalize');
},
/** @private */
_defineEventResponserRelatedProp: function(propName, dataType, paramFieldName)
{
var kname = propName || paramFieldName;
return this.defineProp(propName, {'dataType': dataType, 'serializable': false,
'getter': function() { return this.getEventResponser().getPropValue(kname); },
'setter': function(value) { this.getEventResponser().setPropValue(kname, value); }
});
},
/** @private */
_getKeyEventType: function()
{
return 'keydown';
},
/**
* The shortcut is pressed and the target widget should be executed.
* @param {HTMLEvent} htmlEvent
*/
execute: function(htmlEvent)
{
if (Kekule.globalOptions.widget.shortcut.enabled)
{
var target = this.getExecTarget();
if (target)
{
this.doExecTarget(htmlEvent, target);
}
this.invokeEvent({'htmlEvent': htmlEvent, 'execTarget': target});
return true;
}
else // shortcut disabled, bypass
return false; // explicit indicating we are do nothing
},
/** @private */
doExecTarget: function(htmlEvent, target)
{
var result = true;
if (DataType.isFunctionValue(target))
target.apply(null, [htmlEvent, this]);
else if (target.execute && DataType.isFunctionValue(target.execute))
{
if (DataType.isObjectExValue(target))
{
if (target instanceof Kekule.Widget.BaseWidget)
target.execute(htmlEvent);
else if (target instanceof Kekule.Action)
target.execute(this, htmlEvent);
else
target.execute(htmlEvent, this);
}
else
target.execute(htmlEvent, this);
}
else
result = false;
return result;
},
/**
* Register the shortcut to global manager of document, set it to be activated.
* @param {HTMLDocument} document
*/
registerToGlobal: function(document)
{
var doc = document || Kekule.$document;
if (doc && this._registeredDocs.indexOf(doc) < 0)
{
var globalManager = Kekule.Widget.Utils.getGlobalManager(doc);
globalManager.registerHtmlEventResponser(this.getEventResponser());
Kekule.ArrayUtils.pushUnique(this._registeredDocs, doc);
}
},
/**
* Unregister the shortcut from global manager of document.
* @param {HTMLDocument} document
*/
unregisterFromGlobal: function(document)
{
var doc = document || Kekule.$document;
if (doc && this._registeredDocs.indexOf(doc) >= 0)
{
var globalManager = Kekule.Widget.Utils.getGlobalManager(doc);
globalManager.unregisterHtmlEventResponser(this.getEventResponser());
Kekule.ArrayUtils.remove(this._registeredDocs, doc);
}
},
/**
* Unregister the shortcut from all previously registered documents.
*/
unregisterFromAll: function()
{
var docs = this._registeredDocs;
for (var i = 0, l = docs.length; i < l; ++i)
this.unregisterFromGlobal(docs[i]);
}
});
})();