/**
* This modules handles low-level keyboard events and normalize them across
* browsers.
* @module editor/keyboard
* @private
*/
define([], function() {
const KEY_NAMES = {
'Escape': 'Esc',
' ': 'Spacebar',
'ArrowLeft': 'Left',
'ArrowUp': 'Up',
'ArrowRight': 'Right',
'ArrowDown': 'Down',
'Delete': 'Del',
};
const VIRTUAL_KEY_NAMES = {
'q' : 'KeyQ',
'w' : 'KeyW',
'e' : 'KeyE',
'r' : 'KeyR',
't' : 'KeyT',
'y' : 'KeyY',
'u' : 'KeyU',
'i' : 'KeyI',
'o' : 'KeyO',
'p' : 'KeyP',
'a' : 'KeyA',
's' : 'KeyS',
'd' : 'KeyD',
'f' : 'KeyF',
'g' : 'KeyG',
'h' : 'KeyH',
'j' : 'KeyJ',
'k' : 'KeyK',
'l' : 'KeyL',
'z' : 'KeyZ',
'x' : 'KeyX',
'c' : 'KeyC',
'v' : 'KeyV',
'b' : 'KeyB',
'n' : 'KeyN',
'm' : 'KeyM',
'1' : 'Digit1',
'2' : 'Digit2',
'3' : 'Digit3',
'4' : 'Digit4',
'5' : 'Digit5',
'6' : 'Digit6',
'7' : 'Digit7',
'8' : 'Digit8',
'9' : 'Digit9',
'0' : 'Digit0',
'!' : 'Shift-Digit1',
'@' : 'Shift-Digit2',
'#' : 'Shift-Digit3',
'$' : 'Shift-Digit4',
'%' : 'Shift-Digit5',
'^' : 'Shift-Digit6',
'&' : 'Shift-Digit7',
'*' : 'Shift-Digit8',
'(' : 'Shift-Digit9',
')' : 'Shift-Digit0',
'-' : 'Minus',
'_' : 'Shift-Minus',
'/' : 'Slash',
'\\' : 'Backslash', // Some virtual keyboards (iOS) return '\' as the event.key
// with no evt.code
'|' : 'Shift-Backslash',
'?' : 'Shift-Slash',
' ' : 'Spacebar'
};
/**
*
* Create a normalized string representation of the key combo,
* i.e., key code and modifier keys. For example:
* - `Ctrl-Shift-Alt-KeyF`
* - `Alt-Space`
* - `Shift-Digit6`
* @todo See https://github.com/madrobby/keymaster/blob/master/keymaster.js
* - Doesn't work very well for command-<key>
* - Returns "Alt-Alt" when only the Alt key is pressed
* @memberof module:editor/keyboard
* @param {Event} evt
*/
function keyboardEventToString(evt) {
let keyname;
if (evt.key === 'Unidentified') {
// On Android, the evt.key seems to always be Unidentified.
// Get the value entered in the event target
if (evt.target) {
keyname = VIRTUAL_KEY_NAMES[evt.target.value] || evt.target.value;
}
}
if (!keyname) {
keyname = KEY_NAMES[evt.key] || evt.code;
// For virtual keyboards (iOS, Android) and Microsoft Edge (!)
// the `evt.code`, which represents the physical key pressed, is set
// to undefined. In that case, map the virtual key ("q") to a
// pseudo-hardware key ("KeyQ")
if (!keyname) {
keyname = VIRTUAL_KEY_NAMES[evt.key.toLowerCase()] || evt.key;
}
}
const modifiers = [];
if (evt.ctrlKey) modifiers.push('Ctrl');
if (evt.metaKey) modifiers.push('Meta');
if (evt.altKey) modifiers.push('Alt');
if (evt.shiftKey) modifiers.push('Shift');
// If no modifiers, simply return the key name
if (modifiers.length === 0) return keyname;
modifiers.push(keyname);
return modifiers.join('-');
}
/**
* Setup to capture the keyboard events from a `TextArea` and redispatch them to
* handlers.
*
* In general, commands (arrows, delete, etc..) should be handled
* in the `keystroke()` handler while text input should be handled in
* `typedtext()`.
*
* @param {Element} textarea A `TextArea` element that will capture the keyboard
* events. While this element will usually be a `TextArea`, it could be any
* element that is focusable and can receive keyboard events.
* @param {Object} handlers
* @param {Element} [handlers.container]
* @param {function} handlers.keystroke invoked on a key down event, including
* for special keys such as ESC, arrow keys, tab, etc... and their variants
* with modifiers.
* @param {function} handlers.typedtext invoked on a keypress or other events
* when a key corresponding to a character has been pressed. This include `a-z`,
* `0-9`, `{}`, `^_()`, etc...
* This does not include arrow keys, tab, etc... but does include 'space'
* When a 'character' key is pressed, both `keystroke()` and `typedtext()` will
* be invoked. When a control/function key is pressed, only `keystroke()` will
* be invoked. In some cases, for example when using input methods or entering
* emoji, only `typedtext()` will be invoked.
* @param {function} handlers.paste(text) Invoked in response to a paste
* command. Not all browsers support this (Chrome doesn't), so typedtext()
* will be invoked instead.
* @param {function} handlers.cut
* @param {function} handlers.copy
* @memberof module:editor/keyboard
* @private
*/
function delegateKeyboardEvents(textarea, handlers) {
let keydownEvent = null;
let keypressEvent = null;
let compositionInProgress = false;
let deadKey = false;
// const noop = function() {};
// This callback is invoked after a keyboard event has been processed
// by the textarea
// let callback = noop;
let callbackTimeoutID;
function defer(cb) {
// callback = cb;
clearTimeout(callbackTimeoutID);
callbackTimeoutID = setTimeout(function() {
// callback = noop;
clearTimeout(callbackTimeoutID);
cb();
});
}
function handleTypedText() {
// Some browsers (Firefox, Opera) fire a keypress event for commands
// such as command-C where there might be a non-empty selection.
// We need to ignore these.
if (hasSelection(textarea)) return;
const text = textarea.value;
textarea.value = '';
if (text.length > 0) handlers.typedText(text);
}
function onKeydown(e) {
if ((e.key === 'Dead' || e.key === 'Unidentified') || e.keyCode === 229) {
deadKey = true;
compositionInProgress = false;
// This sequence seems to cancel dead keys
textarea.blur();
textarea.focus();
} else {
deadKey = false;
}
if (!compositionInProgress &&
e.code !== 'ControlLeft' &&
e.code !== 'MetaLeft') {
keydownEvent = e;
keypressEvent = null;
return handlers.keystroke(keyboardEventToString(e), e);
}
return true;
}
function onKeypress(e) {
// If this is not the first keypress after a keydown, that is,
// if this is a repeated keystroke, call the keystroke handler.
if (!compositionInProgress) {
if (keydownEvent && keypressEvent) {
handlers.keystroke(keyboardEventToString(keydownEvent), keydownEvent);
}
keypressEvent = e;
defer(handleTypedText);
}
}
function onKeyup() {
// If we've received a keydown, but no keypress, check what's in the
// textarea field.
if (!compositionInProgress && keydownEvent && !keypressEvent) {
handleTypedText();
}
}
function onPaste() {
// In some cases (Linux browsers), the text area might not be focused
// when doing a middle-click paste command.
textarea.focus();
const text = textarea.value;
textarea.value = '';
if (text.length > 0) handlers.paste(text);
}
function onCopy() {
if (handlers.copy) handlers.copy();
}
function onCut() {
if (handlers.cut) handlers.cut();
}
function onBlur() {
keydownEvent = null;
keypressEvent = null;
if (handlers.blur) handlers.blur();
}
function onFocus() {
if (handlers.focus) handlers.focus();
}
const target = textarea || handlers.container;
target.addEventListener('keydown', onKeydown, true);
target.addEventListener('keypress', onKeypress, true);
target.addEventListener('keyup', onKeyup, true);
target.addEventListener('paste', onPaste, true);
target.addEventListener('copy', onCopy, true);
target.addEventListener('cut', onCut, true);
target.addEventListener('blur', onBlur, true);
target.addEventListener('focus', onFocus, true);
target.addEventListener('compositionstart',
() => { compositionInProgress = true }, true);
target.addEventListener('compositionend',
() => { compositionInProgress = false; defer(handleTypedText); }, true);
// The `input` handler gets called when the field is changed, for example
// with input methods or emoji input...
target.addEventListener('input', () => {
if (deadKey) {
textarea.blur();
textarea.focus();
deadKey = false;
compositionInProgress = false;
defer(handleTypedText);
} else if (!compositionInProgress) {
defer(handleTypedText);
}
});
}
function hasSelection(textarea) {
return textarea.selectionStart !== textarea.selectionEnd;
}
return {
delegateKeyboardEvents: delegateKeyboardEvents,
select: delegateKeyboardEvents.select,
};
});