diff --git a/app/assets/javascripts/discourse/controllers/bookmark.js b/app/assets/javascripts/discourse/controllers/bookmark.js index c14329d253b..140b6f85974 100644 --- a/app/assets/javascripts/discourse/controllers/bookmark.js +++ b/app/assets/javascripts/discourse/controllers/bookmark.js @@ -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); } } diff --git a/app/assets/javascripts/discourse/controllers/keyboard-shortcuts-help.js b/app/assets/javascripts/discourse/controllers/keyboard-shortcuts-help.js index 62feb5b4048..0bc9fe39718 100644 --- a/app/assets/javascripts/discourse/controllers/keyboard-shortcuts-help.js +++ b/app/assets/javascripts/discourse/controllers/keyboard-shortcuts-help.js @@ -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", { diff --git a/app/assets/javascripts/discourse/controllers/topic.js b/app/assets/javascripts/discourse/controllers/topic.js index 80e6124f6db..01a84f73529 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js +++ b/app/assets/javascripts/discourse/controllers/topic.js @@ -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 => { diff --git a/app/assets/javascripts/discourse/initializers/keyboard-shortcuts.js b/app/assets/javascripts/discourse/initializers/keyboard-shortcuts.js index 8801d112d9a..a0de2c5d2ad 100644 --- a/app/assets/javascripts/discourse/initializers/keyboard-shortcuts.js +++ b/app/assets/javascripts/discourse/initializers/keyboard-shortcuts.js @@ -5,6 +5,7 @@ export default { name: "keyboard-shortcuts", initialize(container) { - KeyboardShortcuts.bindEvents(Mousetrap, container); + KeyboardShortcuts.init(Mousetrap, container); + KeyboardShortcuts.bindEvents(); } }; diff --git a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js index 064c2eeb587..c4a962ac688 100644 --- a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js +++ b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js @@ -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"); diff --git a/app/assets/javascripts/discourse/templates/modal/bookmark.hbs b/app/assets/javascripts/discourse/templates/modal/bookmark.hbs index 538b30c74f8..890a7dbdcc8 100644 --- a/app/assets/javascripts/discourse/templates/modal/bookmark.hbs +++ b/app/assets/javascripts/discourse/templates/modal/bookmark.hbs @@ -9,7 +9,7 @@ {{/if}}