From 5e7f531c7fbfbfb560535f88e2f5a46659dc73b1 Mon Sep 17 00:00:00 2001 From: m hagoort Date: Sat, 26 Sep 2020 00:02:29 +0200 Subject: [PATCH] Revamp shortcuts handler (not tested yet) --- .eslintrc.js | 5 +- dev/App/Abstract.js | 2 +- dev/Common/Globals.js | 24 +++------ dev/Common/Selector.js | 52 ++++-------------- dev/View/Admin/Settings/Menu.js | 3 +- dev/View/Popup/Ask.js | 5 +- dev/View/Popup/Compose.js | 19 ++++--- dev/View/Popup/ComposeOpenPgp.js | 3 +- dev/View/Popup/Contacts.js | 4 +- dev/View/Popup/KeyboardShortcutsHelp.js | 6 +-- dev/View/Popup/MessageOpenPgp.js | 3 +- dev/View/Popup/Plugin.js | 2 +- dev/View/Popup/ViewOpenPgpKey.js | 2 +- dev/View/User/AbstractSystemDropDown.js | 4 +- dev/View/User/MailBox/FolderList.js | 11 ++-- dev/View/User/MailBox/MessageList.js | 69 ++++++++++++------------ dev/View/User/MailBox/MessageView.js | 70 ++++++++++--------------- dev/View/User/Settings/Menu.js | 3 +- dev/prototype.js | 1 + dev/shortcuts.js | 70 +++++++++++++++++++++++++ tasks/config.js | 2 +- 21 files changed, 190 insertions(+), 170 deletions(-) create mode 100644 dev/shortcuts.js diff --git a/.eslintrc.js b/.eslintrc.js index ab89cffaa..11abf973b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -13,10 +13,11 @@ module.exports = { es6: true }, globals: { - // RainLoop + // SnappyMail 'rainloopI18N': "readonly", 'rainloopTEMPLATES': "readonly", 'rl': "readonly", + 'shortcuts': "readonly", // '__APP_BOOT': "readonly", // deb/boot.js 'progressJs': "readonly", @@ -31,8 +32,6 @@ module.exports = { 'hasher': "readonly", 'signals': "readonly", 'Crossroads': "readonly", - // vendors/keymaster - 'key': "readonly", // vendors/jua 'Jua': "readonly", // vendors/qr.js diff --git a/dev/App/Abstract.js b/dev/App/Abstract.js index 9428f7a19..9918a6106 100644 --- a/dev/App/Abstract.js +++ b/dev/App/Abstract.js @@ -42,7 +42,7 @@ class AbstractApp { $doc.addEventListener('keypress', fn); $doc.addEventListener('click', fn); - key('esc, enter', KeyState.All, () => rl.Dropdowns.detectVisibility()); + shortcuts.add(['escape','enter'], '', KeyState.All, () => rl.Dropdowns.detectVisibility()); } remote() { diff --git a/dev/Common/Globals.js b/dev/Common/Globals.js index 5ca162318..ea1c399fb 100644 --- a/dev/Common/Globals.js +++ b/dev/Common/Globals.js @@ -54,24 +54,12 @@ export const keyScope = ko.computed({ if (KeyState.Menu !== value) { if (KeyState.Compose === value) { // disableKeyFilter - key.filter = () => useKeyboardShortcuts(); + shortcuts.filter = () => useKeyboardShortcuts(); } else { // restoreKeyFilter - key.filter = (event) => { - if (useKeyboardShortcuts()) { - const el = event.target, - nodeName = el ? el.nodeName : ''; - - return !( - 'INPUT' === nodeName || - 'SELECT' === nodeName || - 'TEXTAREA' === nodeName || - (el && el.contentEditable) - ); - } - - return false; - }; + shortcuts.filter = event => !(event.target.matches + && (event.target.matches('input,select,textarea') + || event.target.closest('[contenteditable]'))); } keyScopeFake(value); @@ -86,13 +74,13 @@ export const keyScope = ko.computed({ keyScopeReal.subscribe((value) => { // console.log('keyScope=' + sValue); // DEBUG - key.setScope(value); + shortcuts.setScope(value); }); dropdownVisibility.subscribe((value) => { if (value) { keyScope(KeyState.Menu); - } else if (KeyState.Menu === key.getScope()) { + } else if (KeyState.Menu === shortcuts.getScope()) { keyScope(keyScopeFake()); } }); diff --git a/dev/Common/Selector.js b/dev/Common/Selector.js index 7d01c2c52..e8d12f93c 100644 --- a/dev/Common/Selector.js +++ b/dev/Common/Selector.js @@ -273,7 +273,7 @@ class Selector { } }); - key('enter', keyScope, () => { + shortcuts.add('enter', '', keyScope, () => { const focused = this.focusedItem(); if (focused && !focused.selected()) { this.actionClick(focused); @@ -283,48 +283,16 @@ class Selector { return true; }); - key('ctrl+up, command+up, ctrl+down, command+down', keyScope, () => false); + shortcuts.add('arrowup', 'meta', keyScope, () => false); + shortcuts.add('arrowdown', 'meta', keyScope, () => false); - key('up, shift+up, down, shift+down, home, end, pageup, pagedown, insert, space', keyScope, (event, handler) => { - if (event && handler && handler.shortcut) { - let eventKey; - switch (handler.shortcut) { - case 'up': - case 'shift+up': - eventKey = 'ArrowUp'; - break; - case 'down': - case 'shift+down': - eventKey = 'ArrowDown'; - break; - case 'insert': - eventKey = 'Insert'; - break; - case 'space': - eventKey = ' '; - break; - case 'home': - eventKey = 'Home'; - break; - case 'end': - eventKey = 'End'; - break; - case 'pageup': - eventKey = 'PageUp'; - break; - case 'pagedown': - eventKey = 'PageDown'; - break; - // no default - } - - if (eventKey) { - this.newSelectPosition(eventKey, key.shift); - return false; - } - } - - return true; + shortcuts.add(['arrowup','arrowdown'], 'shift', keyScope, event => { + this.newSelectPosition(event.key, true); + return false; + }); + shortcuts.add(['arrowup','arrowdown','home','end','pageup','pagedown','insert','space'], '', keyScope, event => { + this.newSelectPosition(event.key, false); + return false; }); } } diff --git a/dev/View/Admin/Settings/Menu.js b/dev/View/Admin/Settings/Menu.js index d85a1aab4..ca8bdb2e1 100644 --- a/dev/View/Admin/Settings/Menu.js +++ b/dev/View/Admin/Settings/Menu.js @@ -26,7 +26,8 @@ class MenuSettingsAdminView extends AbstractViewNext { } onBuild(dom) { - key('up, down', KeyState.Settings, settingsMenuKeysHandler(dom.querySelectorAll('.b-admin-menu .e-item'))); + shortcuts.add(['arrowup','arrowdown'], '', KeyState.Settings, + settingsMenuKeysHandler(dom.querySelectorAll('.b-admin-menu .e-item'))); } } diff --git a/dev/View/Popup/Ask.js b/dev/View/Popup/Ask.js index 5e2ee966f..786f9c6c8 100644 --- a/dev/View/Popup/Ask.js +++ b/dev/View/Popup/Ask.js @@ -86,7 +86,8 @@ class AskPopupView extends AbstractViewNext { } onBuild() { - key('tab, shift+tab, right, left', KeyState.PopupAsk, () => { +// shortcuts.add('tab', 'shift', KeyState.PopupAsk, () => { + shortcuts.add(['tab','arrowright','arrowleft'], '', KeyState.PopupAsk, () => { let btn = this.querySelector('.buttonYes'); if (btn.matches(':focus')) { btn = this.querySelector('.buttonNo'); @@ -95,7 +96,7 @@ class AskPopupView extends AbstractViewNext { return false; }); - key('esc', KeyState.PopupAsk, () => { + shortcuts.add('escape', '', KeyState.PopupAsk, () => { this.noClick(); return false; }); diff --git a/dev/View/Popup/Compose.js b/dev/View/Popup/Compose.js index f3572a5ab..fb49c3dc5 100644 --- a/dev/View/Popup/Compose.js +++ b/dev/View/Popup/Compose.js @@ -1114,9 +1114,10 @@ class ComposePopupView extends AbstractViewNext { onBuild(dom) { this.initUploader(); - key('ctrl+q, command+q, ctrl+w, command+w', KeyState.Compose, ()=>false); + shortcuts.add('q', 'meta', KeyState.Compose, ()=>false); + shortcuts.add('w', 'meta', KeyState.Compose, ()=>false); - key('`', KeyState.Compose, () => { + shortcuts.add('`', '', KeyState.Compose, () => { if (this.oEditor && !this.oEditor.hasFocus() && !inFocus()) { this.identitiesDropdownTrigger(true); return false; @@ -1125,31 +1126,35 @@ class ComposePopupView extends AbstractViewNext { return true; }); - key('ctrl+`', KeyState.Compose, () => { + shortcuts.add('`', 'ctrl', KeyState.Compose, () => { this.identitiesDropdownTrigger(true); return false; }); - key('esc, ctrl+down, command+down', KeyState.Compose, () => { + shortcuts.add('escape', '', KeyState.Compose, () => { + this.skipCommand(); + return false; + }); + shortcuts.add('arrowdown', 'meta', KeyState.Compose, () => { this.skipCommand(); return false; }); if (this.allowFolders) { - key('ctrl+s, command+s', KeyState.Compose, () => { + shortcuts.add('s', 'meta', KeyState.Compose, () => { this.saveCommand(); return false; }); } if (Settings.app('allowCtrlEnterOnCompose')) { - key('ctrl+enter, command+enter', KeyState.Compose, () => { + shortcuts.add('enter', 'meta', KeyState.Compose, () => { this.sendCommand(); return false; }); } - key('shift+esc', KeyState.Compose, () => { + shortcuts.add('escape', 'shift', KeyState.Compose, () => { if (this.modalVisibility()) { this.tryToClosePopup(); } diff --git a/dev/View/Popup/ComposeOpenPgp.js b/dev/View/Popup/ComposeOpenPgp.js index 7ea3da30a..125f41ce6 100644 --- a/dev/View/Popup/ComposeOpenPgp.js +++ b/dev/View/Popup/ComposeOpenPgp.js @@ -303,7 +303,8 @@ class ComposeOpenPgpPopupView extends AbstractViewNext { } onBuild() { - key('tab,shift+tab', KeyState.PopupComposeOpenPGP, () => { +// shortcuts.add(('tab', 'shift', KeyState.PopupComposeOpenPGP, () => { + shortcuts.add('tab', '', KeyState.PopupComposeOpenPGP, () => { let btn = this.querySelector('.inputPassword'); if (btn.matches(':focus')) { btn = this.querySelector('.buttonDo'); diff --git a/dev/View/Popup/Contacts.js b/dev/View/Popup/Contacts.js index ea5d76ae7..8cbd898e4 100644 --- a/dev/View/Popup/Contacts.js +++ b/dev/View/Popup/Contacts.js @@ -606,12 +606,12 @@ class ContactsPopupView extends AbstractViewNext { onBuild(dom) { this.selector.init(dom.querySelector('.b-list-content'), KeyState.ContactList); - key('delete', KeyState.ContactList, () => { + shortcuts.add('delete', '', KeyState.ContactList, () => { this.deleteCommand(); return false; }); - key('c, w', KeyState.ContactList, () => { + shortcuts.add(['c','w'], '', KeyState.ContactList, () => { this.newMessageCommand(); return false; }); diff --git a/dev/View/Popup/KeyboardShortcutsHelp.js b/dev/View/Popup/KeyboardShortcutsHelp.js index 31d2f791e..1e234b93a 100644 --- a/dev/View/Popup/KeyboardShortcutsHelp.js +++ b/dev/View/Popup/KeyboardShortcutsHelp.js @@ -16,8 +16,8 @@ class KeyboardShortcutsHelpPopupView extends AbstractViewNext { onBuild(dom) { dom.querySelectorAll('a[data-toggle="tab"]').forEach(node => node.Tab || new BSN.Tab(node)); - key( - 'tab, shift+tab, left, right', +// shortcuts.add('tab', 'shift', + shortcuts.add(['tab','arrowleft','arrowright'], '', KeyState.PopupKeyboardShortcutsHelp, ((event, handler)=>{ if (event && handler) { @@ -26,7 +26,7 @@ class KeyboardShortcutsHelpPopupView extends AbstractViewNext { let next = 0; tabs.forEach((node, index) => { if (node.matches('.active')) { - if (['tab','right'].includes(handler.shortcut)) { + if (['tab','arrowright'].includes(handler.shortcut)) { next = index < last ? index+1 : 0; } else { next = index ? index-1 : last; diff --git a/dev/View/Popup/MessageOpenPgp.js b/dev/View/Popup/MessageOpenPgp.js index 11ba0b026..62a7dab5f 100644 --- a/dev/View/Popup/MessageOpenPgp.js +++ b/dev/View/Popup/MessageOpenPgp.js @@ -79,7 +79,8 @@ class MessageOpenPgpPopupView extends AbstractViewNext { } onBuild(oDom) { - key('tab,shift+tab', KeyState.PopupMessageOpenPGP, () => { +// shortcuts.add('tab','shift', KeyState.PopupMessageOpenPGP, () => { + shortcuts.add('tab', '', KeyState.PopupMessageOpenPGP, () => { let btn = this.querySelector('.inputPassword'); if (btn.matches(':focus')) { btn = this.querySelector('.buttonDo'); diff --git a/dev/View/Popup/Plugin.js b/dev/View/Popup/Plugin.js index d6293171a..1af53e51c 100644 --- a/dev/View/Popup/Plugin.js +++ b/dev/View/Popup/Plugin.js @@ -110,7 +110,7 @@ class PluginPopupView extends AbstractViewNext { } onBuild() { - key('esc', KeyState.All, () => { + shortcuts.add('escape', '', KeyState.All, () => { if (this.modalVisibility()) { this.tryToClosePopup(); } diff --git a/dev/View/Popup/ViewOpenPgpKey.js b/dev/View/Popup/ViewOpenPgpKey.js index e8bbb7328..1d2ab94cb 100644 --- a/dev/View/Popup/ViewOpenPgpKey.js +++ b/dev/View/Popup/ViewOpenPgpKey.js @@ -40,7 +40,7 @@ class ViewOpenPgpKeyPopupView extends AbstractViewNext { } onBuild() { - key('ctrl+a, command+a', KeyState.PopupViewOpenPGP, () => { + shortcuts.add('a', 'meta', KeyState.PopupViewOpenPGP, () => { this.selectKey(); return false; }); diff --git a/dev/View/User/AbstractSystemDropDown.js b/dev/View/User/AbstractSystemDropDown.js index 2749578da..3c90c69b8 100644 --- a/dev/View/User/AbstractSystemDropDown.js +++ b/dev/View/User/AbstractSystemDropDown.js @@ -81,7 +81,7 @@ class AbstractSystemDropDownUserView extends AbstractViewNext { } onBuild() { - key('`', [KeyState.MessageList, KeyState.MessageView, KeyState.Settings], () => { + shortcuts.add('`', '', [KeyState.MessageList, KeyState.MessageView, KeyState.Settings], () => { if (this.viewModelVisibility()) { MessageStore.messageFullScreenMode(false); this.accountMenuDropdownTrigger(true); @@ -89,7 +89,7 @@ class AbstractSystemDropDownUserView extends AbstractViewNext { }); // shortcuts help - key('shift+/', [KeyState.MessageList, KeyState.MessageView, KeyState.Settings], () => { + shortcuts.add('/', 'shift', [KeyState.MessageList, KeyState.MessageView, KeyState.Settings], () => { if (this.viewModelVisibility()) { showScreenPopup(require('View/Popup/KeyboardShortcutsHelp')); return false; diff --git a/dev/View/User/MailBox/FolderList.js b/dev/View/User/MailBox/FolderList.js index 63f5d7cca..9b655c973 100644 --- a/dev/View/User/MailBox/FolderList.js +++ b/dev/View/User/MailBox/FolderList.js @@ -121,7 +121,7 @@ class FolderListMailBoxUserView extends AbstractViewNext { el && fSelectFolder(el, event, false); }); - key('up, down', KeyState.FolderList, (event, handler) => { + shortcuts.add(['arrowup','arrowdown'], '', KeyState.FolderList, event => { let items = [], index = 0; dom.querySelectorAll('.b-folders .e-item .e-link:not(.hidden)').forEach(node => { if (node.offsetHeight || node.getClientRects().length) { @@ -133,7 +133,7 @@ class FolderListMailBoxUserView extends AbstractViewNext { } }); if (items.length) { - if (handler && 'up' === handler.shortcut) { + if ('ArrowUp' === event.key) { index && --index; } else if (index < items.length - 1) { ++index; @@ -145,7 +145,7 @@ class FolderListMailBoxUserView extends AbstractViewNext { return false; }); - key('enter', KeyState.FolderList, () => { + shortcuts.add('enter', '', KeyState.FolderList, () => { const item = qs('.b-folders .e-item .e-link:not(.hidden).focused'); if (item) { AppStore.focusedState(Focused.MessageList); @@ -155,7 +155,7 @@ class FolderListMailBoxUserView extends AbstractViewNext { return false; }); - key('space', KeyState.FolderList, () => { + shortcuts.add('space', '', KeyState.FolderList, () => { const item = qs('.b-folders .e-item .e-link:not(.hidden).focused'), folder = item && ko.dataFor(item); if (folder) { @@ -167,7 +167,8 @@ class FolderListMailBoxUserView extends AbstractViewNext { return false; }); - key('esc, tab, shift+tab, right', KeyState.FolderList, () => { +// shortcuts.add('tab', 'shift', KeyState.FolderList, () => { + shortcuts.add(['escape','tab','arrowright'], '', KeyState.FolderList, () => { AppStore.focusedState(Focused.MessageList); moveAction(false); return false; diff --git a/dev/View/User/MailBox/MessageList.js b/dev/View/User/MailBox/MessageList.js index 88c410593..068c265f7 100644 --- a/dev/View/User/MailBox/MessageList.js +++ b/dev/View/User/MailBox/MessageList.js @@ -742,7 +742,7 @@ class MessageListMailBoxUserView extends AbstractViewNext { } initShortcuts() { - key('enter', KeyState.MessageList, () => { + shortcuts.add('enter', '', KeyState.MessageList, () => { if (this.message() && this.useAutoSelect()) { dispatchEvent(new CustomEvent('mailbox.message-view.toggle-full-screen')); return false; @@ -753,46 +753,40 @@ class MessageListMailBoxUserView extends AbstractViewNext { if (Settings.capa(Capa.MessageListActions)) { // archive (zip) - key('z', [KeyState.MessageList, KeyState.MessageView], () => { + shortcuts.add('z', '', [KeyState.MessageList, KeyState.MessageView], () => { this.archiveCommand(); return false; }); // delete - key('delete, shift+delete, shift+3', KeyState.MessageList, (event, handler) => { - if (event) { - if (MessageStore.messageListCheckedOrSelected().length) { - if (handler && 'shift+delete' === handler.shortcut) { - this.deleteWithoutMoveCommand(); - } else { - this.deleteCommand(); - } - } - - return false; - } - - return true; + shortcuts.add('delete', 'shift', KeyState.MessageList, () => { + MessageStore.messageListCheckedOrSelected().length && this.deleteWithoutMoveCommand(); + return false; + }); +// shortcuts.add('3', 'shift', KeyState.MessageList, () => { + shortcuts.add('delete', '', KeyState.MessageList, () => { + MessageStore.messageListCheckedOrSelected().length && this.deleteCommand(); + return false; }); } if (Settings.capa(Capa.Reload)) { // check mail - key('ctrl+r, command+r', [KeyState.FolderList, KeyState.MessageList, KeyState.MessageView], () => { + shortcuts.add('r', 'meta', [KeyState.FolderList, KeyState.MessageList, KeyState.MessageView], () => { this.reloadCommand(); return false; }); } // check all - key('ctrl+a, command+a', KeyState.MessageList, () => { + shortcuts.add('a', 'meta', KeyState.MessageList, () => { this.checkAll(!(this.checkAll() && !this.isIncompleteChecked())); return false; }); if (Settings.capa(Capa.Composer)) { // write/compose (open compose popup) - key('w,c', [KeyState.MessageList, KeyState.MessageView], () => { + shortcuts.add(['w','c'], '', [KeyState.MessageList, KeyState.MessageView], () => { showScreenPopup(require('View/Popup/Compose')); return false; }); @@ -800,13 +794,13 @@ class MessageListMailBoxUserView extends AbstractViewNext { if (Settings.capa(Capa.MessageListActions)) { // important - star/flag messages - key('i', [KeyState.MessageList, KeyState.MessageView], () => { + shortcuts.add('i', '', [KeyState.MessageList, KeyState.MessageView], () => { this.flagMessagesFast(); return false; }); } - key('t', [KeyState.MessageList], () => { + shortcuts.add('t', '', [KeyState.MessageList], () => { let message = this.selectorMessageSelected(); if (!message) { message = this.selectorMessageFocused(); @@ -821,7 +815,7 @@ class MessageListMailBoxUserView extends AbstractViewNext { if (Settings.capa(Capa.MessageListActions)) { // move - key('m', KeyState.MessageList, () => { + shortcuts.add('m', '', KeyState.MessageList, () => { if (this.newMoveToFolder()) { this.moveNewCommand(); } else { @@ -834,20 +828,20 @@ class MessageListMailBoxUserView extends AbstractViewNext { if (Settings.capa(Capa.MessageListActions)) { // read - key('q', [KeyState.MessageList, KeyState.MessageView], () => { + shortcuts.add('q', '', [KeyState.MessageList, KeyState.MessageView], () => { this.seenMessagesFast(true); return false; }); // unread - key('u', [KeyState.MessageList, KeyState.MessageView], () => { + shortcuts.add('u', '', [KeyState.MessageList, KeyState.MessageView], () => { this.seenMessagesFast(false); return false; }); } if (Settings.capa(Capa.Composer)) { - key('shift+f', [KeyState.MessageList, KeyState.MessageView], () => { + shortcuts.add('f', 'shift', [KeyState.MessageList, KeyState.MessageView], () => { this.multyForwardCommand(); return false; }); @@ -855,14 +849,14 @@ class MessageListMailBoxUserView extends AbstractViewNext { if (Settings.capa(Capa.Search)) { // search input focus - key('/', [KeyState.MessageList, KeyState.MessageView], () => { + shortcuts.add('/', '', [KeyState.MessageList, KeyState.MessageView], () => { this.inputMessageListSearchFocus(true); return false; }); } // cancel search - key('esc', KeyState.MessageList, () => { + shortcuts.add('escape', '', KeyState.MessageList, () => { if (this.messageListSearchDesc()) { this.cancelSearch(); return false; @@ -875,18 +869,21 @@ class MessageListMailBoxUserView extends AbstractViewNext { }); // change focused state - key('tab, shift+tab, left, right', KeyState.MessageList, (event, handler) => { - if (event && handler && ('shift+tab' === handler.shortcut || 'left' === handler.shortcut)) { - AppStore.focusedState(Focused.FolderList); - } else if (this.message()) { - AppStore.focusedState(Focused.MessageView); - } - + shortcuts.add('tab', 'shift', KeyState.MessageList, () => { + AppStore.focusedState(Focused.FolderList); + return false; + }); + shortcuts.add('arrowleft', '', KeyState.MessageList, () => { + AppStore.focusedState(Focused.FolderList); + return false; + }); + shortcuts.add(['tab','arrowright'], '', KeyState.MessageList, () => { + this.message() && AppStore.focusedState(Focused.MessageView); return false; }); - key('ctrl+left, command+left', KeyState.MessageView, ()=>false); - key('ctrl+right, command+right', KeyState.MessageView, ()=>false); + shortcuts.add('arrowleft', 'meta', KeyState.MessageView, ()=>false); + shortcuts.add('arrowright', 'meta', KeyState.MessageView, ()=>false); } prefetchNextTick() { diff --git a/dev/View/User/MailBox/MessageView.js b/dev/View/User/MailBox/MessageView.js index a07a2a93b..c12374a28 100644 --- a/dev/View/User/MailBox/MessageView.js +++ b/dev/View/User/MailBox/MessageView.js @@ -547,26 +547,25 @@ class MessageViewMailBoxUserView extends AbstractViewNext { initShortcuts() { // exit fullscreen, back - key('esc, backspace', KeyState.MessageView, this.escShortcuts.bind(this)); + shortcuts.add(['escape','backspace'], '', KeyState.MessageView, this.escShortcuts.bind(this)); // fullscreen - key('enter', KeyState.MessageView, () => { + shortcuts.add('enter', '', KeyState.MessageView, () => { this.toggleFullScreen(); return false; }); // reply - key('r', [KeyState.MessageList, KeyState.MessageView], () => { + shortcuts.add('r', '', [KeyState.MessageList, KeyState.MessageView], () => { if (MessageStore.message()) { this.replyCommand(); return false; } - return true; }); // replaAll - key('a', [KeyState.MessageList, KeyState.MessageView], () => { + shortcuts.add('a', '', [KeyState.MessageList, KeyState.MessageView], () => { if (MessageStore.message()) { this.replyAllCommand(); return false; @@ -576,7 +575,7 @@ class MessageViewMailBoxUserView extends AbstractViewNext { }); // forward - key('f', [KeyState.MessageList, KeyState.MessageView], () => { + shortcuts.add('f', '', [KeyState.MessageList, KeyState.MessageView], () => { if (MessageStore.message()) { this.forwardCommand(); return false; @@ -586,7 +585,7 @@ class MessageViewMailBoxUserView extends AbstractViewNext { }); // message information - key('ctrl+i, command+i', [KeyState.MessageList, KeyState.MessageView], () => { + shortcuts.add('i', 'meta', [KeyState.MessageList, KeyState.MessageView], () => { if (MessageStore.message()) { this.showFullInfo(!this.showFullInfo()); } @@ -594,7 +593,7 @@ class MessageViewMailBoxUserView extends AbstractViewNext { }); // toggle message blockquotes - key('b', [KeyState.MessageList, KeyState.MessageView], () => { + shortcuts.add('b', '', [KeyState.MessageList, KeyState.MessageView], () => { const message = MessageStore.message(); if (message && message.body) { message.body.querySelectorAll('.rlBlockquoteSwitcher').forEach(node => node.click()); @@ -603,62 +602,49 @@ class MessageViewMailBoxUserView extends AbstractViewNext { return true; }); - key('ctrl+up, command+up, ctrl+left, command+left', [KeyState.MessageList, KeyState.MessageView], () => { + shortcuts.add(['arrowup','arrowleft'], 'meta', [KeyState.MessageList, KeyState.MessageView], () => { this.goUpCommand(); return false; }); - key('ctrl+down, command+down, ctrl+right, command+right', [KeyState.MessageList, KeyState.MessageView], () => { + shortcuts.add(['arrowdown','arrowright'], 'meta', [KeyState.MessageList, KeyState.MessageView], () => { this.goDownCommand(); return false; }); // print - key('ctrl+p, command+p', [KeyState.MessageView, KeyState.MessageList], () => { + shortcuts.add('p', 'meta', [KeyState.MessageView, KeyState.MessageList], () => { if (this.message()) { this.message().printMessage(); } - return false; }); // delete - key('delete, shift+delete', KeyState.MessageView, (event, handler) => { - if (event) { - if (handler && 'shift+delete' === handler.shortcut) { - this.deleteWithoutMoveCommand(); - } else { - this.deleteCommand(); - } - - return false; - } - - return true; + shortcuts.add('delete', '', KeyState.MessageView, () => { + this.deleteCommand(); + return false; + }); + shortcuts.add('delete', 'shift', KeyState.MessageView, () => { + this.deleteWithoutMoveCommand(); + return false; }); // change focused state - key('tab, shift+tab, left', KeyState.MessageView, (event, handler) => { + shortcuts.add('arrowleft', '', KeyState.MessageView, () => { if (!this.fullScreenMode() && this.message() && Layout.NoPreview !== this.layout()) { - if (event && handler && 'left' === handler.shortcut) { - if (this.oMessageScrollerDom && 0 < this.oMessageScrollerDom.scrollLeft) { - return true; - } - - AppStore.focusedState(Focused.MessageList); - } else { - AppStore.focusedState(Focused.MessageList); + if (this.oMessageScrollerDom && 0 < this.oMessageScrollerDom.scrollLeft) { + return true; } - } else if ( - this.message() && - Layout.NoPreview === this.layout() && - event && - handler && - 'left' === handler.shortcut - ) { - return true; + AppStore.focusedState(Focused.MessageList); + return false; + } + }); +// shortcuts.add('tab', 'shift', KeyState.MessageView, (event, handler) => { + shortcuts.add('tab', '', KeyState.MessageView, () => { + if (!this.fullScreenMode() && this.message() && Layout.NoPreview !== this.layout()) { + AppStore.focusedState(Focused.MessageList); } - return false; }); } diff --git a/dev/View/User/Settings/Menu.js b/dev/View/User/Settings/Menu.js index 93a8650e4..72dfd767b 100644 --- a/dev/View/User/Settings/Menu.js +++ b/dev/View/User/Settings/Menu.js @@ -32,7 +32,8 @@ class MenuSettingsUserView extends AbstractViewNext { ); } - key('up, down', KeyState.Settings, settingsMenuKeysHandler(dom.querySelectorAll('.b-settings-menu .e-item'))); + shortcuts.add(['ArrowUp','ArrowDown'], '', KeyState.Settings, + settingsMenuKeysHandler(dom.querySelectorAll('.b-settings-menu .e-item'))); } link(route) { diff --git a/dev/prototype.js b/dev/prototype.js index 5b3fad43d..05bcf410e 100644 --- a/dev/prototype.js +++ b/dev/prototype.js @@ -1,6 +1,7 @@ (w=>{ Array.isNotEmpty = array => Array.isArray(array) && array.length; + Array.prototype.unique((v, i, a) => a.indexOf(v) === i); // Import momentjs locales function w.moment = { diff --git a/dev/shortcuts.js b/dev/shortcuts.js new file mode 100644 index 000000000..9ff1805c7 --- /dev/null +++ b/dev/shortcuts.js @@ -0,0 +1,70 @@ +"use strict"; + +(win => { + +const + doc = document, + meta = /Mac OS X/.test(navigator.userAgent) ? 'meta' : 'ctrl', + actions = {}, + toArray = v => Array.isArray(v) ? v : [v]; +let + _scope = 'all'; + +doc.addEventListener('keydown', event => { + const key = (event.key||event.code||'').toLowerCase(); + if (actions[key] && win.shortcuts.filter(event)) { + let modifiers = []; + event.altKey && modifiers.push('alt'); + event.ctrlKey && modifiers.push('ctrl'); + event.metaKey && modifiers.push('meta'); + event.shiftKey && modifiers.push('shift'); + modifiers = modifiers.join('+'); + if (actions[key][modifiers]) { + // for each potential shortcut + actions[key][modifiers].forEach(cmd => { + // see if it's in the current scope + if (cmd.scope.includes(_scope) || cmd.scope == 'all') { + try { + // call the handler and stop the event if neccessary + if (cmd.method(event) === false) { + event.preventDefault(); + event.stopPropagation(); + } + } catch (e) { + console.error(e); + } + } + }); + } + } +}); + +win.shortcuts = { + add: (keys, modifiers, scope, method) => { + if (method === undefined) { + method = scope; + scope = 'all'; + } + toArray(keys).forEach(key => { + key = key.toLowerCase(); + if (!actions[key]) { + actions[key] = {}; + } + modifiers = toArray(modifiers) + .map(key => 'meta' == key ? meta : key) + .unique().sort().join('+'); + if (!actions[key][modifiers]) { + actions[key][modifiers] = []; + } + actions[key][modifiers].push({scope:toArray(scope), method:method}); + }); + }, + setScope: scope => _scope = scope || 'all', + getScope: () => _scope, + getMetaKey: () => meta, + // ignore keydown in any element that supports keyboard input + filter: event => !(event.target.matches + && (event.target.matches('input,select,textarea') || event.target.closest('[contenteditable]'))) +}; + +})(this); diff --git a/tasks/config.js b/tasks/config.js index a2db47cc9..c1d853357 100644 --- a/tasks/config.js +++ b/tasks/config.js @@ -79,7 +79,7 @@ config.paths.js = { 'vendors/routes/hasher.min.js', // fixed 'vendors/routes/crossroads.min.js', // fixed 'vendors/jua/jua.min.js', // custom - 'vendors/keymaster/keymaster.js', // custom (modified) + 'dev/shortcuts.js', 'vendors/qr.js/qr.min.js', // fixed (license) 'vendors/bootstrap/js/bootstrap.native.min.js', // fixed 'vendors/knockout/build/output/knockout-latest.js',