Keyboard shortcuts for modifying options menu, creating new note, and deleting note

This commit is contained in:
Mo Bitar
2019-05-11 21:31:59 -05:00
parent 302aada188
commit 6c5d01a99d
4 changed files with 164 additions and 33 deletions

View File

@@ -23,7 +23,8 @@ angular.module('app')
}
})
.controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, actionsManager,
syncManager, modelManager, themeManager, componentManager, storageManager, sessionHistory, privilegesManager) {
syncManager, modelManager, themeManager, componentManager, storageManager, sessionHistory,
privilegesManager, keyboardManager) {
this.spellcheck = true;
this.componentManager = componentManager;
@@ -121,6 +122,9 @@ angular.module('app')
this.showExtensions = false;
this.showMenu = false;
this.noteStatus = null;
// When setting alt key down and deleting note, an alert will come up and block the key up event when alt is released.
// We reset it on set note so that the alt menu restores to default.
this.altKeyDown = false;
this.loadTagsString();
let onReady = () => {
@@ -796,10 +800,24 @@ angular.module('app')
syncManager.sync();
}
this.altKeyObserver = keyboardManager.addKeyObserver({
modifiers: [KeyboardManager.KeyModifierAlt],
onKeyDown: () => {
this.altKeyDown = true;
},
onKeyUp: () => {
this.altKeyDown = false;
}
})
this.deleteKeyObserver = keyboardManager.addKeyObserver({
key: KeyboardManager.KeyBackspace,
notElementIds: ["note-text-editor", "note-title-editor"],
modifiers: [KeyboardManager.KeyModifierMeta],
onKeyDown: () => {
this.deleteNote();
},
})
/*
Editor Customization
@@ -810,49 +828,57 @@ angular.module('app')
return;
}
this.loadedTabListener = true;
/**
* Insert 4 spaces when a tab key is pressed,
* only used when inside of the text editor.
* If the shift key is pressed first, this event is
* not fired.
*/
var parent = this;
var handleTab = function (event) {
if(!event.shiftKey && event.which == 9) {
if(parent.note.locked) {
const editor = document.getElementById("note-text-editor");
this.tabObserver = keyboardManager.addKeyObserver({
element: editor,
key: KeyboardManager.KeyTab,
onKeyDown: (event) => {
if(event.shiftKey) {
return;
}
if(this.note.locked) {
return;
}
event.preventDefault();
// Using document.execCommand gives us undo support
if(!document.execCommand("insertText", false, "\t")) {
let insertSuccessful = document.execCommand("insertText", false, "\t");
if(!insertSuccessful) {
// document.execCommand works great on Chrome/Safari but not Firefox
var start = this.selectionStart;
var end = this.selectionEnd;
var start = editor.selectionStart;
var end = editor.selectionEnd;
var spaces = " ";
// Insert 4 spaces
this.value = this.value.substring(0, start)
+ spaces + this.value.substring(end);
editor.value = editor.value.substring(0, start)
+ spaces + editor.value.substring(end);
// Place cursor 4 spaces away from where
// the tab key was pressed
this.selectionStart = this.selectionEnd = start + 4;
editor.selectionStart = editor.selectionEnd = start + 4;
}
parent.note.text = this.value;
parent.changesMade({bypassDebouncer: true});
this.note.text = editor.value;
this.changesMade({bypassDebouncer: true});
},
})
// This handles when the editor itself is destroyed, and not when our controller is destroyed.
angular.element(editor).on('$destroy', function(){
if(this.tabObserver) {
keyboardManager.removeKeyObserver(this.tabObserver);
this.loadedTabListener = false;
}
}
var element = document.getElementById("note-text-editor");
element.addEventListener('keydown', handleTab);
angular.element(element).on('$destroy', function(){
window.removeEventListener('keydown', handleTab);
this.loadedTabListener = false;
}.bind(this))
}.bind(this));
}
});
});

View File

@@ -23,7 +23,7 @@ angular.module('app')
}
})
.controller('NotesCtrl', function (authManager, $timeout, $rootScope, modelManager,
syncManager, storageManager, desktopManager, privilegesManager) {
syncManager, storageManager, desktopManager, privilegesManager, keyboardManager) {
this.panelController = {};
this.searchSubmitted = false;
@@ -566,4 +566,15 @@ angular.module('app')
return result;
};
// In the browser we're not allowed to override cmd/ctrl + n, so we have to use Control modifier as well.
// These rules don't apply to desktop, but probably better to be consistent.
this.newNoteKeyObserver = keyboardManager.addKeyObserver({
key: "n",
modifiers: [KeyboardManager.KeyModifierMeta, KeyboardManager.KeyModifierCtrl],
onKeyDown: (event) => {
event.preventDefault();
this.createNewNote();
}
})
});

View File

@@ -0,0 +1,94 @@
class KeyboardManager {
constructor($timeout) {
this.observers = [];
this.$timeout = $timeout;
KeyboardManager.KeyTab = "Tab";
KeyboardManager.KeyBackspace = "Backspace";
KeyboardManager.KeyModifierShift = "Shift";
KeyboardManager.KeyModifierCtrl = "Control";
// ⌘ key on Mac, ⊞ key on Windows
KeyboardManager.KeyModifierMeta = "Meta";
KeyboardManager.KeyModifierAlt = "Alt";
KeyboardManager.KeyEventDown = "KeyEventDown";
KeyboardManager.KeyEventUp = "KeyEventUp";
window.addEventListener('keydown', this.handleKeyDown.bind(this));
window.addEventListener('keyup', this.handleKeyUp.bind(this));
}
eventMatchesKeyAndModifiers(event, key, modifiers = []) {
for(let modifier of modifiers) {
// For a modifier like ctrlKey, must check both event.ctrlKey and event.key.
// That's because on keyup, event.ctrlKey would be false, but event.key == Control would be true.
let matches = (
((event.ctrlKey || event.key == KeyboardManager.KeyModifierCtrl) && modifier === KeyboardManager.KeyModifierCtrl) ||
((event.metaKey || event.key == KeyboardManager.KeyModifierMeta) && modifier === KeyboardManager.KeyModifierMeta) ||
((event.altKey || event.key == KeyboardManager.KeyModifierAlt) && modifier === KeyboardManager.KeyModifierAlt) ||
((event.shiftKey || event.key == KeyboardManager.KeyModifierShift) && modifier === KeyboardManager.KeyModifierShift)
)
if(!matches) {
return false;
}
}
// Modifers match, check key
if(!key) {
return true;
}
return key == event.key;
}
notifyObserver(event, keyEventType) {
for(let observer of this.observers) {
if(observer.element && event.target != observer.element) {
continue;
}
if(observer.notElement && observer.notElement == event.target) {
continue;
}
if(observer.notElementIds && observer.notElementIds.includes(event.target.id)) {
continue;
}
if(this.eventMatchesKeyAndModifiers(event, observer.key, observer.modifiers)) {
let callback = keyEventType == KeyboardManager.KeyEventDown ? observer.onKeyDown : observer.onKeyUp;
if(callback) {
this.$timeout(() => {
callback(event);
})
}
}
}
}
handleKeyDown(event) {
this.notifyObserver(event, KeyboardManager.KeyEventDown);
}
handleKeyUp(event) {
this.notifyObserver(event, KeyboardManager.KeyEventUp);
}
addKeyObserver({key, modifiers, onKeyDown, onKeyUp, element, notElement, notElementIds}) {
let observer = {key, modifiers, onKeyDown, onKeyUp, element, notElement, notElementIds};
this.observers.push(observer);
return observer;
}
removeKeyObserver(observer) {
this.observers.splice(this.observers.indexOf(observer), 1);
}
}
angular.module('app').service('keyboardManager', KeyboardManager);

View File

@@ -38,13 +38,13 @@
%menu-row{"label" => "ctrl.note.locked ? 'Unlock' : 'Lock'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleLockNote()", "desc" => "'Locking notes prevents unintentional editing'"}
%menu-row{"label" => "ctrl.note.content.protected ? 'Unprotect' : 'Protect'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleProtectNote()", "desc" => "'Protecting a note will require credentials to view it (Manage Privileges via Account menu)'"}
%menu-row{"label" => "'Preview'", "circle" => "ctrl.note.content.hidePreview ? 'danger' : 'success'", "circle-align" => "'right'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleNotePreview()", "desc" => "'Hide or unhide the note preview from the list of notes'"}
%menu-row{"ng-show" => "!ctrl.note.content.trashed && !ctrl.note.errorDecrypting", "label" => "'Move to Trash'", "action" => "ctrl.selectedMenuItem(); ctrl.deleteNote()", "stylekit-class" => "'warning'", "desc" => "'Send this note to the trash'"}
%menu-row{"ng-show" => "!ctrl.altKeyDown && !ctrl.note.content.trashed && !ctrl.note.errorDecrypting", "label" => "'Move to Trash'", "action" => "ctrl.selectedMenuItem(); ctrl.deleteNote()", "stylekit-class" => "'warning'", "desc" => "'Send this note to the trash'"}
%menu-row{"ng-show" => "!ctrl.note.content.trashed && ctrl.note.errorDecrypting", "label" => "'Delete Permanently'", "action" => "ctrl.selectedMenuItem(); ctrl.deleteNotePermanantely()", "stylekit-class" => "'danger'", "desc" => "'Delete this note permanently from all your devices'"}
%div{"ng-if" => "ctrl.note.content.trashed"}
%menu-row{"label" => "'Restore'", "action" => "ctrl.selectedMenuItem(true); ctrl.restoreTrashedNote()", "stylekit-class" => "'info'", "desc" => "'Undelete this note and restore it back into your notes'"}
%div{"ng-if" => "ctrl.note.content.trashed || ctrl.altKeyDown"}
%menu-row{"label" => "'Restore'", "ng-show" => "ctrl.note.content.trashed", "action" => "ctrl.selectedMenuItem(true); ctrl.restoreTrashedNote()", "stylekit-class" => "'info'", "desc" => "'Undelete this note and restore it back into your notes'"}
%menu-row{"label" => "'Delete Permanently'", "action" => "ctrl.selectedMenuItem(true); ctrl.deleteNotePermanantely()", "stylekit-class" => "'danger'", "desc" => "'Delete this note permanently from all your devices'"}
%menu-row{"label" => "'Empty Trash'", "subtitle" => "ctrl.getTrashCount() + ' notes in trash'", "action" => "ctrl.selectedMenuItem(true); ctrl.emptyTrash()", "stylekit-class" => "'danger'", "desc" => "'Permanently delete all notes in the trash'"}
%menu-row{"label" => "'Empty Trash'", "ng-show" => "ctrl.note.content.trashed || !ctrl.altKeyDown", "subtitle" => "ctrl.getTrashCount() + ' notes in trash'", "action" => "ctrl.selectedMenuItem(true); ctrl.emptyTrash()", "stylekit-class" => "'danger'", "desc" => "'Permanently delete all notes in the trash'"}
.sk-menu-panel-section
.sk-menu-panel-header