|
|
|
@ -1,4 +1,12 @@ |
|
|
|
import { useCallback, useLayoutEffect, useRef, useState, type ClipboardEvent, type KeyboardEvent } from "react"; |
|
|
|
import { |
|
|
|
useCallback, |
|
|
|
useLayoutEffect, |
|
|
|
useRef, |
|
|
|
useState, |
|
|
|
type ClipboardEvent, |
|
|
|
type CompositionEvent, |
|
|
|
type KeyboardEvent, |
|
|
|
} from "react"; |
|
|
|
import { useTranslation } from "react-i18next"; |
|
|
|
|
|
|
|
import type { LyricsSectionTagItem } from "./lyricsSectionTags"; |
|
|
|
@ -23,6 +31,7 @@ export function VoiceLyricsTextbox({ value, onChange, maxLength, placeholder }: |
|
|
|
const { t } = useTranslation(); |
|
|
|
const editorRef = useRef<HTMLDivElement>(null); |
|
|
|
const isInternalUpdateRef = useRef(false); |
|
|
|
const isComposingRef = useRef(false); |
|
|
|
const pendingSlashOpenIndexRef = useRef<number | null>(null); |
|
|
|
const [popover, setPopover] = useState<LyricsSectionTagPopoverState>(null); |
|
|
|
const [activeIndex, setActiveIndex] = useState(0); |
|
|
|
@ -62,6 +71,8 @@ export function VoiceLyricsTextbox({ value, onChange, maxLength, placeholder }: |
|
|
|
const editor = editorRef.current; |
|
|
|
if (!editor) return; |
|
|
|
|
|
|
|
if (isComposingRef.current) return; |
|
|
|
|
|
|
|
if (isInternalUpdateRef.current) { |
|
|
|
isInternalUpdateRef.current = false; |
|
|
|
return; |
|
|
|
@ -79,7 +90,7 @@ export function VoiceLyricsTextbox({ value, onChange, maxLength, placeholder }: |
|
|
|
setLyricsTextboxCaretOffset(editor, Math.min(offset, value.length)); |
|
|
|
}, [value]); |
|
|
|
|
|
|
|
const handleInput = useCallback(() => { |
|
|
|
const syncInputState = useCallback(() => { |
|
|
|
const editor = editorRef.current; |
|
|
|
if (!editor) return; |
|
|
|
|
|
|
|
@ -105,6 +116,32 @@ export function VoiceLyricsTextbox({ value, onChange, maxLength, placeholder }: |
|
|
|
}); |
|
|
|
}, [commitLyrics, openPopover]); |
|
|
|
|
|
|
|
const handleInput = useCallback(() => { |
|
|
|
if (isComposingRef.current) { |
|
|
|
const editor = editorRef.current; |
|
|
|
if (editor) { |
|
|
|
editor.dataset.empty = getLyricsTextboxPlainText(editor).length === 0 ? "true" : "false"; |
|
|
|
} |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
syncInputState(); |
|
|
|
}, [syncInputState]); |
|
|
|
|
|
|
|
const handleCompositionStart = useCallback(() => { |
|
|
|
isComposingRef.current = true; |
|
|
|
}, []); |
|
|
|
|
|
|
|
const handleCompositionEnd = useCallback( |
|
|
|
(_event: CompositionEvent<HTMLDivElement>) => { |
|
|
|
isComposingRef.current = false; |
|
|
|
requestAnimationFrame(() => { |
|
|
|
syncInputState(); |
|
|
|
}); |
|
|
|
}, |
|
|
|
[syncInputState], |
|
|
|
); |
|
|
|
|
|
|
|
const handleSelectTag = useCallback( |
|
|
|
(item: LyricsSectionTagItem) => { |
|
|
|
if (!popover) return; |
|
|
|
@ -134,6 +171,7 @@ export function VoiceLyricsTextbox({ value, onChange, maxLength, placeholder }: |
|
|
|
if ( |
|
|
|
event.key === "/" && |
|
|
|
!event.nativeEvent.isComposing && |
|
|
|
!isComposingRef.current && |
|
|
|
!event.ctrlKey && |
|
|
|
!event.metaKey && |
|
|
|
!event.altKey && |
|
|
|
@ -146,6 +184,8 @@ export function VoiceLyricsTextbox({ value, onChange, maxLength, placeholder }: |
|
|
|
|
|
|
|
if (!popover) return; |
|
|
|
|
|
|
|
if (event.nativeEvent.isComposing || isComposingRef.current) return; |
|
|
|
|
|
|
|
if (event.key === "Escape") { |
|
|
|
event.preventDefault(); |
|
|
|
closePopover(); |
|
|
|
@ -198,6 +238,8 @@ export function VoiceLyricsTextbox({ value, onChange, maxLength, placeholder }: |
|
|
|
contentEditable |
|
|
|
suppressContentEditableWarning |
|
|
|
onInput={handleInput} |
|
|
|
onCompositionStart={handleCompositionStart} |
|
|
|
onCompositionEnd={handleCompositionEnd} |
|
|
|
onKeyDown={handleKeyDown} |
|
|
|
onPaste={handlePaste} |
|
|
|
/> |
|
|
|
|