collabora-online/browser/src/map/handler/Map.Keyboard.js

638 lines
18 KiB
JavaScript

/* -*- js-indent-level: 8 -*- */
/*
* L.Map.Keyboard is handling keyboard interaction with the map, enabled by default.
*
* It handles keyboard interactions which are NOT text input, including those which
* don't require edit permissions (e.g. page scroll). Text input is handled
* at TextInput.
*/
/* global app vex _ */
L.Map.mergeOptions({
keyboard: true,
keyboardPanOffset: 20,
keyboardZoomOffset: 1
});
L.Map.Keyboard = L.Handler.extend({
keyModifier: {
shift: 4096,
ctrl: 8192,
alt: 16384,
ctrlMac: 32768
},
// For UNO keycodes cf. offapi/com/sun/star/awt/Key.idl
keymap: {
8 : 1283, // backspace : BACKSPACE
9 : 1282, // tab : TAB
13 : 1280, // enter : RETURN
16 : null, // shift : UNKOWN
17 : null, // ctrl : UNKOWN
18 : null, // alt : UNKOWN
19 : null, // pause/break : UNKOWN
20 : null, // caps lock : UNKOWN
27 : 1281, // escape : ESCAPE
32 : 1284, // space : SPACE
33 : 1030, // page up : PAGEUP
34 : 1031, // page down : PAGEDOWN
35 : 1029, // end : END
36 : 1028, // home : HOME
37 : 1026, // left arrow : LEFT
38 : 1025, // up arrow : UP
39 : 1027, // right arrow : RIGHT
40 : 1024, // down arrow : DOWN
45 : 1285, // insert : INSERT
46 : 1286, // delete : DELETE
48 : 256, // 0 : NUM0
49 : 257, // 1 : NUM1
50 : 258, // 2 : NUM2
51 : 259, // 3 : NUM3
52 : 260, // 4 : NUM4
53 : 261, // 5 : NUM5
54 : 262, // 6 : NUM6
55 : 263, // 7 : NUM7
56 : 264, // 8 : NUM8
57 : 265, // 9 : NUM9
65 : 512, // A : A
66 : 513, // B : B
67 : 514, // C : C
68 : 515, // D : D
69 : 516, // E : E
70 : 517, // F : F
71 : 518, // G : G
72 : 519, // H : H
73 : 520, // I : I
74 : 521, // J : J
75 : 522, // K : K
76 : 523, // L : L
77 : 524, // M : M
78 : 525, // N : N
79 : 526, // O : O
80 : 527, // P : P
81 : 528, // Q : Q
82 : 529, // R : R
83 : 530, // S : S
84 : 531, // T : T
85 : 532, // U : U
86 : 533, // V : V
87 : 534, // W : W
88 : 535, // X : X
89 : 536, // Y : Y
90 : 537, // Z : Z
91 : null, // left window key : UNKOWN
92 : null, // right window key : UNKOWN
93 : null, // select key : UNKOWN
96 : 256, // numpad 0 : NUM0
97 : 257, // numpad 1 : NUM1
98 : 258, // numpad 2 : NUM2
99 : 259, // numpad 3 : NUM3
100 : 260, // numpad 4 : NUM4
101 : 261, // numpad 5 : NUM5
102 : 262, // numpad 6 : NUM6
103 : 263, // numpad 7 : NUM7
104 : 264, // numpad 8 : NUM8
105 : 265, // numpad 9 : NUM9
106 : 1289, // multiply : MULTIPLY
107 : 1287, // add : ADD
109 : 1288, // subtract : SUBTRACT
110 : 1309, // decimal point : DECIMAL
111 : 1290, // divide : DIVIDE
112 : 768, // f1 : F1
113 : 769, // f2 : F2
114 : 770, // f3 : F3
115 : 771, // f4 : F4
116 : 772, // f5 : F5
117 : 773, // f6 : F6
118 : 774, // f7 : F7
119 : 775, // f8 : F8
120 : 776, // f9 : F9
121 : 777, // f10 : F10
122 : 778, // f11 : F11
144 : 1313, // num lock : NUMLOCK
145 : 1314, // scroll lock : SCROLLLOCK
173 : 1288, // dash : DASH (on Firefox)
186 : 1317, // semi-colon : SEMICOLON
187 : 1295, // equal sign : EQUAL
188 : 1292, // comma : COMMA
189 : 1288, // dash : DASH
190 : null, // period : UNKOWN
191 : null, // forward slash : UNKOWN
192 : null, // grave accent : UNKOWN
219 : null, // open bracket : UNKOWN
220 : null, // back slash : UNKOWN
221 : null, // close bracket : UNKOWN
222 : null // single quote : UNKOWN
},
handleOnKeyDownKeys: {
// these keys need to be handled on keydown in order for them
// to work on chrome
// Backspace and Delete are handled at TextInput's 'beforeinput' handler.
9 : true, // tab
19 : true, // pause/break
20 : true, // caps lock
27 : true, // escape
33 : true, // page up
34 : true, // page down
35 : true, // end
36 : true, // home
37 : true, // left arrow
38 : true, // up arrow
39 : true, // right arrow
40 : true, // down arrow
45 : true, // insert
113 : true // f2
},
keyCodes: {
pageUp: 33,
pageDown: 34,
enter: 13
},
navigationKeyCodes: {
left: [37],
right: [39],
down: [40],
up: [38],
zoomIn: [187, 107, 61, 171],
zoomOut: [189, 109, 173]
},
initialize: function (map) {
this._map = map;
this._setPanOffset(map.options.keyboardPanOffset);
this._setZoomOffset(map.options.keyboardZoomOffset);
this.modifier = 0;
},
addHooks: function () {
var container = this._map._container;
// make the container focusable by tabbing
if (container.tabIndex === -1) {
container.tabIndex = '0';
}
L.DomEvent.on(this._map.getContainer(), 'keydown keyup keypress', this._onKeyDown, this);
L.DomEvent.on(window.document, 'keydown', this._globalKeyEvent, this);
},
removeHooks: function () {
L.DomEvent.off(this._map.getContainer(), 'keydown keyup keypress', this._onKeyDown, this);
L.DomEvent.off(window.document, 'keydown', this._globalKeyEvent, this);
},
_ignoreKeyEvent: function(ev) {
var shift = ev.shiftKey ? this.keyModifier.shift : 0;
if (shift && (ev.keyCode === 45 || ev.keyCode === 46)) {
// don't handle shift+insert, shift+delete
// These are converted to 'cut', 'paste' events which are
// automatically handled by us, so avoid double-handling
return true;
}
},
_setPanOffset: function (pan) {
var keys = this._panKeys = {},
codes = this.navigationKeyCodes,
i, len;
for (i = 0, len = codes.left.length; i < len; i++) {
keys[codes.left[i]] = [-1 * pan, 0];
}
for (i = 0, len = codes.right.length; i < len; i++) {
keys[codes.right[i]] = [pan, 0];
}
for (i = 0, len = codes.down.length; i < len; i++) {
keys[codes.down[i]] = [0, pan];
}
for (i = 0, len = codes.up.length; i < len; i++) {
keys[codes.up[i]] = [0, -1 * pan];
}
},
_setZoomOffset: function (zoom) {
var keys = this._zoomKeys = {},
codes = this.navigationKeyCodes,
i, len;
for (i = 0, len = codes.zoomIn.length; i < len; i++) {
keys[codes.zoomIn[i]] = zoom;
}
for (i = 0, len = codes.zoomOut.length; i < len; i++) {
keys[codes.zoomOut[i]] = -zoom;
}
},
// Convert javascript key codes to UNO key codes.
_toUNOKeyCode: function (keyCode) {
return this.keymap[keyCode] || keyCode;
},
// _onKeyDown - called only as a DOM event handler
// Calls _handleKeyEvent(), but only if the event doesn't have
// a charCode property (set to something different than 0) - that ignores
// any 'beforeinput', 'keypress' and 'input' events that would add
// printable characters. Those are handled by TextInput.js.
_onKeyDown: function (ev) {
if (this._map.uiManager.isUIBlocked())
return;
var completeEvent = app.socket.createCompleteTraceEvent('L.Map.Keyboard._onKeyDown', { type: ev.type, charCode: ev.charCode });
console.log('keyboard handler:', ev.type, ev.key, ev.charCode, this._expectingInput, ev);
if (ev.charCode == 0) {
this._handleKeyEvent(ev);
}
if (this._map._docLayer)
if (ev.shiftKey && ev.type === 'keydown')
this._map._docLayer.shiftKeyPressed = true;
else if (ev.keyCode === 16 && ev.type === 'keyup')
this._map._docLayer.shiftKeyPressed = false;
if (completeEvent)
completeEvent.finish();
},
_globalKeyEvent: function(ev) {
if (this._map.uiManager.isUIBlocked())
return;
if (this._map.jsdialog && this._map.jsdialog.hasDialogOpened()
&& this._map.jsdialog.handleKeyEvent(ev)) {
ev.preventDefault();
return;
}
},
// _handleKeyEvent - checks if the given keyboard event shall trigger
// a message to coolwsd, and calls the given keyEventFn(type, charcode, keycode)
// callback if so.
// Called from _onKeyDown
_handleKeyEvent: function (ev, keyEventFn) {
if (this._map.uiManager.isUIBlocked())
return;
this._map.notifyActive();
if (this._map.slideShow && this._map.slideShow.fullscreen) {
return;
}
var docLayer = this._map._docLayer;
if (!keyEventFn) {
// default is to post keyboard events on the document
keyEventFn = L.bind(docLayer.postKeyboardEvent, docLayer);
}
this.modifier = 0;
var shift = ev.shiftKey ? this.keyModifier.shift : 0;
var ctrl = ev.ctrlKey ? this.keyModifier.ctrl : 0;
var alt = ev.altKey ? this.keyModifier.alt : 0;
var cmd = ev.metaKey ? this.keyModifier.ctrl : 0;
var location = ev.location;
this.modifier = shift | ctrl | alt | cmd;
// On Windows, pressing AltGr = Alt + Ctrl
// Presence of AltGr is detected if previous Ctrl + Alt 'location' === 2 (i.e right)
// because Ctrl + Alt + <some char> won't give any 'location' information.
if (ctrl && alt) {
if (ev.type === 'keydown' && location === 2) {
this._prevCtrlAltLocation = location;
return;
}
else if (location === 1) {
this._prevCtrlAltLocation = undefined;
}
if (this._prevCtrlAltLocation === 2 && location === 0) {
// and we got the final character
if (ev.type === 'keypress') {
ctrl = alt = this.modifier = 0;
}
else {
// Don't handle remnant 'keyup'
return;
}
}
}
if (ctrl || cmd) {
if (this._handleCtrlCommand(ev)) {
return;
}
}
var charCode = ev.charCode;
var keyCode = ev.keyCode;
if ((this.modifier == this.keyModifier.alt || this.modifier == this.keyModifier.shift + this.keyModifier.alt) &&
keyCode >= 48) {
// Presumably a Mac or iOS client accessing a "special character". Just ignore the alt modifier.
// But don't ignore it for Alt + non-printing keys.
this.modifier -= alt;
alt = 0;
}
// handle help - F1
if (ev.type === 'keydown' && !shift && !ctrl && !alt && !cmd && keyCode === 112) {
this._map.showHelp('online-help');
ev.preventDefault();
return;
}
var unoKeyCode = this._toUNOKeyCode(keyCode);
if (this.modifier) {
unoKeyCode |= this.modifier;
if (ev.type !== 'keyup' && (this.modifier !== shift || (keyCode === 32 && !this._map._isCursorVisible))) {
keyEventFn('input', charCode, unoKeyCode);
ev.preventDefault();
return;
}
}
if (this._map.stateChangeHandler._items['.uno:SlideMasterPage'] === 'true') {
ev.preventDefault();
return;
}
if (this._map.isPermissionEdit()) {
docLayer._resetPreFetching();
if (this._ignoreKeyEvent(ev)) {
// key ignored
}
else if (ev.type === 'keydown') {
// console.log(e);
if (this.handleOnKeyDownKeys[keyCode] && charCode === 0) {
keyEventFn('input', charCode, unoKeyCode);
ev.preventDefault();
}
}
else if ((ev.type === 'keypress') && (!this.handleOnKeyDownKeys[keyCode] || charCode !== 0)) {
if (keyCode === 8 || keyCode === 46 || keyCode === 13)
{
// handled generically in TextInput.js
console.log('Ignore backspace/delete/enter keypress');
return;
}
if (charCode === keyCode && charCode !== 13) {
// Chrome sets keyCode = charCode for printable keys
// while LO requires it to be 0
keyCode = 0;
unoKeyCode = this._toUNOKeyCode(keyCode);
}
if (docLayer._debug) {
// key press times will be paired with the invalidation messages
docLayer._debugKeypressQueue.push(+new Date());
}
keyEventFn('input', charCode, unoKeyCode);
}
else if (ev.type === 'keyup') {
if ((this.handleOnKeyDownKeys[keyCode] && charCode === 0) ||
(this.modifier) ||
unoKeyCode === 1280) {
keyEventFn('up', charCode, unoKeyCode);
} else {
// was handled as textinput
}
}
if (keyCode === 9) {
// tab would change focus to other DOM elements
ev.preventDefault();
}
}
else if (!this.modifier && (keyCode === 33 || keyCode === 34) && ev.type === 'keydown') {
if (this._map._docLayer._docType === 'presentation' || this._map._docLayer._docType === 'drawing') {
var partToSelect = keyCode === 33 ? 'prev' : 'next';
this._map._docLayer._preview._scrollViewByDirection(partToSelect);
}
return;
}
else if (!this.modifier && (keyCode === 35 || keyCode === 36) && ev.type === 'keydown') {
if (this._map._docLayer._docType === 'drawing' && app.file.fileBasedView === true) {
partToSelect = keyCode === 36 ? 0 : this._map._docLayer._parts -1;
this._map._docLayer._preview._scrollViewToPartPosition(partToSelect);
}
return;
}
else if (ev.type === 'keydown') {
var key = ev.keyCode;
var map = this._map;
if (key in this._panKeys && !ev.shiftKey) {
if (map._panAnim && map._panAnim._inProgress) {
return;
}
map.fire('scrollby', {x: this._panKeys[key][0], y: this._panKeys[key][1]});
}
else if (key in this._panKeys && ev.shiftKey &&
!docLayer._textCSelections.empty()) {
// if there is a selection and the user wants to modify it
keyEventFn('input', charCode, unoKeyCode);
}
else if (key in this._zoomKeys) {
map.setZoom(map.getZoom() + (ev.shiftKey ? 3 : 1) * this._zoomKeys[key], null, true /* animate? */);
}
}
L.DomEvent.stopPropagation(ev);
},
_isCtrlKey: function (e) {
if (window.ThisIsTheiOSApp || navigator.appVersion.indexOf('Mac') != -1 || navigator.userAgent.indexOf('Mac') != -1)
return e.metaKey;
else
return e.ctrlKey;
},
// Given a DOM keyboard event that happened while the Control key was depressed,
// triggers the appropriate action or coolwsd message.
_handleCtrlCommand: function (e) {
if (this._map.uiManager.isUIBlocked())
return;
// Control
if (e.keyCode == 17)
return true;
if (e.type !== 'keydown' && e.key !== 'c' && e.key !== 'v' && e.key !== 'x' &&
/* Safari */ e.keyCode !== 99 && e.keyCode !== 118 && e.keyCode !== 120) {
e.preventDefault();
return true;
}
if (e.keyCode !== 67 && e.keyCode !== 86 && e.keyCode !== 88 &&
/* Safari */ e.keyCode !== 99 && e.keyCode !== 118 && e.keyCode !== 120 &&
e.key !== 'c' && e.key !== 'v' && e.key !== 'x') {
// not copy or paste
e.preventDefault();
}
if (this._isCtrlKey(e) && e.shiftKey && e.key === '?') {
this._map.showHelp('keyboard-shortcuts');
e.preventDefault();
return true;
}
// Handles paste special. The "Your browser" thing seems to indicate that this code
// snippet is relevant in a browser only.
if (!window.ThisIsAMobileApp && e.ctrlKey && e.shiftKey && e.altKey && (e.key === 'v' || e.key === 'V')) {
var map = this._map;
var msg = _('<p>Your browser has very limited access to the clipboard</p><p>Please press now: <kbd>Ctrl</kbd><span class="kbd--plus">+</span><kbd>V</kbd> to see more options</p><p class="vex-footnote">Close popup to ignore paste special</p>');
msg = L.Util.replaceCtrlInMac(msg);
this._map._clip.pasteSpecialVex = vex.open({
unsafeContent: msg,
showCloseButton: true,
escapeButtonCloses: true,
overlayClosesOnClick: false,
buttons: {},
afterOpen: function() {
map.focus();
}
});
return true;
}
// Handles unformatted paste
if (this._isCtrlKey(e) && e.shiftKey && (e.key === 'v' || e.key === 'V')) {
return true;
}
if (this._isCtrlKey(e) && (e.key === 'k' || e.key === 'K')) {
this._map.showHyperlinkDialog();
e.preventDefault();
return true;
}
if (this._isCtrlKey(e) && (e.key === 'z' || e.key === 'Z')) {
app.socket.sendMessage('uno .uno:Undo');
e.preventDefault();
return true;
}
if (this._isCtrlKey(e) && (e.key === 'y' || e.key === 'Y')) {
app.socket.sendMessage('uno .uno:Redo');
e.preventDefault();
return true;
}
if (this._isCtrlKey(e) && !e.shiftKey && !e.altKey && (e.key === 'f' || e.key === 'F')) {
this._map.fire('focussearch');
e.preventDefault();
return true;
}
if (e.altKey || e.shiftKey) {
// need to handle Ctrl + Alt + C separately for Firefox
if (e.key === 'c' && e.altKey) {
this._map.insertComment();
return true;
}
// Ctrl + Alt
if (!e.shiftKey) {
switch (e.keyCode) {
case 53: // 5
app.socket.sendMessage('uno .uno:Strikeout');
return true;
case 70: // f
app.socket.sendMessage('uno .uno:InsertFootnote');
return true;
case 67: // c
case 77: // m
app.socket.sendMessage('uno .uno:InsertAnnotation');
return true;
case 68: // d
app.socket.sendMessage('uno .uno:InsertEndnote');
return true;
}
} else if (e.altKey) {
switch (e.keyCode) {
case 68: // Ctrl + Shift + Alt + d for tile debugging mode
this._map._docLayer.toggleTileDebugMode();
}
}
return false;
}
/* Without specifying the key type, the messages are sent twice (both keydown/up) */
if (e.type === 'keydown' && window.ThisIsAMobileApp) {
if (e.key === 'c' || e.key === 'C') {
app.socket.sendMessage('uno .uno:Copy');
return true;
}
else if (e.key === 'v' || e.key === 'V') {
app.socket.sendMessage('uno .uno:Paste');
return true;
}
else if (e.key === 'x' || e.key === 'X') {
app.socket.sendMessage('uno .uno:Cut');
return true;
}
if (window.ThisIsTheAndroidApp)
e.preventDefault();
}
switch (e.keyCode) {
case 51: // 3
if (this._map.getDocType() === 'spreadsheet') {
app.socket.sendMessage('uno .uno:SetOptimalColumnWidthDirect');
app.socket.sendMessage('commandvalues command=.uno:ViewRowColumnHeaders');
return true;
}
return false;
case 53: // 5
if (this._map.getDocType() === 'spreadsheet') {
app.socket.sendMessage('uno .uno:Strikeout');
return true;
}
return false;
case 67: // 'C'
case 88: // 'X'
case 99: // 'c'
case 120: // 'x'
case 91: // Left Cmd (Safari)
case 93: // Right Cmd (Safari)
// we prepare for a copy or cut event
this._map.focus();
this._map._textInput.select();
return true;
case 80: // p
this._map.print();
return true;
case 83: // s
// Save only when not read-only.
if (!this._map.isPermissionReadOnly()) {
this._map.fire('postMessage', {msgId: 'UI_Save'});
if (!this._map._disableDefaultAction['UI_Save']) {
this._map.save(false /* An explicit save should terminate cell edit */,
false /* An explicit save should save it again */);
}
}
return true;
case 86: // v
case 118: // v (Safari)
return true;
case 112: // f1
app.socket.sendMessage('uno .uno:NoteVisible');
return true;
case 188: // ,
app.socket.sendMessage('uno .uno:SubScript');
return true;
case 190: // .
app.socket.sendMessage('uno .uno:SuperScript');
return true;
}
if (e.type === 'keypress' && (e.ctrlKey || e.metaKey) &&
(e.key === 'c' || e.key === 'v' || e.key === 'x')) {
// need to handle this separately for Firefox
return true;
}
return false;
}
});