实现一个内容自带行号的textarea

5,648 阅读3分钟

需求背景

最近重构项目的设计里大量用到需要多行输入的组件(支持复制粘贴)。例如输入需要排除的IP列表,需要排除的关键词列表等。在原先的实现里直接采用了textarea,但是这次产品提出了要在每行前面自动带上序号的要求。

效果图

实现方案

在一番思考之后,想到了以下三个方案:

    1. 不用textarea,使用多个input框来模拟。在每次按下enter键之后生成下一个input框和序号。
    1. 使用textarea,在按下enter键之后改变用户输入的文本,加上序号。
    1. 使用textarea,序号所在列使用绝对定位,监听文本变化同步序号列。

简单对比一下三个方案的优劣:

方案 优势 劣势
不用考虑滚动带来的定位问题,获取结果比较方便,多个input自然对应一个结果数组 按下enter之后的事件处理要考虑当前索引的位置,要处理删除行的情况, 复制黏贴要做额外处理
不用考虑滚动带来的定位问题 获取结果时要去除行号,要处理删除行的情况,复制黏贴要做额外处理
不需要额外处理复制粘贴,只需要监听textarea的change事件 要考虑绝对定位导致的样式和滚动带来的定位问题

进过对比和尝试,决定使用方案三,下面讨论一下具体的实现

具体实现

方案三只需要通过监听textareaonChange事件,通过换行符切割出结果数组,再根据数组生成出序号列就行了。难点在于如何让序号列的每一行看起来和textarea的每一行处于同一行

textarea的每行文本高度仅能通过line-height来设置,因此需要将绝对定位的序号列的行高和textarea的行高设为等值。

通过textareapadding-left空出位置来容纳序号所在列。

监听textareaonScroll事件给序号所在列动态设置top值来同步滚动。

提一下换行符在js里的转义字符为\n

代码

import React, { useState, FunctionComponent } from "react";
import { Input } from "antd"
import { throttle } from "lodash"

const { TextArea } = Input

export interface IAreaFormProps {
    defaultList?: string[];
    onChange?: (list: string[]) => void;
}

const AreaForm: FunctionComponent<IAreaFormProps> = (props) => {
    const { defaultList = [], onChange } = props;

    const [list, setList] = useState(defaultList);

    const [areaValue, setAreaValue] = useState(defaultList.join("\n"))

    const [top, setTop] = useState(0)

    const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
        const value = event.currentTarget.value
        const list = value ? value.split("\n") : []
        onChange && onChange(list)
        setList(list)
        setAreaValue(value)
    }


    const handleScroll = (event: React.UIEvent<HTMLTextAreaElement>) => {
        setTop(-event.currentTarget.scrollTop)
    }


    return (
        <div className="area-form">
            <div className="area-form__content">
                <ul className="area-form__index" style={{ top: top + 11 }}>
                    {
                        list.map((_, index) => (
                            <li key={index}>
                                <span>{index + 1}、</span>
                            </li>
                        ))
                    }
                </ul>
                <TextArea autoFocus onScroll={throttle(handleScroll)} value={areaValue} onChange={e => handleChange(e)} autoComplete="off"></TextArea>
            </div>
        </div>
    )

}

export default AreaForm;

如果大家有更好的解决方案,欢迎评论区告知。