演示如下
这是一个典型的字符编码和字符串位置计算问题。表情符号是 Unicode 字符,在 JavaScript 字符串中可能占用多个代码单元(code units),导致位置计算错误。
问题分析
- 表情符号是多码元字符:很多表情符号(特别是复合表情)在 JavaScript 中占用 2 个或更多码元
- slice 方法基于码元索引:
slice()方法基于 UTF-16 码元位置,而不是可见字符位置 - 光标位置计算不准确:
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;
};
关键点
- 使用
substring()而不是slice():substring()对字符串处理更安全 - 正确处理 Unicode 字符:使用
Array.from(string)来按字符而不是码元分割 - 在 UniApp 中注意平台差异:不同平台对 textarea 的 API 支持可能不同
- 使用
$nextTick确保 DOM 更新:在 Vue 更新后操作 DOM