Div 模拟 Textarea

使用 contenteditable 属性实现可编辑的文本输入框

问题

使用 div 模拟 textarea 实现,支持 placeholder、自适应高度等功能。

解答

基础实现

<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>Div 模拟 Textarea</title>
  <style>
    .fake-textarea {
      min-height: 100px;
      max-height: 300px;
      padding: 10px;
      border: 1px solid #ccc;
      border-radius: 4px;
      overflow-y: auto;
      outline: none;
      line-height: 1.5;
      word-break: break-all;
    }

    .fake-textarea:focus {
      border-color: #409eff;
    }

    /* placeholder 样式 */
    .fake-textarea:empty::before {
      content: attr(data-placeholder);
      color: #999;
    }

    /* 聚焦时隐藏 placeholder */
    .fake-textarea:focus::before {
      content: none;
    }
  </style>
</head>
<body>
  <div 
    class="fake-textarea" 
    contenteditable="true"
    data-placeholder="请输入内容..."
  ></div>

  <script>
    const editor = document.querySelector('.fake-textarea');

    // 获取纯文本内容
    function getText() {
      return editor.innerText;
    }

    // 设置内容
    function setText(value) {
      editor.innerText = value;
    }

    // 监听输入
    editor.addEventListener('input', () => {
      console.log('内容:', getText());
    });

    // 阻止粘贴富文本,只保留纯文本
    editor.addEventListener('paste', (e) => {
      e.preventDefault();
      const text = e.clipboardData.getData('text/plain');
      document.execCommand('insertText', false, text);
    });
  </script>
</body>
</html>

封装成组件

class FakeTextarea {
  constructor(container, options = {}) {
    this.options = {
      placeholder: '请输入...',
      maxLength: Infinity,
      onChange: null,
      ...options
    };
    
    this.el = document.createElement('div');
    this.el.className = 'fake-textarea';
    this.el.contentEditable = true;
    this.el.dataset.placeholder = this.options.placeholder;
    
    container.appendChild(this.el);
    this.bindEvents();
  }

  bindEvents() {
    // 输入事件
    this.el.addEventListener('input', () => {
      // 限制最大长度
      if (this.getText().length > this.options.maxLength) {
        this.setText(this.getText().slice(0, this.options.maxLength));
        this.moveCursorToEnd();
      }
      this.options.onChange?.(this.getText());
    });

    // 粘贴纯文本
    this.el.addEventListener('paste', (e) => {
      e.preventDefault();
      const text = e.clipboardData.getData('text/plain');
      document.execCommand('insertText', false, text);
    });
  }

  // 获取内容
  getText() {
    return this.el.innerText;
  }

  // 设置内容
  setText(value) {
    this.el.innerText = value;
  }

  // 清空内容
  clear() {
    this.el.innerHTML = '';
  }

  // 聚焦
  focus() {
    this.el.focus();
    this.moveCursorToEnd();
  }

  // 光标移到末尾
  moveCursorToEnd() {
    const range = document.createRange();
    const selection = window.getSelection();
    range.selectNodeContents(this.el);
    range.collapse(false);
    selection.removeAllRanges();
    selection.addRange(range);
  }
}

// 使用
const textarea = new FakeTextarea(document.body, {
  placeholder: '请输入内容...',
  maxLength: 200,
  onChange: (text) => console.log(text)
});

处理换行问题

// 不同浏览器 contenteditable 换行行为不一致
// 统一使用 <br> 换行
editor.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') {
    e.preventDefault();
    document.execCommand('insertLineBreak');
  }
});

// 获取内容时处理换行
function getText() {
  // 将 <br> 和 <div> 转换为换行符
  return editor.innerHTML
    .replace(/<br\s*\/?>/gi, '\n')
    .replace(/<\/div><div>/gi, '\n')
    .replace(/<\/?div>/gi, '')
    .replace(/&nbsp;/g, ' ')
    .trim();
}

关键点

  • contenteditable="true" 使元素可编辑
  • 使用 :empty::before 伪元素实现 placeholder
  • 粘贴时用 e.clipboardData.getData('text/plain') 获取纯文本
  • innerText 获取纯文本,innerHTML 获取带标签内容
  • 不同浏览器换行行为不同,需要统一处理