Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fix #410 && 评论系统中添加每次换行默认换两行 #411

Merged
merged 4 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 54 additions & 22 deletions src/components/modules/comment/CommentBox/UniversalTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useIsMobile } from '~/atoms/hooks'
import { FloatPopover } from '~/components/ui/float-popover'
import { TextArea } from '~/components/ui/input'
import { useRefValue } from '~/hooks/common/use-ref-value'
import { scrollTextareaToCursor } from '~/lib/dom'

import { getRandomPlaceholder } from './constants'
import {
Expand All @@ -26,29 +27,59 @@ export const UniversalTextArea: Component = ({ className }) => {
const value = useCommentBoxTextValue()

const taRef = useRef<HTMLTextAreaElement>(null)
const handleInsertEmoji = useCallback((emoji: string) => {
if (!taRef.current) {
return
}
const handleInsertEmoji = useCallback(
(emoji: string) => {
if (!taRef.current) {
return
}

const $ta = taRef.current
const start = $ta.selectionStart
const end = $ta.selectionEnd

$ta.value = `${$ta.value.substring(
0,
start,
)} ${emoji} ${$ta.value.substring(end, $ta.value.length)}`

setter('text', $ta.value)
requestAnimationFrame(() => {
const shouldMoveToPos = start + emoji.length + 2
$ta.selectionStart = shouldMoveToPos
$ta.selectionEnd = shouldMoveToPos

$ta.focus()
})
}, [])
const $ta = taRef.current
const start = $ta.selectionStart
const end = $ta.selectionEnd

$ta.value = `${$ta.value.substring(
0,
start,
)} ${emoji} ${$ta.value.substring(end, $ta.value.length)}`

setter('text', $ta.value)
requestAnimationFrame(() => {
const shouldMoveToPos = start + emoji.length + 2
$ta.selectionStart = shouldMoveToPos
$ta.selectionEnd = shouldMoveToPos

$ta.focus()
})
},
[setter],
)

const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey) {
e.preventDefault()
const $ta = taRef.current
if ($ta) {
const start = $ta.selectionStart
const end = $ta.selectionEnd
const textBefore = $ta.value.substring(0, start)
const textAfter = $ta.value.substring(end)
$ta.value = `${textBefore}\n\n${textAfter}`
setter('text', $ta.value)

requestAnimationFrame(() => {
const shouldMoveToPos = start + 2
$ta.selectionStart = shouldMoveToPos
$ta.selectionEnd = shouldMoveToPos
$ta.focus()
// 上面设置的光标,可能不在可见区域内,因此 scroll 到光标所在位置
scrollTextareaToCursor(taRef)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

})
}
}
},
[setter],
)

useEffect(() => {
const $ta = taRef.current
Expand Down Expand Up @@ -80,6 +111,7 @@ export const UniversalTextArea: Component = ({ className }) => {
wrapperClassName={className}
ref={taRef}
defaultValue={value}
onKeyDown={handleKeyDown}
onChange={(e) => setter('text', e.target.value)}
placeholder={placeholder}
onCmdEnter={(e) => {
Expand Down
21 changes: 13 additions & 8 deletions src/components/ui/input/TextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const TextArea = forwardRef<
rounded = 'xl',
bordered = true,
onCmdEnter,
onKeyDown,
...rest
} = props
const mouseX = useMotionValue(0)
Expand All @@ -54,9 +55,20 @@ export const TextArea = forwardRef<
},
[mouseX, mouseY],
)
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
onCmdEnter?.(e)
}
onKeyDown?.(e)
},
[onCmdEnter, onKeyDown],
)
const background = useMotionTemplate`radial-gradient(320px circle at ${mouseX}px ${mouseY}px, var(--spotlight-color) 0%, transparent 85%)`
const isMobile = useIsMobile()
const inputProps = useInputComposition(props)
const inputProps = useInputComposition(
Object.assign({}, props, { onKeyDown: handleKeyDown }),
)
const [isFocus, setIsFocus] = useState(false)
return (
<div
Expand Down Expand Up @@ -112,13 +124,6 @@ export const TextArea = forwardRef<
rest.onBlur?.(e)
}}
{...inputProps}
onKeyDown={(e) => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
onCmdEnter?.(e)
}
rest.onKeyDown?.(e)
inputProps.onKeyDown?.(e)
}}
/>

{children}
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/common/use-input-composition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ export const useInputComposition = (

const handleKeyDown: React.KeyboardEventHandler<any> = useCallback(
(e) => {
onKeyDown?.(e)

// 中文正在输入时,不响应 keydown 事件
if (isCompositionRef.current) {
e.stopPropagation()
return
}
onKeyDown?.(e)
},
[onKeyDown],
)
Expand Down
56 changes: 55 additions & 1 deletion src/lib/dom.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ReactEventHandler } from 'react'
import type { ReactEventHandler, RefObject } from 'react'

export const stopPropagation: ReactEventHandler<any> = (e) =>
e.stopPropagation()
Expand All @@ -23,3 +23,57 @@ export function escapeSelector(selector: string) {

export const nextFrame = (fn: () => void) =>
requestAnimationFrame(() => requestAnimationFrame(fn))

export const textareaStyles = [
'font',
'width',
'padding',
'border',
'boxSizing',
'whiteSpace',
'wordWrap',
'lineHeight',
'letterSpacing',
] as const
export const scrollTextareaToCursor = (
taRef: RefObject<HTMLTextAreaElement>,
) => {
const $ta = taRef.current
if ($ta) {
const div = document.createElement('div')
const styles = getComputedStyle($ta)
// 复制 textarea 的样式到 div
textareaStyles.forEach((style) => {
div.style[style] = styles[style]
})
div.style.position = 'absolute'
div.style.top = '-9999px'
div.style.left = '-9999px'

// 将文本插入到 div 中,并在光标位置添加一个 span
const start = $ta.selectionStart
const end = $ta.selectionEnd
const textBeforeCursor = $ta.value.substring(0, start)
const textAfterCursor = $ta.value.substring(end)
const textBeforeNode = document.createTextNode(textBeforeCursor)
const cursorNode = document.createElement('span')
cursorNode.id = 'cursor'
const textAfterNode = document.createTextNode(textAfterCursor)

div.appendChild(textBeforeNode)
div.appendChild(cursorNode)
div.appendChild(textAfterNode)
document.body.appendChild(div)

// 获取光标元素的位置
const cursorSpan = document.getElementById('cursor')
const cursorY = cursorSpan!.offsetTop
const lineHeight = parseInt(styles.lineHeight)
// 移除临时 div
document.body.removeChild(div)

// 计算滚动位置
const scrollTop = cursorY - $ta.clientHeight / 2 + lineHeight / 2
$ta.scrollTop = Math.max(0, scrollTop)
}
}
Loading