From 178c0704c8f38084f91b40f67b04a0df8f904c78 Mon Sep 17 00:00:00 2001
From: Hilary Liu <2788370451@qq.com>
Date: Fri, 1 Sep 2023 10:30:12 +0800
Subject: [PATCH] feat: add support for saving post by shortcut keys (#4510)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

#### What type of PR is this?
/area editor
/area console


#### What this PR does / why we need it:
1. mac: 在编辑器中添加command+s保存文章
2. windows: 在编辑器中添加ctrl+s保存文章
#### Which issue(s) this PR fixes:

Fixes https://github.com/halo-dev/halo/issues/4340

#### Does this PR introduce a user-facing change?

```release-note
Console 端支持通过快捷键保存文章
```
---
 .../src/components/button/SubmitButton.vue    |  3 +--
 .../src/composables/use-save-keybinding.ts    | 21 +++++++++++++++++++
 console/src/layouts/BasicLayout.vue           |  3 +--
 .../contents/pages/SinglePageEditor.vue       |  3 +++
 .../src/modules/contents/posts/PostEditor.vue |  3 +++
 console/src/utils/device.ts                   |  1 +
 6 files changed, 30 insertions(+), 4 deletions(-)
 create mode 100644 console/src/composables/use-save-keybinding.ts
 create mode 100644 console/src/utils/device.ts

diff --git a/console/src/components/button/SubmitButton.vue b/console/src/components/button/SubmitButton.vue
index dc3d36e9f..597dbd588 100644
--- a/console/src/components/button/SubmitButton.vue
+++ b/console/src/components/button/SubmitButton.vue
@@ -1,4 +1,5 @@
 <script lang="ts" setup>
+import { isMac } from "@/utils/device";
 import { VButton } from "@halo-dev/components";
 import { useMagicKeys } from "@vueuse/core";
 import { computed, useAttrs, watchEffect } from "vue";
@@ -19,8 +20,6 @@ const emit = defineEmits<{
   (event: "submit"): void;
 }>();
 
-const isMac = /macintosh|mac os x/i.test(navigator.userAgent);
-
 const attrs = useAttrs();
 
 const buttonText = computed(() => {
diff --git a/console/src/composables/use-save-keybinding.ts b/console/src/composables/use-save-keybinding.ts
new file mode 100644
index 000000000..cdfb7686c
--- /dev/null
+++ b/console/src/composables/use-save-keybinding.ts
@@ -0,0 +1,21 @@
+import { isMac } from "@/utils/device";
+import { useEventListener } from "@vueuse/core";
+import { useDebounceFn } from "@vueuse/shared";
+import { nextTick } from "vue";
+
+export function useSaveKeybinding(fn: () => void) {
+  const debouncedFn = useDebounceFn(() => {
+    fn();
+  }, 300);
+
+  useEventListener(window, "keydown", (e: KeyboardEvent) => {
+    if (isMac ? e.metaKey : e.ctrlKey) {
+      if (e.key === "s") {
+        e.preventDefault();
+        nextTick(() => {
+          debouncedFn();
+        });
+      }
+    }
+  });
+}
diff --git a/console/src/layouts/BasicLayout.vue b/console/src/layouts/BasicLayout.vue
index 486c20d13..4aa16ffb0 100644
--- a/console/src/layouts/BasicLayout.vue
+++ b/console/src/layouts/BasicLayout.vue
@@ -34,6 +34,7 @@ import {
   useOverlayScrollbars,
   type UseOverlayScrollbarsParams,
 } from "overlayscrollbars-vue";
+import { isMac } from "@/utils/device";
 
 const route = useRoute();
 const router = useRouter();
@@ -70,8 +71,6 @@ const handleLogout = () => {
 // Global Search
 const globalSearchVisible = ref(false);
 
-const isMac = /macintosh|mac os x/i.test(navigator.userAgent);
-
 const handleGlobalSearchKeybinding = (e: KeyboardEvent) => {
   const { key, ctrlKey, metaKey } = e;
   if (key === "k" && ((ctrlKey && !isMac) || metaKey)) {
diff --git a/console/src/modules/contents/pages/SinglePageEditor.vue b/console/src/modules/contents/pages/SinglePageEditor.vue
index 77e03cc11..7d733f972 100644
--- a/console/src/modules/contents/pages/SinglePageEditor.vue
+++ b/console/src/modules/contents/pages/SinglePageEditor.vue
@@ -38,6 +38,7 @@ import { contentAnnotations } from "@/constants/annotations";
 import { usePageUpdateMutate } from "./composables/use-page-update-mutate";
 import { useAutoSaveContent } from "@/composables/use-auto-save-content";
 import { useContentSnapshot } from "@/composables/use-content-snapshot";
+import { useSaveKeybinding } from "@/composables/use-save-keybinding";
 
 const router = useRouter();
 const { t } = useI18n();
@@ -365,6 +366,8 @@ const handlePreview = async () => {
   previewModal.value = true;
   previewPending.value = false;
 };
+
+useSaveKeybinding(handleSave);
 </script>
 
 <template>
diff --git a/console/src/modules/contents/posts/PostEditor.vue b/console/src/modules/contents/posts/PostEditor.vue
index 5f0824c3f..e56c94512 100644
--- a/console/src/modules/contents/posts/PostEditor.vue
+++ b/console/src/modules/contents/posts/PostEditor.vue
@@ -38,6 +38,7 @@ import { usePostUpdateMutate } from "./composables/use-post-update-mutate";
 import { contentAnnotations } from "@/constants/annotations";
 import { useAutoSaveContent } from "@/composables/use-auto-save-content";
 import { useContentSnapshot } from "@/composables/use-content-snapshot";
+import { useSaveKeybinding } from "@/composables/use-save-keybinding";
 
 const router = useRouter();
 const { t } = useI18n();
@@ -381,6 +382,8 @@ const handlePreview = async () => {
   previewModal.value = true;
   previewPending.value = false;
 };
+
+useSaveKeybinding(handleSave);
 </script>
 
 <template>
diff --git a/console/src/utils/device.ts b/console/src/utils/device.ts
new file mode 100644
index 000000000..57a34be8b
--- /dev/null
+++ b/console/src/utils/device.ts
@@ -0,0 +1 @@
+export const isMac = /macintosh|mac os x/i.test(navigator.userAgent);