define(["lib/jquery", "lib/underscore", "common/util/log",
"common/util/dom_utils", "common/util/binding_list", "common/util/id_generator",
"common/util/check"],
function( $, _, Log,
DOMUtils, BindingList, IDGenerator,
check){
"use strict";
var logger = Log("AbstractKeyboard");
// delay between when a key is pressed and when it starts repeating
// set to false to prevent repeating
var REPEAT_FIRST_DELAY = 1000;
// delay between repeats one they start
var REPEAT_SECOND_DELAY = 200;
// key codes for special keys.
// see: http://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes
var specialKeys = {
TAB: 9,
BACKSPACE: 8,
ENTER: 13,
LEFT: 37,
RIGHT: 39
};
// inserts text into a text box at the caret position. based on
// http://stackoverflow.com/a/4384173/2005979 and
// base.insertText from https://github.com/Mottie/Keyboard/blob/master/js/jquery.keyboard.js
var insertAtCaret = function(element, text) {
var startPos = element.selectionStart;
var endPos = element.selectionEnd;
var val = element.value;
// insert character
if(text === specialKeys.BACKSPACE) {
element.value = val.substring(0, startPos-1) + val.substring(endPos, val.length);
}
else {
element.value = val.substring(0, startPos) + text + val.substring(endPos, val.length);
}
element.focus();
// set new caret position
if(text === specialKeys.BACKSPACE) {
moveCaret(element, startPos - 1);
}
else {
moveCaret(element, startPos + text.length);
}
};
// calculates the width and height of the text contained in a text input
// or textarea. based on http://stackoverflow.com/a/2117698/2005979
var textMetrics = function(el, text, preserveWidth) {
var h = 0, w = 0, $el = $(el);
// create a hidden element
var div = document.createElement("div");
var $div = $(div);
document.body.appendChild(div);
$div.css({
position: "absolute",
left: -1000,
top: -1000,
display: "none"
});
// copy text from input to hidden element
$div.text(text);
$div.css("white-space", "pre");
// copy over styles
var styles = ["font-size","font-style", "font-weight", "font-family","line-height", "text-transform", "letter-spacing"];
_.each(styles, function(style) {
$div.css(style, $el.css(style));
});
// preserve with the desired
if(preserveWidth) {
$div.css("width", $el.width());
}
h = $div.outerHeight();
w = $div.outerWidth();
$div.remove();
return {
height: h,
width: w,
};
};
// moves the caret to a given position
var moveCaret = function(el, pos) {
el.setSelectionRange(pos, pos);
// update scrollLeft and scrollTop to ensure the caret is in view
var $el = $(el);
var caretPos;
if(el.tagName === "TEXTAREA") {
caretPos = textMetrics(el, el.value.substring(0, pos), true).height;
var lineHeight = parseInt( $el.css("line-height"), 10) || parseInt($el.css("font-size"), 10) + 4;
var height = $el.height();
var top = el.scrollTop;
var bottom = top + height;
if((caretPos-lineHeight) < top) {
el.scrollTop -= (height / 2);
}
else if((caretPos+lineHeight) > bottom) {
el.scrollTop += (height / 2);
}
}
else {
caretPos = textMetrics(el, el.value.substring(0, pos)).width;
var width = $el.width();
var left = el.scrollLeft;
var right = left + width;
if(caretPos < left) {
el.scrollLeft -= (width / 2);
}
else if(caretPos > right) {
el.scrollLeft += (width / 2);
}
}
};
// handlers for special keys. gets the currently focussed element as
// an argument. only called if an ediatable element is focussed. if they
// return a string, that string is inserted at the caret position.
//
// no need to do KEY_BACKSPACE; that's special-cased in insertAtCaret
var keyHandlers = {};
keyHandlers[specialKeys.TAB] = function() {
return "\t";
};
keyHandlers[specialKeys.ENTER] = function() {
return "\n";
};
keyHandlers[specialKeys.LEFT] = function(el) {
moveCaret(el, el.selectionStart-1);
};
keyHandlers[specialKeys.RIGHT] = function(el) {
moveCaret(el, el.selectionStart+1);
};
/**
* Constructs a new AbstractKeyboard. Subclasses should call this to get a
* basic keyboard before extending it with a concrete implementation.
* Subclasses should pass in an object as `$protected`, which will be
* populated with `keydown(key)` and `keyup(keydownId)`, which allow
* keyboards to simulate a key being pressed or released. `key` is either a
* printable character or one of the constants that is in
* `AbstractKeyboard.KEYS` to represent special keys: `TAB`, `BACKSPACE`,
* `ENTER`, `LEFT`, and `RIGHT`. `keydown` returns an id that
* can be passed to `keyup` to mark that key as released.
*
* AbstractKeyboards are *not* frozen, so you can subclass AbstractKeyboard
* using parasitic inheritance:
*
* function MySpecialKeyboard() {
* var $protected = {};
* var self = AbstractKeyboard($protected);
*
* self.pressAKeyRepeatedly = function(key, numTimes) {
* for(var i = 0; i < numTimes; i++) {
* $protected.keyup($protected.keydown(key));
* }
* };
*
* Object.freeze(self);
* return self;
* }
*
* @param {Object} $protected
* Subclasses should pass an object for this argument. It will be populated
* with protected members available only to subclasses.
*
* @private
* @class An abstract base class for keyboards.
* @name AbstractKeyboard
*/
var AbstractKeyboard = function($protected) {
/** @alias AbstractKeyboard.prototype */
var self = {};
// listeners registered by keypressed
var keypressListeners = BindingList();
// id returned by keydown -> setTimeout id
var timeouts = {};
// fires a lens:keypress event, informs subscribers, and modifies the
// focussed element if it is a text box
var press = function(key) {
var props = {
key: key,
char: key,
keyCode: key,
charCode: key,
which: key
};
keypressListeners.callAll(props);
DOMUtils.fireBubbling("lens:keypress", document.activeElement, props);
var active = $(document.activeElement);
if(active.is("input:text") || active.is("textarea")) {
// character to type
var chr = key;
// run the key handler for special keys. if it returns a
// string, that string becomes the inserted chr.
if(_.isNumber(key) && keyHandlers[key]) {
chr = keyHandlers[key](active[0]);
}
// if we have a character to type, insert it at the current
// carat position.
if(chr) {
insertAtCaret(active[0], chr);
}
}
};
// simulated a key being pressed. Returns an ID that can be passed to
// keyup.
$protected.keydown = function(key) {
press(key);
// we use multiple calls to setTimeout, so we need to have a stable
// ID that we map to whatever the current setTimeout ID is.
var keydownId = IDGenerator.uuid();
// repeatedly press the key if it's held down.
if(REPEAT_FIRST_DELAY !== false) {
var timeoutId = window.setTimeout(function() {
var repeater = function() {
press(key);
var timeoutId = setTimeout(repeater, REPEAT_SECOND_DELAY);
timeouts[keydownId] = timeoutId;
};
repeater();
}, REPEAT_FIRST_DELAY);
timeouts[keydownId] = timeoutId;
}
return keydownId;
};
// simulates a key being released. Takes an ID returned from keydown.
$protected.keyup = function(keydownId) {
window.clearTimeout(timeouts[keydownId]);
};
/**
* Dismisses the keyboard.
*/
self.dismiss = function() {
logger.error("dismiss is unimplemented");
};
/**
* Binds a handler to the keypress event.
*
* @param {function} handler Handler to bind
*/
self.keyPressed = function(handler) {
check(handler, Function);
return keypressListeners.add(handler);
};
// dont' freeze self; we need to be able to subclass it
return self;
};
/**
* Map of special keys to character codes that can be passed to keyup.
* @private
*/
AbstractKeyboard.KEYS = specialKeys;
return AbstractKeyboard;
});