From 8a6aa0bae788549fbac496303fdc23c2bafd7b66 Mon Sep 17 00:00:00 2001 From: Prozilla Date: Mon, 31 Jul 2023 20:51:12 +0200 Subject: [PATCH] Added keyboard shortcuts for header menu --- .../applications/.common/HeaderMenu.jsx | 76 ++++++++++------ .../applications/text-editor/TextEditor.jsx | 88 +++++++++++++------ .../text-editor/TextEditor.module.css | 7 ++ src/components/utils/DropdownButton.jsx | 10 ++- .../utils/DropdownButton.module.css | 15 +++- src/features/utils/string.js | 31 +++++++ src/hooks/utils/keyboard.js | 8 +- src/styles/global.css | 6 +- 8 files changed, 176 insertions(+), 65 deletions(-) create mode 100644 src/features/utils/string.js diff --git a/src/components/applications/.common/HeaderMenu.jsx b/src/components/applications/.common/HeaderMenu.jsx index ba9513a..d78c7e4 100644 --- a/src/components/applications/.common/HeaderMenu.jsx +++ b/src/components/applications/.common/HeaderMenu.jsx @@ -1,36 +1,62 @@ +import { useState } from "react"; +import { removeFromArray } from "../../../features/utils/array.js"; +import { useKeyboardListener } from "../../../hooks/utils/keyboard.js"; import { DropdownButton } from "../../utils/DropdownButton.jsx"; import styles from "./HeaderMenu.module.css"; /** * @param {Object} props - * @param {Function} props.onNew - * @param {Function} props.onOpen - * @param {Function} props.onSave - * @param {Function} props.onSaveAs - * @param {Function} props.onExit + * @param {Object>} props.options + * @param {Object>>} props.shortcuts */ -export function HeaderMenu({ onNew, onOpen, onSave, onSaveAs, onExit }) { +export function HeaderMenu({ options, shortcuts }) { + const [activeKeys, setActiveKeys] = useState([]); + + const checkShortcuts = (event, allowExecution = true) => { + for (const [category, group] of Object.entries(shortcuts)) { + for (const [name, shortcut] of Object.entries(group)) { + let active = true; + + shortcut.forEach((key) => { + if (!activeKeys.includes(key) && event.key != key) + return active = false; + }); + + if (active) { + event.preventDefault(); + if (shortcut.includes(event.key) && allowExecution) { + options?.[category]?.[name]?.(); + } + } + } + } + } + + const onKeyDown = (event) => { + const isRepeated = activeKeys.includes(event.key); + checkShortcuts(event, isRepeated); + + if (!isRepeated) + setActiveKeys(activeKeys.concat([event.key])); + }; + + const onKeyUp = (event) => { + checkShortcuts(event); + + if (activeKeys.includes(event.key)) { + const keys = [...activeKeys]; + removeFromArray(event.key, keys); + setActiveKeys(keys); + } + }; + + useKeyboardListener({ onKeyDown, onKeyUp }); + return (
- { - onNew?.(); - }, - "Open": () => { - onOpen?.(); - }, - "Save": () => { - onSave?.(); - }, - "Save as": () => { - onSaveAs?.(); - }, - "Exit": () => { - onExit?.(); - }, - }}/> - - + {Object.entries(options).map(([key, value]) => + + )}
); } \ No newline at end of file diff --git a/src/components/applications/text-editor/TextEditor.jsx b/src/components/applications/text-editor/TextEditor.jsx index 0c41c28..3f5874f 100644 --- a/src/components/applications/text-editor/TextEditor.jsx +++ b/src/components/applications/text-editor/TextEditor.jsx @@ -5,6 +5,9 @@ import styles from "./TextEditor.module.css"; import { HeaderMenu } from "../.common/HeaderMenu.jsx"; import Markdown from "markdown-to-jsx"; +const defaultZoom = 16; +const zoomSpeed = 4; + /** * @param {Object} props * @param {VirtualFile} props.file @@ -13,7 +16,8 @@ export function TextEditor({ file, setTitle, close, mode }) { const [currentFile, setCurrentFile] = useState(file); const [currentMode, setCurrentMode] = useState(mode); const [content, setContent] = useState(file?.content ?? ""); - const [unsavedChanges, setUnsavedChanges] = useState(false); + const [unsavedChanges, setUnsavedChanges] = useState(file == null); + const [zoom, setZoom] = useState(defaultZoom); useEffect(() => { (async () => { @@ -50,19 +54,20 @@ export function TextEditor({ file, setTitle, close, mode }) { const newText = () => { setCurrentFile(null); setCurrentMode("edit"); - } + setUnsavedChanges(true); + }; const saveTextAs = () => { - setUnsavedChanges(false); - } + onChange({ target: { value: content } }); + }; const saveText = () => { if (currentFile == null) return saveTextAs(); currentFile.content = content; - setUnsavedChanges(false); - } + onChange({ target: { value: content } }); + }; const onChange = (event) => { const value = event.target.value; @@ -70,44 +75,71 @@ export function TextEditor({ file, setTitle, close, mode }) { if (currentFile != null) { setUnsavedChanges(currentFile.content !== value); } else { - setUnsavedChanges(value !== ""); + setUnsavedChanges(true); } return setContent(value); }; - const onKeyDown = (event) => { - if (event.key === "s" && event.ctrlKey) { - event.preventDefault(); - saveText(); - } - }; - return ( -
+
{ close(); }} + options={{ + "File": { + "New": newText, + "Save": saveText, + // "Save As": saveTextAs, + "Exit": () => { + close(); + }, + }, + "View": { + [currentMode === "view" ? "Edit mode" : "Preview mode"]: () => { + setCurrentMode(currentMode === "view" ? "edit" : "view"); + }, + "Zoom In": () => { + setZoom(zoom + zoomSpeed); + }, + "Zoom Out": () => { + setZoom(zoom - zoomSpeed); + }, + "Reset Zoom": () => { + setZoom(defaultZoom); + } + } + }} + shortcuts={{ + "File": { + "New": ["Control", "e"], + "Save": ["Control", "s"], + "Exit": ["Control", "x"], + }, + "View": { + "Zoom In": ["Control", "+"], + "Zoom Out": ["Control", "-"], + "Reset Zoom": ["Control", "0"], + } + }} /> {currentMode === "view" ?
- - {content} - + } }}> + {content} + + :

{content}

+ }
: