FEATURE: Bookmark keyboard shortcuts (#9318)

Adds keyboard bindings and associated help menu for selecting reminder type in bookmark modal, and pressing Enter to save.

Introduce the following APIs for `KeyboardShortcuts`:

* `pause` - Uses the provided array of combinations and unbinds them using `Mousetrap`.
* `unpause` - Uses the provided combinations and rebinds them to their default shortcuts listed in `KeyboardShortcuts`.
* `addBindings` - Adds the array of keyboard shortcut bindings and calls the provided callback when a binding is fired with Mousetrap.
* `unbind` - Takes an object literal of a binding map and unbinds all of them e.g. `{ enter: { handler: saveAndClose" } };`
This commit is contained in:
Martin Brennan 2020-04-07 14:03:15 +10:00 committed by GitHub
parent d04ba4b3b2
commit 93c38cc175
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 205 additions and 28 deletions

View File

@ -1,10 +1,22 @@
import { and } from "@ember/object/computed";
import { next } from "@ember/runloop";
import Controller from "@ember/controller";
import { Promise } from "rsvp";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import discourseComputed from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { ajax } from "discourse/lib/ajax";
import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts";
// global shortcuts that interfere with these modal shortcuts, they are rebound when the
// modal is closed
//
// c createTopic
// r replyToPost
// l toggle like
// d deletePost
// t replyAsNewTopic
const GLOBAL_SHORTCUTS_TO_PAUSE = ["c", "r", "l", "d", "t"];
const START_OF_DAY_HOUR = 8;
const LATER_TODAY_CUTOFF_HOUR = 17;
const REMINDER_TYPES = {
@ -21,6 +33,28 @@ const REMINDER_TYPES = {
LATER_THIS_WEEK: "later_this_week"
};
const BOOKMARK_BINDINGS = {
enter: { handler: "saveAndClose" },
"l t": { handler: "selectReminderType", args: [REMINDER_TYPES.LATER_TODAY] },
"l w": {
handler: "selectReminderType",
args: [REMINDER_TYPES.LATER_THIS_WEEK]
},
"n b d": {
handler: "selectReminderType",
args: [REMINDER_TYPES.NEXT_BUSINESS_DAY]
},
"n d": { handler: "selectReminderType", args: [REMINDER_TYPES.TOMORROW] },
"n w": { handler: "selectReminderType", args: [REMINDER_TYPES.NEXT_WEEK] },
"n b w": {
handler: "selectReminderType",
args: [REMINDER_TYPES.START_OF_NEXT_BUSINESS_WEEK]
},
"n m": { handler: "selectReminderType", args: [REMINDER_TYPES.NEXT_MONTH] },
"c r": { handler: "selectReminderType", args: [REMINDER_TYPES.CUSTOM] },
"n r": { handler: "selectReminderType", args: [REMINDER_TYPES.NONE] }
};
export default Controller.extend(ModalFunctionality, {
loading: false,
errorMessage: null,
@ -33,6 +67,7 @@ export default Controller.extend(ModalFunctionality, {
customReminderTime: null,
lastCustomReminderDate: null,
lastCustomReminderTime: null,
mouseTrap: null,
userTimezone: null,
onShow() {
@ -49,7 +84,12 @@ export default Controller.extend(ModalFunctionality, {
userTimezone: this.currentUser.resolvedTimezone()
});
this.bindKeyboardShortcuts();
this.loadLastUsedCustomReminderDatetime();
// make sure the input is cleared, otherwise the keyboard shortcut to toggle
// bookmark for post ends up in the input
next(() => this.set("name", null));
},
loadLastUsedCustomReminderDatetime() {
@ -71,9 +111,29 @@ export default Controller.extend(ModalFunctionality, {
}
},
bindKeyboardShortcuts() {
KeyboardShortcuts.pause(GLOBAL_SHORTCUTS_TO_PAUSE);
KeyboardShortcuts.addBindings(BOOKMARK_BINDINGS, binding => {
if (binding.args) {
return this.send(binding.handler, ...binding.args);
}
this.send(binding.handler);
});
},
unbindKeyboardShortcuts() {
KeyboardShortcuts.unbind(BOOKMARK_BINDINGS, this.mouseTrap);
},
restoreGlobalShortcuts() {
KeyboardShortcuts.unpause(...GLOBAL_SHORTCUTS_TO_PAUSE);
},
// we always want to save the bookmark unless the user specifically
// clicks the save or cancel button to mimic browser behaviour
onClose() {
this.unbindKeyboardShortcuts();
this.restoreGlobalShortcuts();
if (!this.closeWithoutSaving && !this.isSavingBookmarkManually) {
this.saveBookmark().catch(e => this.handleSaveError(e));
}
@ -102,10 +162,7 @@ export default Controller.extend(ModalFunctionality, {
return REMINDER_TYPES;
},
@discourseComputed()
showLastCustom() {
return this.lastCustomReminderTime && this.lastCustomReminderDate;
},
showLastCustom: and("lastCustomReminderTime", "lastCustomReminderDate"),
@discourseComputed()
showLaterToday() {
@ -299,10 +356,16 @@ export default Controller.extend(ModalFunctionality, {
actions: {
saveAndClose() {
if (this.saving) {
return;
}
this.saving = true;
this.isSavingBookmarkManually = true;
this.saveBookmark()
.then(() => this.send("closeModal"))
.catch(e => this.handleSaveError(e));
.catch(e => this.handleSaveError(e))
.finally(() => (this.saving = false));
},
closeWithoutSavingBookmark() {
@ -311,6 +374,9 @@ export default Controller.extend(ModalFunctionality, {
},
selectReminderType(type) {
if (type === REMINDER_TYPES.LATER_TODAY && !this.showLaterToday) {
return;
}
this.set("selectedReminderType", type);
}
}

View File

@ -1,5 +1,6 @@
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { setting } from "discourse/lib/computed";
const KEY = "keyboard_shortcuts_help";
@ -51,6 +52,8 @@ export default Controller.extend(ModalFunctionality, {
this.set("modal.modalClass", "keyboard-shortcuts-modal");
},
showBookmarkShortcuts: setting("enable_bookmarks_with_reminders"),
shortcuts: {
jump_to: {
home: buildShortcut("jump_to.home", { keys1: ["g", "h"] }),
@ -125,6 +128,41 @@ export default Controller.extend(ModalFunctionality, {
keysDelimiter: PLUS
})
},
bookmarks: {
enter: buildShortcut("bookmarks.enter", { keys1: [ENTER] }),
later_today: buildShortcut("bookmarks.later_today", {
keys1: ["l", "t"],
shortcutsDelimiter: "space"
}),
later_this_week: buildShortcut("bookmarks.later_this_week", {
keys1: ["l", "w"],
shortcutsDelimiter: "space"
}),
tomorrow: buildShortcut("bookmarks.tomorrow", {
keys1: ["n", "d"],
shortcutsDelimiter: "space"
}),
next_week: buildShortcut("bookmarks.next_week", {
keys1: ["n", "w"],
shortcutsDelimiter: "space"
}),
next_business_week: buildShortcut("bookmarks.next_business_week", {
keys1: ["n", "b", "w"],
shortcutsDelimiter: "space"
}),
next_business_day: buildShortcut("bookmarks.next_business_day", {
keys1: ["n", "b", "d"],
shortcutsDelimiter: "space"
}),
custom: buildShortcut("bookmarks.custom", {
keys1: ["c", "r"],
shortcutsDelimiter: "space"
}),
none: buildShortcut("bookmarks.none", {
keys1: ["n", "r"],
shortcutsDelimiter: "space"
})
},
actions: {
bookmark_topic: buildShortcut("actions.bookmark_topic", { keys1: ["f"] }),
reply_as_new_topic: buildShortcut("actions.reply_as_new_topic", {

View File

@ -662,6 +662,9 @@ export default Controller.extend(bufferedProperty("model"), {
if (!this.currentUser) {
return bootbox.alert(I18n.t("bookmarks.not_bookmarked"));
} else if (post) {
if (this.siteSettings.enable_bookmarks_with_reminders) {
return post.toggleBookmarkWithReminder();
}
return post.toggleBookmark().catch(popupAjaxError);
} else {
return this.model.toggleBookmark().then(changedIds => {

View File

@ -5,6 +5,7 @@ export default {
name: "keyboard-shortcuts",
initialize(container) {
KeyboardShortcuts.bindEvents(Mousetrap, container);
KeyboardShortcuts.init(Mousetrap, container);
KeyboardShortcuts.bindEvents();
}
};

View File

@ -6,7 +6,7 @@ import { ajax } from "discourse/lib/ajax";
import { throttle } from "@ember/runloop";
import { INPUT_DELAY } from "discourse-common/config/environment";
export let bindings = {
export let DEFAULT_BINDINGS = {
"!": { postAction: "showFlags" },
"#": { handler: "goToPost", anonymous: true },
"/": { handler: "toggleSearch", anonymous: true },
@ -84,7 +84,7 @@ export let bindings = {
const animationDuration = 100;
export default {
bindEvents(keyTrapper, container) {
init(keyTrapper, container) {
this.keyTrapper = keyTrapper;
this.container = container;
this._stopCallback();
@ -96,11 +96,18 @@ export default {
// Disable the shortcut if private messages are disabled
if (!siteSettings.enable_personal_messages) {
delete bindings["g m"];
delete DEFAULT_BINDINGS["g m"];
}
},
Object.keys(bindings).forEach(key => {
const binding = bindings[key];
bindEvents() {
Object.keys(DEFAULT_BINDINGS).forEach(key => {
this.bindKey(key);
});
},
bindKey(key) {
const binding = DEFAULT_BINDINGS[key];
if (!binding.anonymous && !this.currentUser) {
return;
}
@ -119,7 +126,40 @@ export default {
} else if (binding.click) {
this._bindToClick(binding.click, key);
}
},
// for cases when you want to disable global keyboard shortcuts
// so that you can override them (e.g. inside a modal)
pause(combinations) {
combinations.forEach(combo => this.keyTrapper.unbind(combo));
},
// restore global shortcuts that you have paused
unpause(...combinations) {
combinations.forEach(combo => this.bindKey(combo));
},
// add bindings to the key trapper, if none is specified then
// the shortcuts will be bound globally.
addBindings(newBindings, callback) {
Object.keys(newBindings).forEach(key => {
let binding = newBindings[key];
this.keyTrapper.bind(key, event => {
// usually the caller that is adding the binding
// will want to decide what to do with it when the
// event is fired
callback(binding, event);
event.stopPropagation();
});
});
},
// unbinds all the shortcuts in a key binding object e.g.
// {
// 'c': createTopic
// }
unbind(bindings) {
this.pause(Object.keys(bindings));
},
toggleBookmark() {

View File

@ -9,7 +9,7 @@
{{/if}}
<div class="control-group">
{{input value=name name="name" class="bookmark-name" enter=(action "saveAndClose") placeholder=(i18n "post.bookmarks.name_placeholder")}}
{{input value=name name="name" class="bookmark-name" placeholder=(i18n "post.bookmarks.name_placeholder")}}
</div>
{{#if showBookmarkReminderControls}}

View File

@ -55,6 +55,23 @@
<li>{{html-safe shortcuts.actions.quote_post}}</li>
</ul>
</section>
{{#if showBookmarkShortcuts}}
<section class="keyboard-shortcuts-bookmark-section">
<h4>{{i18n "keyboard_shortcuts_help.bookmarks.title"}}</h4>
<ul>
<li>{{html-safe shortcuts.bookmarks.enter}}</li>
<li>{{html-safe shortcuts.bookmarks.later_today}}</li>
<li>{{html-safe shortcuts.bookmarks.later_this_week}}</li>
<li>{{html-safe shortcuts.bookmarks.tomorrow}}</li>
<li>{{html-safe shortcuts.bookmarks.next_week}}</li>
<li>{{html-safe shortcuts.bookmarks.next_month}}</li>
<li>{{html-safe shortcuts.bookmarks.next_business_week}}</li>
<li>{{html-safe shortcuts.bookmarks.next_business_day}}</li>
<li>{{html-safe shortcuts.bookmarks.custom}}</li>
<li>{{html-safe shortcuts.bookmarks.none}}</li>
</ul>
</section>
{{/if}}
</div>
<div class="column">
<section>

View File

@ -3075,6 +3075,18 @@ en:
title: "Composing"
return: "%{shortcut} Return to composer"
fullscreen: "%{shortcut} Fullscreen composer"
bookmarks:
title: "Bookmarking"
enter: "%{shortcut} Save and close"
later_today: "%{shortcut} Later today"
later_this_week: "%{shortcut} Later this week"
tomorrow: "%{shortcut} Tomorrow"
next_week: "%{shortcut} Next week"
next_month: "%{shortcut} Next month"
next_business_week: "%{shortcut} Start of next week"
next_business_day: "%{shortcut} Next business day"
custom: "%{shortcut} Custom date and time"
none: "%{shortcut} No reminder"
actions:
title: "Actions"
bookmark_topic: "%{shortcut} Toggle bookmark topic"