chore: streamline MemoEditor components and remove unused code

This commit is contained in:
Johnny 2025-11-30 12:30:00 +08:00
parent 26cb357685
commit 7aa8262ef2
21 changed files with 147 additions and 488 deletions

View File

@ -1,7 +1,7 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import OverflowTip from "@/components/kit/OverflowTip"; import OverflowTip from "@/components/kit/OverflowTip";
import { Command } from "../types/command"; import type { EditorRefActions } from ".";
import { EditorRefActions } from "."; import type { Command } from "./commands";
import { SuggestionsPopup } from "./SuggestionsPopup"; import { SuggestionsPopup } from "./SuggestionsPopup";
import { useSuggestions } from "./useSuggestions"; import { useSuggestions } from "./useSuggestions";

View File

@ -1,4 +1,11 @@
import { Command } from "@/components/MemoEditor/types/command"; /**
* Command type for slash commands in the editor
*/
export interface Command {
name: string;
run: () => string;
cursorOffset?: number;
}
export const editorCommands: Command[] = [ export const editorCommands: Command[] = [
{ {

View File

@ -1,7 +1,6 @@
import { forwardRef, ReactNode, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react"; import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { EDITOR_HEIGHT } from "../constants"; import { EDITOR_HEIGHT } from "../constants";
import { Command } from "../types/command";
import CommandSuggestions from "./CommandSuggestions"; import CommandSuggestions from "./CommandSuggestions";
import { editorCommands } from "./commands"; import { editorCommands } from "./commands";
import TagSuggestions from "./TagSuggestions"; import TagSuggestions from "./TagSuggestions";
@ -27,8 +26,6 @@ interface Props {
className: string; className: string;
initialContent: string; initialContent: string;
placeholder: string; placeholder: string;
tools?: ReactNode;
commands?: Command[];
onContentChange: (content: string) => void; onContentChange: (content: string) => void;
onPaste: (event: React.ClipboardEvent) => void; onPaste: (event: React.ClipboardEvent) => void;
/** Whether Focus Mode is active - adjusts height constraints for immersive writing */ /** Whether Focus Mode is active - adjusts height constraints for immersive writing */

View File

@ -3,8 +3,6 @@ import type { EditorRefActions } from "./index";
/** /**
* Handles keyboard shortcuts for markdown formatting * Handles keyboard shortcuts for markdown formatting
* Requires Cmd/Ctrl key to be pressed * Requires Cmd/Ctrl key to be pressed
*
* @alias handleEditorKeydownWithMarkdownShortcuts - for backward compatibility
*/ */
export function handleMarkdownShortcuts(event: React.KeyboardEvent, editor: EditorRefActions): void { export function handleMarkdownShortcuts(event: React.KeyboardEvent, editor: EditorRefActions): void {
switch (event.key.toLowerCase()) { switch (event.key.toLowerCase()) {
@ -23,9 +21,6 @@ export function handleMarkdownShortcuts(event: React.KeyboardEvent, editor: Edit
} }
} }
// Backward compatibility alias
export const handleEditorKeydownWithMarkdownShortcuts = handleMarkdownShortcuts;
/** /**
* Inserts a hyperlink for the selected text * Inserts a hyperlink for the selected text
* If selected text is a URL, creates a link with empty text * If selected text is a URL, creates a link with empty text

View File

@ -197,7 +197,6 @@ const InsertMenu = observer((props: Props) => {
filteredMemos={linkMemo.filteredMemos} filteredMemos={linkMemo.filteredMemos}
isFetching={linkMemo.isFetching} isFetching={linkMemo.isFetching}
onSelectMemo={linkMemo.addMemoRelation} onSelectMemo={linkMemo.addMemoRelation}
getHighlightedContent={linkMemo.getHighlightedContent}
/> />
<LocationDialog <LocationDialog

View File

@ -3,6 +3,34 @@ import { Input } from "@/components/ui/input";
import { Memo } from "@/types/proto/api/v1/memo_service"; import { Memo } from "@/types/proto/api/v1/memo_service";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
/**
* Highlights search text within content string
*/
function highlightSearchText(content: string, searchText: string): React.ReactNode {
if (!searchText) return content;
const index = content.toLowerCase().indexOf(searchText.toLowerCase());
if (index === -1) return content;
let before = content.slice(0, index);
if (before.length > 20) {
before = "..." + before.slice(before.length - 20);
}
const highlighted = content.slice(index, index + searchText.length);
let after = content.slice(index + searchText.length);
if (after.length > 20) {
after = after.slice(0, 20) + "...";
}
return (
<>
{before}
<mark className="font-medium">{highlighted}</mark>
{after}
</>
);
}
interface LinkMemoDialogProps { interface LinkMemoDialogProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
@ -11,7 +39,6 @@ interface LinkMemoDialogProps {
filteredMemos: Memo[]; filteredMemos: Memo[];
isFetching: boolean; isFetching: boolean;
onSelectMemo: (memo: Memo) => void; onSelectMemo: (memo: Memo) => void;
getHighlightedContent: (content: string) => React.ReactNode;
} }
export const LinkMemoDialog = ({ export const LinkMemoDialog = ({
@ -22,7 +49,6 @@ export const LinkMemoDialog = ({
filteredMemos, filteredMemos,
isFetching, isFetching,
onSelectMemo, onSelectMemo,
getHighlightedContent,
}: LinkMemoDialogProps) => { }: LinkMemoDialogProps) => {
const t = useTranslate(); const t = useTranslate();
@ -54,7 +80,7 @@ export const LinkMemoDialog = ({
<div className="w-full flex flex-col justify-start items-start"> <div className="w-full flex flex-col justify-start items-start">
<p className="text-xs text-muted-foreground select-none">{memo.displayTime?.toLocaleString()}</p> <p className="text-xs text-muted-foreground select-none">{memo.displayTime?.toLocaleString()}</p>
<p className="mt-0.5 text-sm leading-5 line-clamp-2"> <p className="mt-0.5 text-sm leading-5 line-clamp-2">
{searchText ? getHighlightedContent(memo.content) : memo.snippet} {searchText ? highlightSearchText(memo.content, searchText) : memo.snippet}
</p> </p>
</div> </div>
</div> </div>

View File

@ -1,19 +1,14 @@
// Custom hooks for MemoEditor // Custom hooks for MemoEditor (internal use only)
export { useAbortController } from "./useAbortController"; export { useAbortController } from "./useAbortController";
export { useBlobUrls } from "./useBlobUrls"; export { useBlobUrls } from "./useBlobUrls";
export { useDebounce } from "./useDebounce";
export { useDragAndDrop } from "./useDragAndDrop"; export { useDragAndDrop } from "./useDragAndDrop";
export { useFileUpload } from "./useFileUpload"; export { useFileUpload } from "./useFileUpload";
export { useFocusMode } from "./useFocusMode"; export { useFocusMode } from "./useFocusMode";
export { useLinkMemo } from "./useLinkMemo"; export { useLinkMemo } from "./useLinkMemo";
export { useLocalFileManager } from "./useLocalFileManager"; export { useLocalFileManager } from "./useLocalFileManager";
export { useLocation } from "./useLocation"; export { useLocation } from "./useLocation";
export type { UseMemoEditorHandlersOptions, UseMemoEditorHandlersReturn } from "./useMemoEditorHandlers";
export { useMemoEditorHandlers } from "./useMemoEditorHandlers"; export { useMemoEditorHandlers } from "./useMemoEditorHandlers";
export type { UseMemoEditorInitOptions, UseMemoEditorInitReturn } from "./useMemoEditorInit";
export { useMemoEditorInit } from "./useMemoEditorInit"; export { useMemoEditorInit } from "./useMemoEditorInit";
export type { UseMemoEditorKeyboardOptions } from "./useMemoEditorKeyboard";
export { useMemoEditorKeyboard } from "./useMemoEditorKeyboard"; export { useMemoEditorKeyboard } from "./useMemoEditorKeyboard";
export type { UseMemoEditorStateReturn } from "./useMemoEditorState";
export { useMemoEditorState } from "./useMemoEditorState"; export { useMemoEditorState } from "./useMemoEditorState";
export { useMemoSave } from "./useMemoSave"; export { useMemoSave } from "./useMemoSave";

View File

@ -1,78 +1,23 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
/** /**
* Custom hook for managing AbortController lifecycle * Hook for managing AbortController lifecycle
* Useful for canceling async operations like fetch requests
*
* @returns Object with methods to create and abort requests
*
* @example
* ```tsx
* const { getSignal, abort, abortAndCreate } = useAbortController();
*
* // Create signal for fetch
* const signal = getSignal();
* fetch(url, { signal });
*
* // Cancel on user action
* abort();
*
* // Or cancel previous and create new
* const newSignal = abortAndCreate();
* fetch(newUrl, { signal: newSignal });
* ```
*/ */
export function useAbortController() { export function useAbortController() {
const controllerRef = useRef<AbortController | null>(null); const controllerRef = useRef<AbortController | null>(null);
// Clean up on unmount useEffect(() => () => controllerRef.current?.abort(), []);
useEffect(() => {
return () => {
controllerRef.current?.abort();
};
}, []);
/** const abort = () => {
* Aborts the current request if one exists
*/
const abort = (): void => {
controllerRef.current?.abort(); controllerRef.current?.abort();
controllerRef.current = null; controllerRef.current = null;
}; };
/**
* Creates a new AbortController and returns its signal
* Does not abort previous controller
*/
const create = (): AbortSignal => {
const controller = new AbortController();
controllerRef.current = controller;
return controller.signal;
};
/**
* Aborts current request and creates a new AbortController
* Useful for debounced requests
*/
const abortAndCreate = (): AbortSignal => { const abortAndCreate = (): AbortSignal => {
abort(); abort();
return create(); controllerRef.current = new AbortController();
};
/**
* Gets the signal from the current controller, or creates new one
*/
const getSignal = (): AbortSignal => {
if (!controllerRef.current) {
return create();
}
return controllerRef.current.signal; return controllerRef.current.signal;
}; };
return { return { abort, abortAndCreate };
abort,
create,
abortAndCreate,
getSignal,
};
} }

View File

@ -1,65 +1,31 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
/** /**
* Custom hook for managing blob URLs lifecycle * Hook for managing blob URLs lifecycle with automatic cleanup
* Automatically tracks and cleans up all blob URLs on unmount to prevent memory leaks
*
* @returns Object with methods to create, revoke, and manage blob URLs
*
* @example
* ```tsx
* const { createBlobUrl, revokeBlobUrl, revokeAll } = useBlobUrls();
*
* // Create blob URL (automatically tracked)
* const url = createBlobUrl(file);
*
* // Manually revoke when needed
* revokeBlobUrl(url);
*
* // All URLs are automatically revoked on unmount
* ```
*/ */
export function useBlobUrls() { export function useBlobUrls() {
const blobUrlsRef = useRef<Set<string>>(new Set()); const urlsRef = useRef<Set<string>>(new Set());
// Clean up all blob URLs on unmount useEffect(
useEffect(() => { () => () => {
return () => { for (const url of urlsRef.current) {
blobUrlsRef.current.forEach((url) => URL.revokeObjectURL(url)); URL.revokeObjectURL(url);
blobUrlsRef.current.clear(); }
}; },
}, []); [],
);
/**
* Creates a blob URL from a file or blob and tracks it for automatic cleanup
*/
const createBlobUrl = (blob: Blob | File): string => {
const url = URL.createObjectURL(blob);
blobUrlsRef.current.add(url);
return url;
};
/**
* Revokes a specific blob URL and removes it from tracking
*/
const revokeBlobUrl = (url: string): void => {
if (blobUrlsRef.current.has(url)) {
URL.revokeObjectURL(url);
blobUrlsRef.current.delete(url);
}
};
/**
* Revokes all tracked blob URLs
*/
const revokeAll = (): void => {
blobUrlsRef.current.forEach((url) => URL.revokeObjectURL(url));
blobUrlsRef.current.clear();
};
return { return {
createBlobUrl, createBlobUrl: (blob: Blob | File): string => {
revokeBlobUrl, const url = URL.createObjectURL(blob);
revokeAll, urlsRef.current.add(url);
return url;
},
revokeBlobUrl: (url: string) => {
if (urlsRef.current.has(url)) {
URL.revokeObjectURL(url);
urlsRef.current.delete(url);
}
},
}; };
} }

View File

@ -1,49 +0,0 @@
import { useCallback, useEffect, useRef } from "react";
/**
* Custom hook for debouncing function calls
*
* @param callback - Function to debounce
* @param delay - Delay in milliseconds before invoking the callback
* @returns Debounced version of the callback function
*
* @example
* ```tsx
* const debouncedSearch = useDebounce((query: string) => {
* performSearch(query);
* }, 300);
*
* // Call multiple times, only last call executes after 300ms
* debouncedSearch("hello");
* ```
*/
export function useDebounce<T extends (...args: any[]) => void>(callback: T, delay: number): (...args: Parameters<T>) => void {
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const callbackRef = useRef(callback);
// Keep callback ref up to date
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
// Clean up timeout on unmount
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
return useCallback(
(...args: Parameters<T>) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
callbackRef.current(...args);
}, delay);
},
[delay],
);
}

View File

@ -1,59 +1,32 @@
import { useState } from "react"; import { useState } from "react";
interface UseDragAndDropOptions {
onDrop: (files: FileList) => void;
}
/** /**
* Custom hook for handling drag-and-drop file uploads * Hook for handling drag-and-drop file uploads
* Manages drag state and event handlers
*
* @param options - Configuration options
* @returns Drag state and event handlers
*
* @example
* ```tsx
* const { isDragging, dragHandlers } = useDragAndDrop({
* onDrop: (files) => handleFiles(files),
* });
*
* <div {...dragHandlers} className={isDragging ? 'border-dashed' : ''}>
* Drop files here
* </div>
* ```
*/ */
export function useDragAndDrop({ onDrop }: UseDragAndDropOptions) { export function useDragAndDrop(onDrop: (files: FileList) => void) {
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const handleDragOver = (event: React.DragEvent): void => {
if (event.dataTransfer && event.dataTransfer.types.includes("Files")) {
event.preventDefault();
event.dataTransfer.dropEffect = "copy";
if (!isDragging) {
setIsDragging(true);
}
}
};
const handleDragLeave = (event: React.DragEvent): void => {
event.preventDefault();
setIsDragging(false);
};
const handleDrop = (event: React.DragEvent): void => {
if (event.dataTransfer && event.dataTransfer.files.length > 0) {
event.preventDefault();
setIsDragging(false);
onDrop(event.dataTransfer.files);
}
};
return { return {
isDragging, isDragging,
dragHandlers: { dragHandlers: {
onDragOver: handleDragOver, onDragOver: (e: React.DragEvent) => {
onDragLeave: handleDragLeave, if (e.dataTransfer?.types.includes("Files")) {
onDrop: handleDrop, e.preventDefault();
e.dataTransfer.dropEffect = "copy";
setIsDragging(true);
}
},
onDragLeave: (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
},
onDrop: (e: React.DragEvent) => {
if (e.dataTransfer?.files.length) {
e.preventDefault();
setIsDragging(false);
onDrop(e.dataTransfer.files);
}
},
}, },
}; };
} }

View File

@ -1,40 +1,13 @@
import { useCallback, useEffect } from "react"; import { useEffect } from "react";
interface UseFocusModeOptions {
isFocusMode: boolean;
onToggle: () => void;
}
interface UseFocusModeReturn {
toggleFocusMode: () => void;
}
/** /**
* Custom hook for managing focus mode functionality * Hook to lock body scroll when focus mode is active
* Handles:
* - Body scroll lock when focus mode is active
* - Toggle functionality
* - Cleanup on unmount
*/ */
export function useFocusMode({ isFocusMode, onToggle }: UseFocusModeOptions): UseFocusModeReturn { export function useFocusMode(isFocusMode: boolean): void {
// Lock body scroll when focus mode is active to prevent background scrolling
useEffect(() => { useEffect(() => {
if (isFocusMode) { document.body.style.overflow = isFocusMode ? "hidden" : "";
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
// Cleanup on unmount
return () => { return () => {
document.body.style.overflow = ""; document.body.style.overflow = "";
}; };
}, [isFocusMode]); }, [isFocusMode]);
const toggleFocusMode = useCallback(() => {
onToggle();
}, [onToggle]);
return {
toggleFocusMode,
};
} }

View File

@ -59,39 +59,11 @@ export const useLinkMemo = ({ isOpen, currentMemoName, existingRelations, onAddR
onAddRelation(relation); onAddRelation(relation);
}; };
const getHighlightedContent = (content: string): React.ReactNode => {
if (!searchText) return content;
const index = content.toLowerCase().indexOf(searchText.toLowerCase());
if (index === -1) {
return content;
}
let before = content.slice(0, index);
if (before.length > 20) {
before = "..." + before.slice(before.length - 20);
}
const highlighted = content.slice(index, index + searchText.length);
let after = content.slice(index + searchText.length);
if (after.length > 20) {
after = after.slice(0, 20) + "...";
}
return (
<>
{before}
<mark className="font-medium">{highlighted}</mark>
{after}
</>
);
};
return { return {
searchText, searchText,
setSearchText, setSearchText,
isFetching, isFetching,
filteredMemos, filteredMemos,
addMemoRelation, addMemoRelation,
getHighlightedContent,
}; };
}; };

View File

@ -2,7 +2,7 @@ import { useCallback } from "react";
import { TAB_SPACE_WIDTH } from "@/helpers/consts"; import { TAB_SPACE_WIDTH } from "@/helpers/consts";
import { FOCUS_MODE_EXIT_KEY, FOCUS_MODE_TOGGLE_KEY } from "../constants"; import { FOCUS_MODE_EXIT_KEY, FOCUS_MODE_TOGGLE_KEY } from "../constants";
import type { EditorRefActions } from "../Editor"; import type { EditorRefActions } from "../Editor";
import { handleEditorKeydownWithMarkdownShortcuts } from "../Editor/markdownShortcuts"; import { handleMarkdownShortcuts } from "../Editor/markdownShortcuts";
export interface UseMemoEditorKeyboardOptions { export interface UseMemoEditorKeyboardOptions {
editorRef: React.RefObject<EditorRefActions>; editorRef: React.RefObject<EditorRefActions>;
@ -48,7 +48,7 @@ export const useMemoEditorKeyboard = (options: UseMemoEditorKeyboardOptions) =>
onSave(); onSave();
return; return;
} }
handleEditorKeydownWithMarkdownShortcuts(event, editorRef.current); handleMarkdownShortcuts(event, editorRef.current);
} }
// Tab handling // Tab handling

View File

@ -1,11 +1,9 @@
import { useCallback, useState } from "react"; import { useState } from "react";
import type { Attachment } from "@/types/proto/api/v1/attachment_service"; import type { Attachment } from "@/types/proto/api/v1/attachment_service";
import type { Location, MemoRelation } from "@/types/proto/api/v1/memo_service"; import type { Location, MemoRelation } from "@/types/proto/api/v1/memo_service";
import { Visibility } from "@/types/proto/api/v1/memo_service"; import { Visibility } from "@/types/proto/api/v1/memo_service";
import type { MemoEditorState } from "../types/memo-editor";
export interface UseMemoEditorStateReturn { interface MemoEditorState {
state: MemoEditorState;
memoVisibility: Visibility; memoVisibility: Visibility;
attachmentList: Attachment[]; attachmentList: Attachment[];
relationList: MemoRelation[]; relationList: MemoRelation[];
@ -15,25 +13,12 @@ export interface UseMemoEditorStateReturn {
isRequesting: boolean; isRequesting: boolean;
isComposing: boolean; isComposing: boolean;
isDraggingFile: boolean; isDraggingFile: boolean;
setMemoVisibility: (visibility: Visibility) => void;
setAttachmentList: (attachments: Attachment[]) => void;
setRelationList: (relations: MemoRelation[]) => void;
setLocation: (location: Location | undefined) => void;
setIsFocusMode: (isFocusMode: boolean) => void;
toggleFocusMode: () => void;
setUploadingAttachment: (isUploading: boolean) => void;
setRequesting: (isRequesting: boolean) => void;
setComposing: (isComposing: boolean) => void;
setDraggingFile: (isDragging: boolean) => void;
resetState: () => void;
} }
/** /**
* Hook for managing MemoEditor state * Hook for managing MemoEditor state
* Centralizes all state management and provides clean setters
*/ */
export const useMemoEditorState = (initialVisibility: Visibility = Visibility.PRIVATE): UseMemoEditorStateReturn => { export const useMemoEditorState = (initialVisibility: Visibility = Visibility.PRIVATE) => {
const [state, setState] = useState<MemoEditorState>({ const [state, setState] = useState<MemoEditorState>({
memoVisibility: initialVisibility, memoVisibility: initialVisibility,
isFocusMode: false, isFocusMode: false,
@ -46,79 +31,29 @@ export const useMemoEditorState = (initialVisibility: Visibility = Visibility.PR
isDraggingFile: false, isDraggingFile: false,
}); });
const setMemoVisibility = useCallback((visibility: Visibility) => { const update = <K extends keyof MemoEditorState>(key: K, value: MemoEditorState[K]) => {
setState((prev) => ({ ...prev, memoVisibility: visibility })); setState((prev) => ({ ...prev, [key]: value }));
}, []); };
const setAttachmentList = useCallback((attachments: Attachment[]) => {
setState((prev) => ({ ...prev, attachmentList: attachments }));
}, []);
const setRelationList = useCallback((relations: MemoRelation[]) => {
setState((prev) => ({ ...prev, relationList: relations }));
}, []);
const setLocation = useCallback((location: Location | undefined) => {
setState((prev) => ({ ...prev, location }));
}, []);
const setIsFocusMode = useCallback((isFocusMode: boolean) => {
setState((prev) => ({ ...prev, isFocusMode }));
}, []);
const toggleFocusMode = useCallback(() => {
setState((prev) => ({ ...prev, isFocusMode: !prev.isFocusMode }));
}, []);
const setUploadingAttachment = useCallback((isUploading: boolean) => {
setState((prev) => ({ ...prev, isUploadingAttachment: isUploading }));
}, []);
const setRequesting = useCallback((isRequesting: boolean) => {
setState((prev) => ({ ...prev, isRequesting }));
}, []);
const setComposing = useCallback((isComposing: boolean) => {
setState((prev) => ({ ...prev, isComposing }));
}, []);
const setDraggingFile = useCallback((isDragging: boolean) => {
setState((prev) => ({ ...prev, isDraggingFile: isDragging }));
}, []);
const resetState = useCallback(() => {
setState((prev) => ({
...prev,
isRequesting: false,
attachmentList: [],
relationList: [],
location: undefined,
isDraggingFile: false,
}));
}, []);
return { return {
state, ...state,
memoVisibility: state.memoVisibility, setMemoVisibility: (v: Visibility) => update("memoVisibility", v),
attachmentList: state.attachmentList, setAttachmentList: (v: Attachment[]) => update("attachmentList", v),
relationList: state.relationList, setRelationList: (v: MemoRelation[]) => update("relationList", v),
location: state.location, setLocation: (v: Location | undefined) => update("location", v),
isFocusMode: state.isFocusMode, toggleFocusMode: () => setState((prev) => ({ ...prev, isFocusMode: !prev.isFocusMode })),
isUploadingAttachment: state.isUploadingAttachment, setUploadingAttachment: (v: boolean) => update("isUploadingAttachment", v),
isRequesting: state.isRequesting, setRequesting: (v: boolean) => update("isRequesting", v),
isComposing: state.isComposing, setComposing: (v: boolean) => update("isComposing", v),
isDraggingFile: state.isDraggingFile, setDraggingFile: (v: boolean) => update("isDraggingFile", v),
resetState: () =>
setMemoVisibility, setState((prev) => ({
setAttachmentList, ...prev,
setRelationList, isRequesting: false,
setLocation, attachmentList: [],
setIsFocusMode, relationList: [],
toggleFocusMode, location: undefined,
setUploadingAttachment, isDraggingFile: false,
setRequesting, })),
setComposing,
setDraggingFile,
resetState,
}; };
}; };

View File

@ -18,7 +18,6 @@ import { ErrorBoundary, FocusModeExitButton, FocusModeOverlay } from "./componen
import { FOCUS_MODE_STYLES, LOCALSTORAGE_DEBOUNCE_DELAY } from "./constants"; import { FOCUS_MODE_STYLES, LOCALSTORAGE_DEBOUNCE_DELAY } from "./constants";
import Editor, { type EditorRefActions } from "./Editor"; import Editor, { type EditorRefActions } from "./Editor";
import { import {
useDebounce,
useDragAndDrop, useDragAndDrop,
useFocusMode, useFocusMode,
useLocalFileManager, useLocalFileManager,
@ -31,12 +30,19 @@ import {
import InsertMenu from "./Toolbar/InsertMenu"; import InsertMenu from "./Toolbar/InsertMenu";
import VisibilitySelector from "./Toolbar/VisibilitySelector"; import VisibilitySelector from "./Toolbar/VisibilitySelector";
import { MemoEditorContext } from "./types"; import { MemoEditorContext } from "./types";
import type { MemoEditorProps } from "./types/memo-editor";
// Re-export for backward compatibility export interface Props {
export type { MemoEditorProps as Props }; className?: string;
cacheKey?: string;
placeholder?: string;
memoName?: string;
parentMemoName?: string;
autoFocus?: boolean;
onConfirm?: (memoName: string) => void;
onCancel?: () => void;
}
const MemoEditor = observer((props: MemoEditorProps) => { const MemoEditor = observer((props: Props) => {
const { className, cacheKey, memoName, parentMemoName, autoFocus, onConfirm, onCancel } = props; const { className, cacheKey, memoName, parentMemoName, autoFocus, onConfirm, onCancel } = props;
const t = useTranslate(); const t = useTranslate();
const { i18n } = useTranslation(); const { i18n } = useTranslation();
@ -160,29 +166,31 @@ const MemoEditor = observer((props: MemoEditorProps) => {
}); });
// Focus mode management with body scroll lock // Focus mode management with body scroll lock
useFocusMode({ useFocusMode(isFocusMode);
isFocusMode,
onToggle: toggleFocusMode,
});
// Drag-and-drop for file uploads // Drag-and-drop for file uploads
const { isDragging, dragHandlers } = useDragAndDrop({ const { isDragging, dragHandlers } = useDragAndDrop(addFiles);
onDrop: addFiles,
});
// Sync drag state with component state // Sync drag state with component state
useEffect(() => { useEffect(() => {
setDraggingFile(isDragging); setDraggingFile(isDragging);
}, [isDragging, setDraggingFile]); }, [isDragging, setDraggingFile]);
// Debounced cache setter to avoid writing to localStorage on every keystroke // Debounced cache setter
const saveContentToCache = useDebounce((content: string) => { const cacheTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
if (content !== "") { const saveContentToCache = useCallback(
setContentCache(content); (content: string) => {
} else { clearTimeout(cacheTimeoutRef.current);
localStorage.removeItem(contentCacheKey); cacheTimeoutRef.current = setTimeout(() => {
} if (content !== "") {
}, LOCALSTORAGE_DEBOUNCE_DELAY); setContentCache(content);
} else {
localStorage.removeItem(contentCacheKey);
}
}, LOCALSTORAGE_DEBOUNCE_DELAY);
},
[contentCacheKey, setContentCache],
);
// Compute reference relations // Compute reference relations
const referenceRelations = useMemo(() => { const referenceRelations = useMemo(() => {

View File

@ -1,5 +0,0 @@
export type Command = {
name: string;
run: () => string;
cursorOffset?: number;
};

View File

@ -1,5 +1,3 @@
// MemoEditor type exports // MemoEditor type exports
export type { Command } from "./command";
export { MemoEditorContext, type MemoEditorContextValue } from "./context"; export { MemoEditorContext, type MemoEditorContextValue } from "./context";
export type { LinkMemoState, LocationState } from "./insert-menu"; export type { LocationState } from "./insert-menu";
export type { EditorConfig, MemoEditorProps, MemoEditorState } from "./memo-editor";

View File

@ -1,5 +1,4 @@
import { LatLng } from "leaflet"; import { LatLng } from "leaflet";
import { Memo } from "@/types/proto/api/v1/memo_service";
export interface LocationState { export interface LocationState {
placeholder: string; placeholder: string;
@ -7,9 +6,3 @@ export interface LocationState {
latInput: string; latInput: string;
lngInput: string; lngInput: string;
} }
export interface LinkMemoState {
searchText: string;
isFetching: boolean;
fetchedMemos: Memo[];
}

View File

@ -1,63 +0,0 @@
import type { Attachment } from "@/types/proto/api/v1/attachment_service";
import type { Location, MemoRelation, Visibility } from "@/types/proto/api/v1/memo_service";
/**
* Props for the MemoEditor component
*/
export interface MemoEditorProps {
/** Optional CSS class name */
className?: string;
/** Cache key for localStorage persistence */
cacheKey?: string;
/** Placeholder text for empty editor */
placeholder?: string;
/** Name of the memo being edited (for edit mode) */
memoName?: string;
/** Name of parent memo (for comment/reply mode) */
parentMemoName?: string;
/** Whether to auto-focus the editor on mount */
autoFocus?: boolean;
/** Callback when memo is saved successfully */
onConfirm?: (memoName: string) => void;
/** Callback when editing is canceled */
onCancel?: () => void;
}
/**
* Internal state for MemoEditor component
*/
export interface MemoEditorState {
/** Visibility level of the memo */
memoVisibility: Visibility;
/** List of attachments */
attachmentList: Attachment[];
/** List of related memos */
relationList: MemoRelation[];
/** Geographic location */
location: Location | undefined;
/** Whether attachments are currently being uploaded */
isUploadingAttachment: boolean;
/** Whether save/update request is in progress */
isRequesting: boolean;
/** Whether IME composition is active (for Asian languages) */
isComposing: boolean;
/** Whether files are being dragged over the editor */
isDraggingFile: boolean;
/** Whether Focus Mode is enabled */
isFocusMode: boolean;
}
/**
* Configuration for the Editor sub-component
*/
export interface EditorConfig {
className: string;
initialContent: string;
placeholder: string;
onContentChange: (content: string) => void;
onPaste: (event: React.ClipboardEvent) => void;
isFocusMode: boolean;
isInIME: boolean;
onCompositionStart: () => void;
onCompositionEnd: () => void;
}

View File

@ -1,9 +1,3 @@
// UNKNOWN_ID is the symbol for unknown id.
export const UNKNOWN_ID = -1;
// DAILY_TIMESTAMP is the timestamp for a day.
export const DAILY_TIMESTAMP = 3600 * 24 * 1000;
// TAB_SPACE_WIDTH is the default tab space width. // TAB_SPACE_WIDTH is the default tab space width.
export const TAB_SPACE_WIDTH = 2; export const TAB_SPACE_WIDTH = 2;