Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
7aa8262e
Commit
7aa8262e
authored
Nov 30, 2025
by
Johnny
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
chore: streamline MemoEditor components and remove unused code
parent
26cb3576
Changes
21
Hide whitespace changes
Inline
Side-by-side
Showing
21 changed files
with
147 additions
and
488 deletions
+147
-488
CommandSuggestions.tsx
web/src/components/MemoEditor/Editor/CommandSuggestions.tsx
+2
-2
commands.ts
web/src/components/MemoEditor/Editor/commands.ts
+8
-1
index.tsx
web/src/components/MemoEditor/Editor/index.tsx
+1
-4
markdownShortcuts.ts
web/src/components/MemoEditor/Editor/markdownShortcuts.ts
+0
-5
InsertMenu.tsx
web/src/components/MemoEditor/Toolbar/InsertMenu.tsx
+0
-1
LinkMemoDialog.tsx
web/src/components/MemoEditor/components/LinkMemoDialog.tsx
+29
-3
index.ts
web/src/components/MemoEditor/hooks/index.ts
+1
-6
useAbortController.ts
web/src/components/MemoEditor/hooks/useAbortController.ts
+5
-60
useBlobUrls.ts
web/src/components/MemoEditor/hooks/useBlobUrls.ts
+21
-55
useDebounce.ts
web/src/components/MemoEditor/hooks/useDebounce.ts
+0
-49
useDragAndDrop.ts
web/src/components/MemoEditor/hooks/useDragAndDrop.ts
+20
-47
useFocusMode.ts
web/src/components/MemoEditor/hooks/useFocusMode.ts
+4
-31
useLinkMemo.ts
web/src/components/MemoEditor/hooks/useLinkMemo.ts
+0
-28
useMemoEditorKeyboard.ts
web/src/components/MemoEditor/hooks/useMemoEditorKeyboard.ts
+2
-2
useMemoEditorState.ts
web/src/components/MemoEditor/hooks/useMemoEditorState.ts
+25
-90
index.tsx
web/src/components/MemoEditor/index.tsx
+28
-20
command.ts
web/src/components/MemoEditor/types/command.ts
+0
-5
index.ts
web/src/components/MemoEditor/types/index.ts
+1
-3
insert-menu.ts
web/src/components/MemoEditor/types/insert-menu.ts
+0
-7
memo-editor.ts
web/src/components/MemoEditor/types/memo-editor.ts
+0
-63
consts.ts
web/src/helpers/consts.ts
+0
-6
No files found.
web/src/components/MemoEditor/Editor/CommandSuggestions.tsx
View file @
7aa8262e
import
{
observer
}
from
"mobx-react-lite"
;
import
OverflowTip
from
"@/components/kit/OverflowTip"
;
import
{
Command
}
from
"../types/command
"
;
import
{
EditorRefActions
}
from
".
"
;
import
type
{
EditorRefActions
}
from
".
"
;
import
type
{
Command
}
from
"./commands
"
;
import
{
SuggestionsPopup
}
from
"./SuggestionsPopup"
;
import
{
useSuggestions
}
from
"./useSuggestions"
;
...
...
web/src/components/MemoEditor/Editor/commands.ts
View file @
7aa8262e
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
[]
=
[
{
...
...
web/src/components/MemoEditor/Editor/index.tsx
View file @
7aa8262e
import
{
forwardRef
,
ReactNode
,
useCallback
,
useEffect
,
useImperativeHandle
,
useRef
,
useState
}
from
"react"
;
import
{
forwardRef
,
useCallback
,
useEffect
,
useImperativeHandle
,
useRef
}
from
"react"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
EDITOR_HEIGHT
}
from
"../constants"
;
import
{
Command
}
from
"../types/command"
;
import
CommandSuggestions
from
"./CommandSuggestions"
;
import
{
editorCommands
}
from
"./commands"
;
import
TagSuggestions
from
"./TagSuggestions"
;
...
...
@@ -27,8 +26,6 @@ interface Props {
className
:
string
;
initialContent
:
string
;
placeholder
:
string
;
tools
?:
ReactNode
;
commands
?:
Command
[];
onContentChange
:
(
content
:
string
)
=>
void
;
onPaste
:
(
event
:
React
.
ClipboardEvent
)
=>
void
;
/** Whether Focus Mode is active - adjusts height constraints for immersive writing */
...
...
web/src/components/MemoEditor/Editor/markdownShortcuts.ts
View file @
7aa8262e
...
...
@@ -3,8 +3,6 @@ import type { EditorRefActions } from "./index";
/**
* Handles keyboard shortcuts for markdown formatting
* Requires Cmd/Ctrl key to be pressed
*
* @alias handleEditorKeydownWithMarkdownShortcuts - for backward compatibility
*/
export
function
handleMarkdownShortcuts
(
event
:
React
.
KeyboardEvent
,
editor
:
EditorRefActions
):
void
{
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
* If selected text is a URL, creates a link with empty text
...
...
web/src/components/MemoEditor/Toolbar/InsertMenu.tsx
View file @
7aa8262e
...
...
@@ -197,7 +197,6 @@ const InsertMenu = observer((props: Props) => {
filteredMemos=
{
linkMemo
.
filteredMemos
}
isFetching=
{
linkMemo
.
isFetching
}
onSelectMemo=
{
linkMemo
.
addMemoRelation
}
getHighlightedContent=
{
linkMemo
.
getHighlightedContent
}
/>
<
LocationDialog
...
...
web/src/components/MemoEditor/components/LinkMemoDialog.tsx
View file @
7aa8262e
...
...
@@ -3,6 +3,34 @@ import { Input } from "@/components/ui/input";
import
{
Memo
}
from
"@/types/proto/api/v1/memo_service"
;
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
{
open
:
boolean
;
onOpenChange
:
(
open
:
boolean
)
=>
void
;
...
...
@@ -11,7 +39,6 @@ interface LinkMemoDialogProps {
filteredMemos
:
Memo
[];
isFetching
:
boolean
;
onSelectMemo
:
(
memo
:
Memo
)
=>
void
;
getHighlightedContent
:
(
content
:
string
)
=>
React
.
ReactNode
;
}
export
const
LinkMemoDialog
=
({
...
...
@@ -22,7 +49,6 @@ export const LinkMemoDialog = ({
filteredMemos
,
isFetching
,
onSelectMemo
,
getHighlightedContent
,
}:
LinkMemoDialogProps
)
=>
{
const
t
=
useTranslate
();
...
...
@@ -54,7 +80,7 @@ export const LinkMemoDialog = ({
<
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=
"mt-0.5 text-sm leading-5 line-clamp-2"
>
{
searchText
?
getHighlightedContent
(
memo
.
conten
t
)
:
memo
.
snippet
}
{
searchText
?
highlightSearchText
(
memo
.
content
,
searchTex
t
)
:
memo
.
snippet
}
</
p
>
</
div
>
</
div
>
...
...
web/src/components/MemoEditor/hooks/index.ts
View file @
7aa8262e
// Custom hooks for MemoEditor
// Custom hooks for MemoEditor
(internal use only)
export
{
useAbortController
}
from
"./useAbortController"
;
export
{
useBlobUrls
}
from
"./useBlobUrls"
;
export
{
useDebounce
}
from
"./useDebounce"
;
export
{
useDragAndDrop
}
from
"./useDragAndDrop"
;
export
{
useFileUpload
}
from
"./useFileUpload"
;
export
{
useFocusMode
}
from
"./useFocusMode"
;
export
{
useLinkMemo
}
from
"./useLinkMemo"
;
export
{
useLocalFileManager
}
from
"./useLocalFileManager"
;
export
{
useLocation
}
from
"./useLocation"
;
export
type
{
UseMemoEditorHandlersOptions
,
UseMemoEditorHandlersReturn
}
from
"./useMemoEditorHandlers"
;
export
{
useMemoEditorHandlers
}
from
"./useMemoEditorHandlers"
;
export
type
{
UseMemoEditorInitOptions
,
UseMemoEditorInitReturn
}
from
"./useMemoEditorInit"
;
export
{
useMemoEditorInit
}
from
"./useMemoEditorInit"
;
export
type
{
UseMemoEditorKeyboardOptions
}
from
"./useMemoEditorKeyboard"
;
export
{
useMemoEditorKeyboard
}
from
"./useMemoEditorKeyboard"
;
export
type
{
UseMemoEditorStateReturn
}
from
"./useMemoEditorState"
;
export
{
useMemoEditorState
}
from
"./useMemoEditorState"
;
export
{
useMemoSave
}
from
"./useMemoSave"
;
web/src/components/MemoEditor/hooks/useAbortController.ts
View file @
7aa8262e
import
{
useEffect
,
useRef
}
from
"react"
;
/**
* Custom 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 });
* ```
* Hook for managing AbortController lifecycle
*/
export
function
useAbortController
()
{
const
controllerRef
=
useRef
<
AbortController
|
null
>
(
null
);
// Clean up on unmount
useEffect
(()
=>
{
return
()
=>
{
controllerRef
.
current
?.
abort
();
};
},
[]);
useEffect
(()
=>
()
=>
controllerRef
.
current
?.
abort
(),
[]);
/**
* Aborts the current request if one exists
*/
const
abort
=
():
void
=>
{
const
abort
=
()
=>
{
controllerRef
.
current
?.
abort
();
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
=>
{
abort
();
return
create
();
};
/**
* Gets the signal from the current controller, or creates new one
*/
const
getSignal
=
():
AbortSignal
=>
{
if
(
!
controllerRef
.
current
)
{
return
create
();
}
controllerRef
.
current
=
new
AbortController
();
return
controllerRef
.
current
.
signal
;
};
return
{
abort
,
create
,
abortAndCreate
,
getSignal
,
};
return
{
abort
,
abortAndCreate
};
}
web/src/components/MemoEditor/hooks/useBlobUrls.ts
View file @
7aa8262e
import
{
useEffect
,
useRef
}
from
"react"
;
/**
* Custom hook for managing blob URLs lifecycle
* 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
* ```
* Hook for managing blob URLs lifecycle with automatic cleanup
*/
export
function
useBlobUrls
()
{
const
blobU
rlsRef
=
useRef
<
Set
<
string
>>
(
new
Set
());
const
u
rlsRef
=
useRef
<
Set
<
string
>>
(
new
Set
());
// Clean up all blob URLs on unmount
useEffect
(()
=>
{
return
()
=>
{
blobUrlsRef
.
current
.
forEach
((
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
();
};
useEffect
(
()
=>
()
=>
{
for
(
const
url
of
urlsRef
.
current
)
{
URL
.
revokeObjectURL
(
url
);
}
},
[],
);
return
{
createBlobUrl
,
revokeBlobUrl
,
revokeAll
,
createBlobUrl
:
(
blob
:
Blob
|
File
):
string
=>
{
const
url
=
URL
.
createObjectURL
(
blob
);
urlsRef
.
current
.
add
(
url
);
return
url
;
},
revokeBlobUrl
:
(
url
:
string
)
=>
{
if
(
urlsRef
.
current
.
has
(
url
))
{
URL
.
revokeObjectURL
(
url
);
urlsRef
.
current
.
delete
(
url
);
}
},
};
}
web/src/components/MemoEditor/hooks/useDebounce.ts
deleted
100644 → 0
View file @
26cb3576
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
],
);
}
web/src/components/MemoEditor/hooks/useDragAndDrop.ts
View file @
7aa8262e
import
{
useState
}
from
"react"
;
interface
UseDragAndDropOptions
{
onDrop
:
(
files
:
FileList
)
=>
void
;
}
/**
* Custom 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>
* ```
* Hook for handling drag-and-drop file uploads
*/
export
function
useDragAndDrop
(
{
onDrop
}:
UseDragAndDropOptions
)
{
export
function
useDragAndDrop
(
onDrop
:
(
files
:
FileList
)
=>
void
)
{
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
{
isDragging
,
dragHandlers
:
{
onDragOver
:
handleDragOver
,
onDragLeave
:
handleDragLeave
,
onDrop
:
handleDrop
,
onDragOver
:
(
e
:
React
.
DragEvent
)
=>
{
if
(
e
.
dataTransfer
?.
types
.
includes
(
"Files"
))
{
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
);
}
},
},
};
}
web/src/components/MemoEditor/hooks/useFocusMode.ts
View file @
7aa8262e
import
{
useCallback
,
useEffect
}
from
"react"
;
interface
UseFocusModeOptions
{
isFocusMode
:
boolean
;
onToggle
:
()
=>
void
;
}
interface
UseFocusModeReturn
{
toggleFocusMode
:
()
=>
void
;
}
import
{
useEffect
}
from
"react"
;
/**
* Custom hook for managing focus mode functionality
* Handles:
* - Body scroll lock when focus mode is active
* - Toggle functionality
* - Cleanup on unmount
* Hook to lock body scroll when focus mode is active
*/
export
function
useFocusMode
({
isFocusMode
,
onToggle
}:
UseFocusModeOptions
):
UseFocusModeReturn
{
// Lock body scroll when focus mode is active to prevent background scrolling
export
function
useFocusMode
(
isFocusMode
:
boolean
):
void
{
useEffect
(()
=>
{
if
(
isFocusMode
)
{
document
.
body
.
style
.
overflow
=
"hidden"
;
}
else
{
document
.
body
.
style
.
overflow
=
""
;
}
// Cleanup on unmount
document
.
body
.
style
.
overflow
=
isFocusMode
?
"hidden"
:
""
;
return
()
=>
{
document
.
body
.
style
.
overflow
=
""
;
};
},
[
isFocusMode
]);
const
toggleFocusMode
=
useCallback
(()
=>
{
onToggle
();
},
[
onToggle
]);
return
{
toggleFocusMode
,
};
}
web/src/components/MemoEditor/hooks/useLinkMemo.ts
x
→
web/src/components/MemoEditor/hooks/useLinkMemo.ts
View file @
7aa8262e
...
...
@@ -59,39 +59,11 @@ export const useLinkMemo = ({ isOpen, currentMemoName, existingRelations, onAddR
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
{
searchText
,
setSearchText
,
isFetching
,
filteredMemos
,
addMemoRelation
,
getHighlightedContent
,
};
};
web/src/components/MemoEditor/hooks/useMemoEditorKeyboard.ts
View file @
7aa8262e
...
...
@@ -2,7 +2,7 @@ import { useCallback } from "react";
import
{
TAB_SPACE_WIDTH
}
from
"@/helpers/consts"
;
import
{
FOCUS_MODE_EXIT_KEY
,
FOCUS_MODE_TOGGLE_KEY
}
from
"../constants"
;
import
type
{
EditorRefActions
}
from
"../Editor"
;
import
{
handle
EditorKeydownWith
MarkdownShortcuts
}
from
"../Editor/markdownShortcuts"
;
import
{
handleMarkdownShortcuts
}
from
"../Editor/markdownShortcuts"
;
export
interface
UseMemoEditorKeyboardOptions
{
editorRef
:
React
.
RefObject
<
EditorRefActions
>
;
...
...
@@ -48,7 +48,7 @@ export const useMemoEditorKeyboard = (options: UseMemoEditorKeyboardOptions) =>
onSave
();
return
;
}
handle
EditorKeydownWith
MarkdownShortcuts
(
event
,
editorRef
.
current
);
handleMarkdownShortcuts
(
event
,
editorRef
.
current
);
}
// Tab handling
...
...
web/src/components/MemoEditor/hooks/useMemoEditorState.ts
View file @
7aa8262e
import
{
use
Callback
,
use
State
}
from
"react"
;
import
{
useState
}
from
"react"
;
import
type
{
Attachment
}
from
"@/types/proto/api/v1/attachment_service"
;
import
type
{
Location
,
MemoRelation
}
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
{
state
:
MemoEditorState
;
interface
MemoEditorState
{
memoVisibility
:
Visibility
;
attachmentList
:
Attachment
[];
relationList
:
MemoRelation
[];
...
...
@@ -15,25 +13,12 @@ export interface UseMemoEditorStateReturn {
isRequesting
:
boolean
;
isComposing
:
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
* 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
>
({
memoVisibility
:
initialVisibility
,
isFocusMode
:
false
,
...
...
@@ -46,79 +31,29 @@ export const useMemoEditorState = (initialVisibility: Visibility = Visibility.PR
isDraggingFile
:
false
,
});
const
setMemoVisibility
=
useCallback
((
visibility
:
Visibility
)
=>
{
setState
((
prev
)
=>
({
...
prev
,
memoVisibility
:
visibility
}));
},
[]);
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
,
}));
},
[]);
const
update
=
<
K
extends
keyof
MemoEditorState
>
(
key
:
K
,
value
:
MemoEditorState
[
K
])
=>
{
setState
((
prev
)
=>
({
...
prev
,
[
key
]:
value
}));
};
return
{
state
,
memoVisibility
:
state
.
memoVisibility
,
attachmentList
:
state
.
attachmentList
,
relationList
:
state
.
relationList
,
location
:
state
.
location
,
isFocusMode
:
state
.
isFocusMode
,
isUploadingAttachment
:
state
.
isUploadingAttachment
,
isRequesting
:
state
.
isRequesting
,
isComposing
:
state
.
isComposing
,
isDraggingFile
:
state
.
isDraggingFile
,
setMemoVisibility
,
setAttachmentList
,
setRelationList
,
setLocation
,
setIsFocusMode
,
toggleFocusMode
,
setUploadingAttachment
,
setRequesting
,
setComposing
,
setDraggingFile
,
resetState
,
...
state
,
setMemoVisibility
:
(
v
:
Visibility
)
=>
update
(
"memoVisibility"
,
v
),
setAttachmentList
:
(
v
:
Attachment
[])
=>
update
(
"attachmentList"
,
v
),
setRelationList
:
(
v
:
MemoRelation
[])
=>
update
(
"relationList"
,
v
),
setLocation
:
(
v
:
Location
|
undefined
)
=>
update
(
"location"
,
v
),
toggleFocusMode
:
()
=>
setState
((
prev
)
=>
({
...
prev
,
isFocusMode
:
!
prev
.
isFocusMode
})),
setUploadingAttachment
:
(
v
:
boolean
)
=>
update
(
"isUploadingAttachment"
,
v
),
setRequesting
:
(
v
:
boolean
)
=>
update
(
"isRequesting"
,
v
),
setComposing
:
(
v
:
boolean
)
=>
update
(
"isComposing"
,
v
),
setDraggingFile
:
(
v
:
boolean
)
=>
update
(
"isDraggingFile"
,
v
),
resetState
:
()
=>
setState
((
prev
)
=>
({
...
prev
,
isRequesting
:
false
,
attachmentList
:
[],
relationList
:
[],
location
:
undefined
,
isDraggingFile
:
false
,
})),
};
};
web/src/components/MemoEditor/index.tsx
View file @
7aa8262e
...
...
@@ -18,7 +18,6 @@ import { ErrorBoundary, FocusModeExitButton, FocusModeOverlay } from "./componen
import
{
FOCUS_MODE_STYLES
,
LOCALSTORAGE_DEBOUNCE_DELAY
}
from
"./constants"
;
import
Editor
,
{
type
EditorRefActions
}
from
"./Editor"
;
import
{
useDebounce
,
useDragAndDrop
,
useFocusMode
,
useLocalFileManager
,
...
...
@@ -31,12 +30,19 @@ import {
import
InsertMenu
from
"./Toolbar/InsertMenu"
;
import
VisibilitySelector
from
"./Toolbar/VisibilitySelector"
;
import
{
MemoEditorContext
}
from
"./types"
;
import
type
{
MemoEditorProps
}
from
"./types/memo-editor"
;
// Re-export for backward compatibility
export
type
{
MemoEditorProps
as
Props
};
export
interface
Props
{
className
?:
string
;
cacheKey
?:
string
;
placeholder
?:
string
;
memoName
?:
string
;
parentMemoName
?:
string
;
autoFocus
?:
boolean
;
onConfirm
?:
(
memoName
:
string
)
=>
void
;
onCancel
?:
()
=>
void
;
}
const
MemoEditor
=
observer
((
props
:
MemoEditor
Props
)
=>
{
const
MemoEditor
=
observer
((
props
:
Props
)
=>
{
const
{
className
,
cacheKey
,
memoName
,
parentMemoName
,
autoFocus
,
onConfirm
,
onCancel
}
=
props
;
const
t
=
useTranslate
();
const
{
i18n
}
=
useTranslation
();
...
...
@@ -160,29 +166,31 @@ const MemoEditor = observer((props: MemoEditorProps) => {
});
// Focus mode management with body scroll lock
useFocusMode
({
isFocusMode
,
onToggle
:
toggleFocusMode
,
});
useFocusMode
(
isFocusMode
);
// Drag-and-drop for file uploads
const
{
isDragging
,
dragHandlers
}
=
useDragAndDrop
({
onDrop
:
addFiles
,
});
const
{
isDragging
,
dragHandlers
}
=
useDragAndDrop
(
addFiles
);
// Sync drag state with component state
useEffect
(()
=>
{
setDraggingFile
(
isDragging
);
},
[
isDragging
,
setDraggingFile
]);
// Debounced cache setter to avoid writing to localStorage on every keystroke
const
saveContentToCache
=
useDebounce
((
content
:
string
)
=>
{
if
(
content
!==
""
)
{
setContentCache
(
content
);
}
else
{
localStorage
.
removeItem
(
contentCacheKey
);
}
},
LOCALSTORAGE_DEBOUNCE_DELAY
);
// Debounced cache setter
const
cacheTimeoutRef
=
useRef
<
ReturnType
<
typeof
setTimeout
>>
();
const
saveContentToCache
=
useCallback
(
(
content
:
string
)
=>
{
clearTimeout
(
cacheTimeoutRef
.
current
);
cacheTimeoutRef
.
current
=
setTimeout
(()
=>
{
if
(
content
!==
""
)
{
setContentCache
(
content
);
}
else
{
localStorage
.
removeItem
(
contentCacheKey
);
}
},
LOCALSTORAGE_DEBOUNCE_DELAY
);
},
[
contentCacheKey
,
setContentCache
],
);
// Compute reference relations
const
referenceRelations
=
useMemo
(()
=>
{
...
...
web/src/components/MemoEditor/types/command.ts
deleted
100644 → 0
View file @
26cb3576
export
type
Command
=
{
name
:
string
;
run
:
()
=>
string
;
cursorOffset
?:
number
;
};
web/src/components/MemoEditor/types/index.ts
View file @
7aa8262e
// MemoEditor type exports
export
type
{
Command
}
from
"./command"
;
export
{
MemoEditorContext
,
type
MemoEditorContextValue
}
from
"./context"
;
export
type
{
LinkMemoState
,
LocationState
}
from
"./insert-menu"
;
export
type
{
EditorConfig
,
MemoEditorProps
,
MemoEditorState
}
from
"./memo-editor"
;
export
type
{
LocationState
}
from
"./insert-menu"
;
web/src/components/MemoEditor/types/insert-menu.ts
View file @
7aa8262e
import
{
LatLng
}
from
"leaflet"
;
import
{
Memo
}
from
"@/types/proto/api/v1/memo_service"
;
export
interface
LocationState
{
placeholder
:
string
;
...
...
@@ -7,9 +6,3 @@ export interface LocationState {
latInput
:
string
;
lngInput
:
string
;
}
export
interface
LinkMemoState
{
searchText
:
string
;
isFetching
:
boolean
;
fetchedMemos
:
Memo
[];
}
web/src/components/MemoEditor/types/memo-editor.ts
deleted
100644 → 0
View file @
26cb3576
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
;
}
web/src/helpers/consts.ts
View file @
7aa8262e
// 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.
export
const
TAB_SPACE_WIDTH
=
2
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment