Skip to content

Commit db4a859

Browse files
authored
fix: add default two line breaks per line break in comment Textarea
#410 && 评论系统中添加每次换行默认换两行 (#411) * fix: fix #410 && 评论系统中添加每次换行默认换两行 * fix: fix ci error * fix: 修复 cursorSpan 为 null 的问题
1 parent 36d4d69 commit db4a859

File tree

4 files changed

+124
-33
lines changed

4 files changed

+124
-33
lines changed

src/components/modules/comment/CommentBox/UniversalTextArea.tsx

+54-22
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useIsMobile } from '~/atoms/hooks'
88
import { FloatPopover } from '~/components/ui/float-popover'
99
import { TextArea } from '~/components/ui/input'
1010
import { useRefValue } from '~/hooks/common/use-ref-value'
11+
import { scrollTextareaToCursor } from '~/lib/dom'
1112

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

2829
const taRef = useRef<HTMLTextAreaElement>(null)
29-
const handleInsertEmoji = useCallback((emoji: string) => {
30-
if (!taRef.current) {
31-
return
32-
}
30+
const handleInsertEmoji = useCallback(
31+
(emoji: string) => {
32+
if (!taRef.current) {
33+
return
34+
}
3335

34-
const $ta = taRef.current
35-
const start = $ta.selectionStart
36-
const end = $ta.selectionEnd
37-
38-
$ta.value = `${$ta.value.substring(
39-
0,
40-
start,
41-
)} ${emoji} ${$ta.value.substring(end, $ta.value.length)}`
42-
43-
setter('text', $ta.value)
44-
requestAnimationFrame(() => {
45-
const shouldMoveToPos = start + emoji.length + 2
46-
$ta.selectionStart = shouldMoveToPos
47-
$ta.selectionEnd = shouldMoveToPos
48-
49-
$ta.focus()
50-
})
51-
}, [])
36+
const $ta = taRef.current
37+
const start = $ta.selectionStart
38+
const end = $ta.selectionEnd
39+
40+
$ta.value = `${$ta.value.substring(
41+
0,
42+
start,
43+
)} ${emoji} ${$ta.value.substring(end, $ta.value.length)}`
44+
45+
setter('text', $ta.value)
46+
requestAnimationFrame(() => {
47+
const shouldMoveToPos = start + emoji.length + 2
48+
$ta.selectionStart = shouldMoveToPos
49+
$ta.selectionEnd = shouldMoveToPos
50+
51+
$ta.focus()
52+
})
53+
},
54+
[setter],
55+
)
56+
57+
const handleKeyDown = useCallback(
58+
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
59+
if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey) {
60+
e.preventDefault()
61+
const $ta = taRef.current
62+
if ($ta) {
63+
const start = $ta.selectionStart
64+
const end = $ta.selectionEnd
65+
const textBefore = $ta.value.substring(0, start)
66+
const textAfter = $ta.value.substring(end)
67+
$ta.value = `${textBefore}\n\n${textAfter}`
68+
setter('text', $ta.value)
69+
70+
requestAnimationFrame(() => {
71+
const shouldMoveToPos = start + 2
72+
$ta.selectionStart = shouldMoveToPos
73+
$ta.selectionEnd = shouldMoveToPos
74+
$ta.focus()
75+
// 上面设置的光标,可能不在可见区域内,因此 scroll 到光标所在位置
76+
scrollTextareaToCursor(taRef)
77+
})
78+
}
79+
}
80+
},
81+
[setter],
82+
)
5283

5384
useEffect(() => {
5485
const $ta = taRef.current
@@ -80,6 +111,7 @@ export const UniversalTextArea: Component = ({ className }) => {
80111
wrapperClassName={className}
81112
ref={taRef}
82113
defaultValue={value}
114+
onKeyDown={handleKeyDown}
83115
onChange={(e) => setter('text', e.target.value)}
84116
placeholder={placeholder}
85117
onCmdEnter={(e) => {

src/components/ui/input/TextArea.tsx

+13-8
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const TextArea = forwardRef<
4242
rounded = 'xl',
4343
bordered = true,
4444
onCmdEnter,
45+
onKeyDown,
4546
...rest
4647
} = props
4748
const mouseX = useMotionValue(0)
@@ -54,9 +55,20 @@ export const TextArea = forwardRef<
5455
},
5556
[mouseX, mouseY],
5657
)
58+
const handleKeyDown = useCallback(
59+
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
60+
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
61+
onCmdEnter?.(e)
62+
}
63+
onKeyDown?.(e)
64+
},
65+
[onCmdEnter, onKeyDown],
66+
)
5767
const background = useMotionTemplate`radial-gradient(320px circle at ${mouseX}px ${mouseY}px, var(--spotlight-color) 0%, transparent 85%)`
5868
const isMobile = useIsMobile()
59-
const inputProps = useInputComposition(props)
69+
const inputProps = useInputComposition(
70+
Object.assign({}, props, { onKeyDown: handleKeyDown }),
71+
)
6072
const [isFocus, setIsFocus] = useState(false)
6173
return (
6274
<div
@@ -112,13 +124,6 @@ export const TextArea = forwardRef<
112124
rest.onBlur?.(e)
113125
}}
114126
{...inputProps}
115-
onKeyDown={(e) => {
116-
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
117-
onCmdEnter?.(e)
118-
}
119-
rest.onKeyDown?.(e)
120-
inputProps.onKeyDown?.(e)
121-
}}
122127
/>
123128

124129
{children}

src/hooks/common/use-input-composition.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ export const useInputComposition = (
3636

3737
const handleKeyDown: React.KeyboardEventHandler<any> = useCallback(
3838
(e) => {
39-
onKeyDown?.(e)
40-
39+
// 中文正在输入时,不响应 keydown 事件
4140
if (isCompositionRef.current) {
4241
e.stopPropagation()
4342
return
4443
}
44+
onKeyDown?.(e)
4545
},
4646
[onKeyDown],
4747
)

src/lib/dom.ts

+55-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ReactEventHandler } from 'react'
1+
import type { ReactEventHandler, RefObject } from 'react'
22

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

2424
export const nextFrame = (fn: () => void) =>
2525
requestAnimationFrame(() => requestAnimationFrame(fn))
26+
27+
export const textareaStyles = [
28+
'font',
29+
'width',
30+
'padding',
31+
'border',
32+
'boxSizing',
33+
'whiteSpace',
34+
'wordWrap',
35+
'lineHeight',
36+
'letterSpacing',
37+
] as const
38+
export const scrollTextareaToCursor = (
39+
taRef: RefObject<HTMLTextAreaElement>,
40+
) => {
41+
const $ta = taRef.current
42+
if ($ta) {
43+
const div = document.createElement('div')
44+
const styles = getComputedStyle($ta)
45+
// 复制 textarea 的样式到 div
46+
textareaStyles.forEach((style) => {
47+
div.style[style] = styles[style]
48+
})
49+
div.style.position = 'absolute'
50+
div.style.top = '-9999px'
51+
div.style.left = '-9999px'
52+
53+
// 将文本插入到 div 中,并在光标位置添加一个 span
54+
const start = $ta.selectionStart
55+
const end = $ta.selectionEnd
56+
const textBeforeCursor = $ta.value.substring(0, start)
57+
const textAfterCursor = $ta.value.substring(end)
58+
const textBeforeNode = document.createTextNode(textBeforeCursor)
59+
const cursorNode = document.createElement('span')
60+
cursorNode.id = 'cursor'
61+
const textAfterNode = document.createTextNode(textAfterCursor)
62+
63+
div.appendChild(textBeforeNode)
64+
div.appendChild(cursorNode)
65+
div.appendChild(textAfterNode)
66+
document.body.appendChild(div)
67+
68+
// 获取光标元素的位置
69+
const cursorSpan = document.getElementById('cursor')
70+
const cursorY = cursorSpan!.offsetTop
71+
const lineHeight = parseInt(styles.lineHeight)
72+
// 移除临时 div
73+
document.body.removeChild(div)
74+
75+
// 计算滚动位置
76+
const scrollTop = cursorY - $ta.clientHeight / 2 + lineHeight / 2
77+
$ta.scrollTop = Math.max(0, scrollTop)
78+
}
79+
}

0 commit comments

Comments
 (0)