You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用HTML/Javascript实现自定义项目符号文本框?

自定义项目符号文本框实现方案(解决跨选问题)

需求梳理

  • 初始状态显示单个项目符号
  • 每行以项目符号开头,按Enter自动换行并添加项目符号
  • 在仅含项目符号的空行按Backspace,删除该行并回到上一行末尾
  • 按Tab缩进项目符号,Shift+Tab取消缩进,且仅可缩进一次
  • 支持文本粘贴

现有实现痛点

当前实现满足所有功能,但无法跨多行选中文本进行批量操作(比如批量删除需要逐行处理),核心原因是采用了多行独立的可编辑区域,破坏了原生选区的跨行支持。

改进后的完整代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>仿Word项目符号文本框</title>
    <style>
        .bullet-editor {
            border: 1px solid #ccc;
            min-height: 120px;
            padding: 10px;
            font-size: 16px;
            line-height: 1.5;
        }
        .bullet-line {
            position: relative;
            padding-left: 20px;
        }
        .bullet-line::before {
            content: "•";
            position: absolute;
            left: 0;
            user-select: none;
        }
        .bullet-line.indent::before {
            content: "◦";
            left: 20px;
        }
    </style>
</head>
<body>
    <div class="bullet-editor" contenteditable="true" id="editor">
        <div class="bullet-line"><br></div>
    </div>

    <script>
        const editor = document.getElementById('editor');

        // 初始化编辑器事件监听
        editor.addEventListener('keydown', handleKeydown);
        editor.addEventListener('input', handleInput);
        editor.addEventListener('paste', handlePaste);

        function handleKeydown(e) {
            const selection = window.getSelection();
            const currentLine = getCurrentLine(selection.anchorNode);

            switch(e.key) {
                case 'Enter':
                    e.preventDefault();
                    insertNewLine(currentLine);
                    break;
                case 'Backspace':
                    handleBackspace(e, currentLine, selection);
                    break;
                case 'Tab':
                    e.preventDefault();
                    handleTab(e, currentLine);
                    break;
            }
        }

        function handleInput() {
            // 确保空行保留<br>,避免光标错位
            const lines = editor.querySelectorAll('.bullet-line');
            lines.forEach(line => {
                if (line.textContent.trim() === '' && !line.querySelector('br')) {
                    line.innerHTML = '<br>';
                }
            });
        }

        function handlePaste(e) {
            e.preventDefault();
            const text = (e.clipboardData || window.clipboardData).getData('text');
            const lines = text.split(/\r?\n/).filter(line => line.trim() !== '');
            
            const currentLine = getCurrentLine(window.getSelection().anchorNode);
            const currentText = currentLine.textContent.trim();
            
            // 替换当前行内容,插入剩余行
            currentLine.textContent = currentText + lines[0];
            let lastLine = currentLine;
            for(let i = 1; i < lines.length; i++) {
                const newLine = createNewLine();
                newLine.textContent = lines[i];
                editor.insertBefore(newLine, lastLine.nextSibling);
                lastLine = newLine;
            }
            
            // 将光标移到最后一行末尾
            moveCursorToEnd(lastLine);
        }

        function getCurrentLine(node) {
            while(node && !node.classList?.contains('bullet-line')) {
                node = node.parentNode;
            }
            return node || editor.firstElementChild;
        }

        function createNewLine(indent = false) {
            const line = document.createElement('div');
            line.classList.add('bullet-line');
            if(indent) line.classList.add('indent');
            line.innerHTML = '<br>';
            return line;
        }

        function insertNewLine(prevLine) {
            const isIndent = prevLine.classList.contains('indent');
            const newLine = createNewLine(isIndent);
            editor.insertBefore(newLine, prevLine.nextSibling);
            moveCursorToEnd(newLine);
        }

        function handleBackspace(e, currentLine, selection) {
            // 处理空行删除
            if(currentLine.textContent.trim() === '' && editor.children.length > 1) {
                e.preventDefault();
                const prevLine = currentLine.previousElementSibling;
                editor.removeChild(currentLine);
                moveCursorToEnd(prevLine);
                return;
            }

            // 处理行首删除(非首行)
            const range = selection.getRangeAt(0);
            if(range.startOffset === 0 && currentLine !== editor.firstElementChild) {
                e.preventDefault();
                const prevLine = currentLine.previousElementSibling;
                prevLine.textContent += currentLine.textContent.trim();
                editor.removeChild(currentLine);
                moveCursorToEnd(prevLine);
            }
        }

        function handleTab(e, currentLine) {
            if(currentLine === editor.firstElementChild) return;
            
            if(e.shiftKey) {
                currentLine.classList.remove('indent');
            } else {
                if(!currentLine.classList.contains('indent')) {
                    currentLine.classList.add('indent');
                }
            }
        }

        function moveCursorToEnd(element) {
            const selection = window.getSelection();
            const range = document.createRange();
            range.selectNodeContents(element);
            range.collapse(false);
            selection.removeAllRanges();
            selection.addRange(range);
            element.focus();
        }
    </script>
</body>
</html>

关键改进说明

  1. 原生选区支持:将编辑器整体设为contenteditable="true",不再拆分独立可编辑区域,原生支持跨多行选中文本,批量删除、复制等操作完全正常。
  2. 动态维护项目符号:通过CSS伪元素::before渲染项目符号,避免手动维护DOM结构,减少复杂度。
  3. 快捷键逻辑优化:保留原生键位行为的同时,仅拦截需要自定义处理的场景(如Enter换行添加项目符号、Backspace删除空行)。
  4. 粘贴处理增强:自动将粘贴的多行文本拆分为带项目符号的独立行,保持格式一致。

内容的提问来源于stack exchange,提问作者Noah Bruckner

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 19:32:02