Skip to content

Commit 4de8712

Browse files
authored
fix: keyboard shortcuts (#5250)
1 parent 3571188 commit 4de8712

File tree

3 files changed

+114
-3
lines changed

3 files changed

+114
-3
lines changed

web/src/components/MemoEditor/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,12 @@ const MemoEditor = observer((props: Props) => {
197197

198198
if (isMetaKey) {
199199
if (event.key === "Enter") {
200+
event.preventDefault();
201+
handleSaveBtnClick();
202+
return;
203+
}
204+
if (event.key.toLowerCase() === "s") {
205+
event.preventDefault();
200206
handleSaveBtnClick();
201207
return;
202208
}

web/src/components/MemoView.tsx

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { BookmarkIcon, EyeOffIcon, MessageCircleMoreIcon } from "lucide-react";
22
import { observer } from "mobx-react-lite";
3-
import { memo, useCallback, useState } from "react";
3+
import { memo, useCallback, useEffect, useRef, useState } from "react";
4+
import toast from "react-hot-toast";
45
import { Link, useLocation } from "react-router-dom";
56
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
67
import useAsyncEffect from "@/hooks/useAsyncEffect";
@@ -50,6 +51,8 @@ const MemoView: React.FC<Props> = observer((props: Props) => {
5051
urls: [],
5152
index: 0,
5253
});
54+
const [shortcutActive, setShortcutActive] = useState(false);
55+
const cardRef = useRef<HTMLDivElement>(null);
5356
const instanceMemoRelatedSetting = instanceStore.state.memoRelatedSetting;
5457
const referencedMemos = memo.relations.filter((relation) => relation.type === MemoRelation_Type.REFERENCE);
5558
const commentAmount = memo.relations.filter(
@@ -124,6 +127,89 @@ const MemoView: React.FC<Props> = observer((props: Props) => {
124127
}
125128
};
126129

130+
const archiveMemo = useCallback(async () => {
131+
if (isArchived) {
132+
return;
133+
}
134+
135+
try {
136+
await memoStore.updateMemo(
137+
{
138+
name: memo.name,
139+
state: State.ARCHIVED,
140+
},
141+
["state"],
142+
);
143+
toast.success(t("message.archived-successfully"));
144+
userStore.setStatsStateId();
145+
} catch (error: any) {
146+
console.error(error);
147+
toast.error(error?.details);
148+
}
149+
}, [isArchived, memo.name, t, memoStore, userStore]);
150+
151+
useEffect(() => {
152+
if (!shortcutActive || readonly || showEditor || !cardRef.current) {
153+
return;
154+
}
155+
156+
const cardEl = cardRef.current;
157+
const isTextInputElement = (element: HTMLElement | null) => {
158+
if (!element) {
159+
return false;
160+
}
161+
if (element.isContentEditable) {
162+
return true;
163+
}
164+
if (element instanceof HTMLTextAreaElement) {
165+
return true;
166+
}
167+
168+
if (element instanceof HTMLInputElement) {
169+
const textTypes = ["text", "search", "email", "password", "url", "tel", "number"];
170+
return textTypes.includes(element.type || "text");
171+
}
172+
173+
return false;
174+
};
175+
176+
const handleKeyDown = (event: KeyboardEvent) => {
177+
const target = event.target as HTMLElement | null;
178+
if (!cardEl.contains(target) || isTextInputElement(target)) {
179+
return;
180+
}
181+
182+
if (event.metaKey || event.ctrlKey || event.altKey) {
183+
return;
184+
}
185+
186+
const key = event.key.toLowerCase();
187+
if (key === "e") {
188+
event.preventDefault();
189+
setShowEditor(true);
190+
} else if (key === "a" && !isArchived) {
191+
event.preventDefault();
192+
archiveMemo();
193+
}
194+
};
195+
196+
cardEl.addEventListener("keydown", handleKeyDown);
197+
return () => cardEl.removeEventListener("keydown", handleKeyDown);
198+
}, [shortcutActive, readonly, showEditor, isArchived, archiveMemo]);
199+
200+
useEffect(() => {
201+
if (showEditor || readonly) {
202+
setShortcutActive(false);
203+
}
204+
}, [showEditor, readonly]);
205+
206+
const handleShortcutActivation = (active: boolean) => {
207+
if (readonly) {
208+
return;
209+
}
210+
setShortcutActive(active);
211+
};
212+
127213
const displayTime = isArchived ? (
128214
memo.displayTime?.toLocaleString()
129215
) : (
@@ -142,9 +228,14 @@ const MemoView: React.FC<Props> = observer((props: Props) => {
142228
) : (
143229
<div
144230
className={cn(
145-
"relative group flex flex-col justify-start items-start bg-card w-full px-4 py-3 mb-2 gap-2 text-card-foreground rounded-lg border border-border transition-colors",
231+
"relative group flex flex-col justify-start items-start bg-card w-full px-4 py-3 mb-2 gap-2 text-card-foreground rounded-lg border border-border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
232+
shortcutActive && !showEditor && "border-ring ring-2 ring-ring bg-accent/10",
146233
className,
147234
)}
235+
ref={cardRef}
236+
tabIndex={readonly ? -1 : 0}
237+
onFocus={() => handleShortcutActivation(true)}
238+
onBlur={() => handleShortcutActivation(false)}
148239
>
149240
<div className="w-full flex flex-row justify-between items-center gap-2">
150241
<div className="w-auto max-w-[calc(100%-8rem)] grow flex flex-row justify-start items-center">

web/src/components/SearchBar.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { SearchIcon } from "lucide-react";
22
import { observer } from "mobx-react-lite";
3-
import { useState } from "react";
3+
import { useEffect, useRef, useState } from "react";
44
import { cn } from "@/lib/utils";
55
import { memoFilterStore } from "@/store";
66
import { useTranslate } from "@/utils/i18n";
@@ -9,6 +9,19 @@ import MemoDisplaySettingMenu from "./MemoDisplaySettingMenu";
99
const SearchBar = observer(() => {
1010
const t = useTranslate();
1111
const [queryText, setQueryText] = useState("");
12+
const inputRef = useRef<HTMLInputElement>(null);
13+
14+
useEffect(() => {
15+
const handleGlobalShortcut = (event: KeyboardEvent) => {
16+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
17+
event.preventDefault();
18+
inputRef.current?.focus();
19+
}
20+
};
21+
22+
window.addEventListener("keydown", handleGlobalShortcut);
23+
return () => window.removeEventListener("keydown", handleGlobalShortcut);
24+
}, []);
1225

1326
const onTextChange = (event: React.FormEvent<HTMLInputElement>) => {
1427
setQueryText(event.currentTarget.value);
@@ -40,6 +53,7 @@ const SearchBar = observer(() => {
4053
value={queryText}
4154
onChange={onTextChange}
4255
onKeyDown={onKeyDown}
56+
ref={inputRef}
4357
/>
4458
<MemoDisplaySettingMenu className="absolute right-2 top-2 text-sidebar-foreground" />
4559
</div>

0 commit comments

Comments
 (0)