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

React中用keypress事件实现Slack风格emoji时如何获取冒号后文本

Slack风格Emoji选择器实现方案

核心修改点

  • 新增3个状态:emoji触发起始下标、当前搜索关键词、筛选后的emoji列表
  • 新增光标位置获取逻辑,支持在文本任意位置触发emoji选择
  • 优化触发/关闭逻辑,避免误触发
  • 实现emoji快速插入,自动替换关键词为对应emoji

完整实现代码

import React, { Component, useRef, useState } from 'react';
import { render } from 'react-dom';
import ES from 'emoji-set';

const TextArea = () => {
  const [text, setText] = useState('');
  const [openEmoji, setOpenEmoji] = useState(false);
  const [emojiStartIndex, setEmojiStartIndex] = useState(-1);
  const [filteredEmojis, setFilteredEmojis] = useState([]);
  const textareaRef = useRef<HTMLTextAreaElement>(null);

  // 关闭emoji选择器重置状态
  const closeEmojiPicker = () => {
    setOpenEmoji(false);
    setEmojiStartIndex(-1);
    setFilteredEmojis([]);
  };

  const keyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
    const cursorPos = e.currentTarget.selectionStart;
    // 按ESC直接关闭
    if (e.key === 'Escape') {
      closeEmojiPicker();
      return;
    }
    // 按回车如果emoji选择器开着,默认选第一个,可根据需求改
    if (e.key === 'Enter' && openEmoji && filteredEmojis.length) {
      e.preventDefault();
      insertEmoji(filteredEmojis[0].char);
      return;
    }
    // 触发emoji选择器
    if (e.key === ':') {
      setOpenEmoji(true);
      setEmojiStartIndex(cursorPos);
      // 刚输入:的时候关键词为空,可返回前20个热门emoji避免全量加载
      setFilteredEmojis(ES.getAll().slice(0, 20));
      return;
    }
    // 回删到:之前关闭选择器
    if (e.key === 'Backspace' && cursorPos <= emojiStartIndex + 1) {
      closeEmojiPicker();
      return;
    }
    // 按空格关闭选择器
    if (e.key === ' ') {
      closeEmojiPicker();
      return;
    }
  };

  const textChanged = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    const newText = e.target.value;
    setText(newText);

    if (!openEmoji || emojiStartIndex < 0) return;
    
    const cursorPos = textareaRef.current?.selectionStart || newText.length;
    // 提取:后面的搜索关键词
    const searchKey = newText.slice(emojiStartIndex + 1, cursorPos).trim();
    
    if (!searchKey) {
      // 关键词为空返回前20个常用emoji,避免全量加载
      setFilteredEmojis(ES.getAll().slice(0, 20));
      return;
    }
    // 匹配emoji关键词
    const matched = ES.getKeywords(searchKey).map(key => ({
      keyword: key,
      char: ES.get(key)
    }));
    setFilteredEmojis(matched);
  };

  // 插入选中的emoji
  const insertEmoji = (emojiChar: string) => {
    const cursorPos = textareaRef.current?.selectionStart || text.length;
    const newText = text.slice(0, emojiStartIndex) + emojiChar + text.slice(cursorPos);
    setText(newText);
    closeEmojiPicker();
    // 光标移到emoji后面
    setTimeout(() => {
      textareaRef.current?.setSelectionRange(emojiStartIndex + 1, emojiStartIndex + 1);
    }, 0);
  };

  return (
    <div style={{ position: 'relative' }}>
      <textarea
        ref={textareaRef}
        value={text}
        onKeyDown={keyDown}
        onChange={textChanged}
        placeholder="Type here..."
        spellCheck={false}
        style={{
          width: '350px',
          height: '50vh'
        }}
      />
      {/* emoji选择器浮层 */}
      {openEmoji && filteredEmojis.length > 0 && (
        <div style={{
          position: 'absolute',
          bottom: 'calc(50vh + 10px)',
          left: 0,
          border: '1px solid #e0e0e0',
          borderRadius: '4px',
          background: '#fff',
          maxHeight: '200px',
          overflowY: 'auto',
          width: '200px',
          zIndex: 999
        }}>
          {filteredEmojis.map((emoji, idx) => (
            <div
              key={idx}
              onClick={() => insertEmoji(emoji.char)}
              style={{
                padding: '6px 12px',
                cursor: 'pointer',
                display: 'flex',
                gap: '8px',
                alignItems: 'center'
              }}
              onMouseOver={(e) => e.currentTarget.style.background = '#f5f5f5'}
              onMouseOut={(e) => e.currentTarget.style.background = 'transparent'}
            >
              <span>{emoji.char}</span>
              <span style={{ fontSize: '12px', color: '#666' }}>{emoji.keyword}</span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
};

class App extends Component {
  render() {
    return (
      <div>
        <h1>React Slack Emoji</h1>
        <TextArea />
      </div>
    );
  }
}

render(<App />, document.getElementById('root'));

性能优化说明

  • 只有触发emoji选择器时才执行搜索逻辑,平时输入无额外性能消耗
  • 空关键词时默认只返回前20个常用emoji,避免全量emoji渲染卡顿
  • emoji匹配使用emoji-set内置的原生方法,性能远高于自定义遍历匹配
  • 如果emoji列表需求更大,可额外添加虚拟滚动组件进一步优化渲染性能

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.11 02:51:02