字符编码和字符串位置计算问题 — 输入框中加入表情包的问题

70 阅读1分钟

演示如下

动画.gif 这是一个典型的字符编码和字符串位置计算问题。表情符号是 Unicode 字符,在 JavaScript 字符串中可能占用多个代码单元(code units),导致位置计算错误。

问题分析

  1. 表情符号是多码元字符:很多表情符号(特别是复合表情)在 JavaScript 中占用 2 个或更多码元
  2. slice 方法基于码元索引slice() 方法基于 UTF-16 码元位置,而不是可见字符位置
  3. 光标位置计算不准确cursorPosition.value 可能没有正确反映多码元字符的位置

解决方案

方案一:使用 Textarea 的 selection API(推荐)

const handleSelectEmoji = (emoji) => {
  const textarea = document.getElementById('your-textarea-id'); // 获取 textarea 元素
  if (!textarea) return;

  const start = textarea.selectionStart;
  const end = textarea.selectionEnd;
  const value = messageForm.content || '';

  // 在光标位置插入表情
  const before = value.substring(0, start);
  const after = value.substring(end);
  
  messageForm.content = before + emoji + after;
  
  // 更新光标位置
  setTimeout(() => {
    textarea.focus();
    const newPosition = start + emoji.length;
    textarea.setSelectionRange(newPosition, newPosition);
  }, 0);
};

方案二:在 UniApp 中正确处理(使用 ref)

<template>
  <view>
    <textarea 
      ref="textareaRef"
      v-model="messageForm.content"
      @input="onInput"
      @focus="updateCursorPosition"
      @blur="updateCursorPosition"
      @click="updateCursorPosition"
    />
    
    <!-- 表情选择器 -->
    <view class="emoji-picker">
      <text 
        v-for="emoji in emojiList" 
        :key="emoji"
        @click="handleSelectEmoji(emoji)"
        class="emoji"
      >
        {{ emoji }}
      </text>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      messageForm: {
        content: ''
      },
      cursorPosition: 0,
      emojiList: ['😀', '😂', '🤣', '😊', '🙃', '😇'] // 示例表情
    }
  },
  methods: {
    handleSelectEmoji(emoji) {
      const textarea = this.$refs.textareaRef;
      if (!textarea) return;
      
      // 在 UniApp 中,需要通过 $vm 获取原生元素
      const textareaEl = textarea.$el ? textarea.$el : textarea;
      
      // 获取当前光标位置
      this.$nextTick(() => {
        const currentValue = this.messageForm.content || '';
        const start = this.cursorPosition;
        
        const before = currentValue.substring(0, start);
        const after = currentValue.substring(start);
        
        this.messageForm.content = before + emoji + after;
        
        // 更新光标位置
        this.cursorPosition = start + emoji.length;
        
        // 确保 textarea 保持焦点
        this.$nextTick(() => {
          if (textareaEl.focus) {
            textareaEl.focus();
          }
          // 尝试设置光标位置
          this.setCursorPosition(textareaEl, this.cursorPosition);
        });
      });
    },
    
    onInput(event) {
      this.updateCursorPosition();
    },
    
    updateCursorPosition() {
      // 在 UniApp 中,可以通过定时器获取大致的光标位置
      setTimeout(() => {
        const textarea = this.$refs.textareaRef;
        if (textarea && textarea._getTextarea) {
          // 某些平台可能支持获取光标位置
          try {
            const position = textarea._getTextarea?.selectionStart || 0;
            this.cursorPosition = position;
          } catch (e) {
            console.log('无法获取精确光标位置');
          }
        }
      }, 100);
    },
    
    setCursorPosition(element, position) {
      if (element.setSelectionRange) {
        element.setSelectionRange(position, position);
      }
    }
  }
}
</script>

方案三:使用字符串迭代器正确处理 Unicode 字符

const handleSelectEmoji = (emoji) => {
  const value = messageForm.content || '';
  
  // 使用字符串迭代器正确处理 Unicode 字符
  const before = Array.from(value).slice(0, cursorPosition.value).join('');
  const after = Array.from(value).slice(cursorPosition.value).join('');
  
  console.log('before--->', before);
  console.log('emoji--->', emoji);
  console.log('after--->', after);
  
  messageForm.content = before + emoji + after;
  
  // 正确计算新位置(基于字符数而不是码元数)
  cursorPosition.value += Array.from(emoji).length;
};

关键点

  1. 使用 substring() 而不是 slice()substring() 对字符串处理更安全
  2. 正确处理 Unicode 字符:使用 Array.from(string) 来按字符而不是码元分割
  3. 在 UniApp 中注意平台差异:不同平台对 textarea 的 API 支持可能不同
  4. 使用 $nextTick 确保 DOM 更新:在 Vue 更新后操作 DOM