FEATURE: Bookmark keyboard shortcuts (#9318)

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" } };
diff --git a/app/assets/javascripts/discourse/controllers/bookmark.js b/app/assets/javascripts/discourse/controllers/bookmark.js
index c14329d..140b6f8 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 62feb5b..0bc9fe3 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 80e6124..01a84f7 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();
+        }

[... diff too long, it was truncated ...]

GitHub sha: 93c38cc1

This commit appears in #9318 which was merged by martin.