mirror of https://github.com/usememos/memos.git
chore: streamline MemoEditor components and remove unused code
This commit is contained in:
parent
26cb357685
commit
7aa8262ef2
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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[] = [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
export type Command = {
|
|
||||||
name: string;
|
|
||||||
run: () => string;
|
|
||||||
cursorOffset?: number;
|
|
||||||
};
|
|
||||||
|
|
@ -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";
|
|
||||||
|
|
|
||||||
|
|
@ -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[];
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue