diff --git a/web/src/components/MemoEditor/index.tsx b/web/src/components/MemoEditor/index.tsx index e9868ca75..9548d99f8 100644 --- a/web/src/components/MemoEditor/index.tsx +++ b/web/src/components/MemoEditor/index.tsx @@ -151,6 +151,12 @@ const MemoEditor = observer((props: Props) => { const isMetaKey = event.ctrlKey || event.metaKey; if (isMetaKey) { if (event.key === "Enter") { + event.preventDefault(); + handleSaveBtnClick(); + return; + } + if (event.key.toLowerCase() === "s") { + event.preventDefault(); handleSaveBtnClick(); return; } diff --git a/web/src/components/MemoView.tsx b/web/src/components/MemoView.tsx index 3040084b8..a07e91cb8 100644 --- a/web/src/components/MemoView.tsx +++ b/web/src/components/MemoView.tsx @@ -1,6 +1,7 @@ import { BookmarkIcon, EyeOffIcon, MessageCircleMoreIcon } from "lucide-react"; import { observer } from "mobx-react-lite"; -import { memo, useCallback, useState } from "react"; +import { memo, useCallback, useEffect, useRef, useState } from "react"; +import toast from "react-hot-toast"; import { Link, useLocation } from "react-router-dom"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import useAsyncEffect from "@/hooks/useAsyncEffect"; @@ -50,6 +51,8 @@ const MemoView: React.FC = observer((props: Props) => { urls: [], index: 0, }); + const [shortcutActive, setShortcutActive] = useState(false); + const cardRef = useRef(null); const instanceMemoRelatedSetting = instanceStore.state.memoRelatedSetting; const referencedMemos = memo.relations.filter((relation) => relation.type === MemoRelation_Type.REFERENCE); const commentAmount = memo.relations.filter( @@ -124,6 +127,89 @@ const MemoView: React.FC = observer((props: Props) => { } }; + const archiveMemo = useCallback(async () => { + if (isArchived) { + return; + } + + try { + await memoStore.updateMemo( + { + name: memo.name, + state: State.ARCHIVED, + }, + ["state"], + ); + toast.success(t("message.archived-successfully")); + userStore.setStatsStateId(); + } catch (error: any) { + console.error(error); + toast.error(error?.details); + } + }, [isArchived, memo.name, t]); + + useEffect(() => { + if (!shortcutActive || readonly || showEditor || !cardRef.current) { + return; + } + + const cardEl = cardRef.current; + const isTextInputElement = (element: HTMLElement | null) => { + if (!element) { + return false; + } + if (element.isContentEditable) { + return true; + } + if (element instanceof HTMLTextAreaElement) { + return true; + } + + if (element instanceof HTMLInputElement) { + const textTypes = ["text", "search", "email", "password", "url", "tel", "number"]; + return textTypes.includes(element.type || "text"); + } + + return false; + }; + + const handleKeyDown = (event: KeyboardEvent) => { + const target = event.target as HTMLElement | null; + if (!cardEl.contains(target) || isTextInputElement(target)) { + return; + } + + if (event.metaKey || event.ctrlKey || event.altKey) { + return; + } + + const key = event.key.toLowerCase(); + if (key === "e") { + event.preventDefault(); + setShowEditor(true); + } else if (key === "a" && !isArchived) { + event.preventDefault(); + archiveMemo(); + } + }; + + cardEl.addEventListener("keydown", handleKeyDown); + return () => cardEl.removeEventListener("keydown", handleKeyDown); + }, [shortcutActive, readonly, showEditor, isArchived, archiveMemo]); + + useEffect(() => { + if (showEditor || readonly) { + setShortcutActive(false); + } + }, [showEditor, readonly]); + + const handleShortcutActivation = (active: boolean) => { + if (readonly) { + return; + } + setShortcutActive(active); + }; + const displayTime = isArchived ? ( memo.displayTime?.toLocaleString() ) : ( @@ -142,9 +228,13 @@ const MemoView: React.FC = observer((props: Props) => { ) : (
handleShortcutActivation(true)} + onBlur={() => handleShortcutActivation(false)} >
diff --git a/web/src/components/SearchBar.tsx b/web/src/components/SearchBar.tsx index 8c8349f35..5ff7ac4ad 100644 --- a/web/src/components/SearchBar.tsx +++ b/web/src/components/SearchBar.tsx @@ -1,6 +1,6 @@ import { SearchIcon } from "lucide-react"; import { observer } from "mobx-react-lite"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { cn } from "@/lib/utils"; import { memoFilterStore } from "@/store"; import { useTranslate } from "@/utils/i18n"; @@ -9,6 +9,19 @@ import MemoDisplaySettingMenu from "./MemoDisplaySettingMenu"; const SearchBar = observer(() => { const t = useTranslate(); const [queryText, setQueryText] = useState(""); + const inputRef = useRef(null); + + useEffect(() => { + const handleGlobalShortcut = (event: KeyboardEvent) => { + if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { + event.preventDefault(); + inputRef.current?.focus(); + } + }; + + window.addEventListener("keydown", handleGlobalShortcut); + return () => window.removeEventListener("keydown", handleGlobalShortcut); + }, []); const onTextChange = (event: React.FormEvent) => { setQueryText(event.currentTarget.value); @@ -40,6 +53,7 @@ const SearchBar = observer(() => { value={queryText} onChange={onTextChange} onKeyDown={onKeyDown} + ref={inputRef} />