Skip to content

Commit

Permalink
fix: add default two line breaks per line break in comment Textarea
Browse files Browse the repository at this point in the history
 #410 && 评论系统中添加每次换行默认换两行 (#411)
* fix: fix #410 && 评论系统中添加每次换行默认换两行

* fix: fix ci error

* fix: 修复 cursorSpan 为 null 的问题
  • Loading branch information
sheben404 authored Jul 30, 2024
1 parent 36d4d69 commit db4a859
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 33 deletions.
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)
})
}
}
},
[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)
}
}

0 comments on commit db4a859

Please sign in to comment.