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:
parent
d04ba4b3b2
commit
93c38cc175
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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", {
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -5,6 +5,7 @@ export default {
|
|||
name: "keyboard-shortcuts",
|
||||
|
||||
initialize(container) {
|
||||
KeyboardShortcuts.bindEvents(Mousetrap, container);
|
||||
KeyboardShortcuts.init(Mousetrap, container);
|
||||
KeyboardShortcuts.bindEvents();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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,32 +96,72 @@ export default {
|
|||
|
||||
// Disable the shortcut if private messages are disabled
|
||||
if (!siteSettings.enable_personal_messages) {
|
||||
delete bindings["g m"];
|
||||
delete DEFAULT_BINDINGS["g m"];
|
||||
}
|
||||
},
|
||||
|
||||
bindEvents() {
|
||||
Object.keys(DEFAULT_BINDINGS).forEach(key => {
|
||||
this.bindKey(key);
|
||||
});
|
||||
},
|
||||
|
||||
bindKey(key) {
|
||||
const binding = DEFAULT_BINDINGS[key];
|
||||
if (!binding.anonymous && !this.currentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(bindings).forEach(key => {
|
||||
const binding = bindings[key];
|
||||
if (!binding.anonymous && !this.currentUser) {
|
||||
return;
|
||||
if (binding.path) {
|
||||
this._bindToPath(binding.path, key);
|
||||
} else if (binding.handler) {
|
||||
if (binding.global) {
|
||||
// global shortcuts will trigger even while focusing on input/textarea
|
||||
this._globalBindToFunction(binding.handler, key);
|
||||
} else {
|
||||
this._bindToFunction(binding.handler, key);
|
||||
}
|
||||
} else if (binding.postAction) {
|
||||
this._bindToSelectedPost(binding.postAction, key);
|
||||
} else if (binding.click) {
|
||||
this._bindToClick(binding.click, key);
|
||||
}
|
||||
},
|
||||
|
||||
if (binding.path) {
|
||||
this._bindToPath(binding.path, key);
|
||||
} else if (binding.handler) {
|
||||
if (binding.global) {
|
||||
// global shortcuts will trigger even while focusing on input/textarea
|
||||
this._globalBindToFunction(binding.handler, key);
|
||||
} else {
|
||||
this._bindToFunction(binding.handler, key);
|
||||
}
|
||||
} else if (binding.postAction) {
|
||||
this._bindToSelectedPost(binding.postAction, key);
|
||||
} 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() {
|
||||
this.sendToSelectedPost("toggleBookmark");
|
||||
this.sendToTopicListItemView("toggleBookmark");
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue