Keyboard shortcuts for modifying options menu, creating new note, and deleting note
This commit is contained in:
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
94
app/assets/javascripts/app/services/keyboardManager.js
Normal file
94
app/assets/javascripts/app/services/keyboardManager.js
Normal 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);
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user