234 lines
6.0 KiB
Vue
234 lines
6.0 KiB
Vue
|
|
<template>
|
||
|
|
<view class="simple_editor_wrap">
|
||
|
|
<textarea
|
||
|
|
class="simple_editor_textarea"
|
||
|
|
:value="textValue"
|
||
|
|
:placeholder="placeholder"
|
||
|
|
:auto-height="true"
|
||
|
|
:maxlength="maxlength"
|
||
|
|
:show-confirm-bar="false"
|
||
|
|
:hold-keyboard="true"
|
||
|
|
:adjust-position="false"
|
||
|
|
@input="onInput"
|
||
|
|
@focus="onFocus"
|
||
|
|
@blur="onBlur"
|
||
|
|
:selection-start="cursorPos"
|
||
|
|
:selection-end="cursorPos"
|
||
|
|
:id="textareaId"
|
||
|
|
/>
|
||
|
|
</view>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
import GraphemeSplitter from 'grapheme-splitter';
|
||
|
|
|
||
|
|
export default {
|
||
|
|
props: {
|
||
|
|
placeholder: {
|
||
|
|
type: String,
|
||
|
|
default: "",
|
||
|
|
},
|
||
|
|
value: {
|
||
|
|
type: String,
|
||
|
|
default: "",
|
||
|
|
},
|
||
|
|
maxlength: {
|
||
|
|
type: Number,
|
||
|
|
default: -1,
|
||
|
|
}
|
||
|
|
},
|
||
|
|
data() {
|
||
|
|
return {
|
||
|
|
textValue: "",
|
||
|
|
cursorPos: -1, // 光标位置
|
||
|
|
textareaId: "simple_editor_" + Date.now(), // 唯一ID
|
||
|
|
};
|
||
|
|
},
|
||
|
|
watch: {
|
||
|
|
value: {
|
||
|
|
handler(newVal) {
|
||
|
|
if (newVal !== this.textValue) {
|
||
|
|
this.textValue = newVal;
|
||
|
|
}
|
||
|
|
},
|
||
|
|
immediate: true
|
||
|
|
}
|
||
|
|
},
|
||
|
|
methods: {
|
||
|
|
onInput(e) {
|
||
|
|
this.textValue = e.detail.value;
|
||
|
|
// 更新光标位置
|
||
|
|
this.cursorPos = e.detail.cursor || this.textValue.length;
|
||
|
|
this.$emit("input", {
|
||
|
|
detail: {
|
||
|
|
value: this.textValue,
|
||
|
|
text: this.textValue,
|
||
|
|
html: this.textValue // 简单编辑器,HTML 就是文本
|
||
|
|
}
|
||
|
|
});
|
||
|
|
},
|
||
|
|
onFocus(e) {
|
||
|
|
this.$emit("focus", e);
|
||
|
|
},
|
||
|
|
onBlur(e) {
|
||
|
|
// 保存光标位置
|
||
|
|
this.cursorPos = e.detail.cursor || this.textValue.length;
|
||
|
|
this.$emit("blur", e);
|
||
|
|
},
|
||
|
|
// 插入文本(表情或普通文本)
|
||
|
|
insertText(text, successFn, errFn) {
|
||
|
|
try {
|
||
|
|
// 获取当前光标位置(从 textarea 获取)
|
||
|
|
this.getCursorPosition((currentPos) => {
|
||
|
|
// 在光标位置插入文本
|
||
|
|
const beforeText = this.textValue.substring(0, currentPos);
|
||
|
|
const afterText = this.textValue.substring(currentPos);
|
||
|
|
const newText = beforeText + text + afterText;
|
||
|
|
|
||
|
|
// 更新文本值
|
||
|
|
this.textValue = newText;
|
||
|
|
|
||
|
|
// 更新光标位置(插入文本后)
|
||
|
|
const newCursorPos = currentPos + text.length;
|
||
|
|
this.cursorPos = newCursorPos;
|
||
|
|
|
||
|
|
// 触发 input 事件
|
||
|
|
this.$emit("input", {
|
||
|
|
detail: {
|
||
|
|
value: newText,
|
||
|
|
text: newText,
|
||
|
|
html: newText
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// 使用 $nextTick 确保 DOM 更新后再设置光标
|
||
|
|
this.$nextTick(() => {
|
||
|
|
// 设置 textarea 的光标位置
|
||
|
|
this.setCursorPosition(newCursorPos);
|
||
|
|
|
||
|
|
if (successFn) {
|
||
|
|
successFn.call(this, [{ success: true }]);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
} catch (err) {
|
||
|
|
console.error("插入文本失败", err);
|
||
|
|
if (errFn) {
|
||
|
|
errFn.call(this, [err]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
// 获取光标位置
|
||
|
|
getCursorPosition(callback) {
|
||
|
|
// 尝试通过查询获取光标位置
|
||
|
|
// 如果无法获取,使用保存的位置或文本长度
|
||
|
|
const pos = this.cursorPos >= 0 ? this.cursorPos : this.textValue.length;
|
||
|
|
if (callback) {
|
||
|
|
callback(pos);
|
||
|
|
}
|
||
|
|
return pos;
|
||
|
|
},
|
||
|
|
// 设置光标位置
|
||
|
|
setCursorPosition(pos) {
|
||
|
|
this.cursorPos = pos;
|
||
|
|
// 尝试通过选择范围来设置光标
|
||
|
|
// 注意:uni-app 的 textarea 可能不支持动态设置 selection-start/end
|
||
|
|
// 所以这里主要是更新内部状态
|
||
|
|
},
|
||
|
|
// 清空内容
|
||
|
|
clear() {
|
||
|
|
this.textValue = "";
|
||
|
|
this.cursorPos = 0;
|
||
|
|
this.$emit("input", {
|
||
|
|
detail: {
|
||
|
|
value: "",
|
||
|
|
text: "",
|
||
|
|
html: ""
|
||
|
|
}
|
||
|
|
});
|
||
|
|
},
|
||
|
|
// 删除(退格)
|
||
|
|
delete() {
|
||
|
|
if (this.textValue.length === 0) return;
|
||
|
|
|
||
|
|
const currentPos = this.cursorPos >= 0 ? this.cursorPos : this.textValue.length;
|
||
|
|
if (currentPos <= 0) return;
|
||
|
|
|
||
|
|
const splitter = new GraphemeSplitter();
|
||
|
|
const graphemes = splitter.splitGraphemes(this.textValue);
|
||
|
|
|
||
|
|
let currentIndex = 0;
|
||
|
|
let graphemeIndex = 0;
|
||
|
|
|
||
|
|
// 找到光标位置对应的字素簇
|
||
|
|
for (; graphemeIndex < graphemes.length; graphemeIndex++) {
|
||
|
|
currentIndex += graphemes[graphemeIndex].length;
|
||
|
|
if (currentIndex >= currentPos) break;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (graphemeIndex > 0) {
|
||
|
|
// 计算被删除的字符长度
|
||
|
|
const deletedLength = graphemes[graphemeIndex - 1].length;
|
||
|
|
|
||
|
|
// 删除字素簇
|
||
|
|
graphemes.splice(graphemeIndex - 1, 1);
|
||
|
|
|
||
|
|
// 更新文本
|
||
|
|
this.textValue = graphemes.join('');
|
||
|
|
|
||
|
|
// 更新光标位置
|
||
|
|
this.cursorPos = currentPos - deletedLength;
|
||
|
|
|
||
|
|
// 触发 input 事件
|
||
|
|
this.$emit("input", {
|
||
|
|
detail: {
|
||
|
|
value: this.textValue,
|
||
|
|
text: this.textValue,
|
||
|
|
html: this.textValue
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// 使用 $nextTick 确保 DOM 更新后再设置光标
|
||
|
|
this.$nextTick(() => {
|
||
|
|
this.setCursorPosition(this.cursorPos);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
},
|
||
|
|
// 获取纯文本内容
|
||
|
|
getText() {
|
||
|
|
return this.textValue;
|
||
|
|
},
|
||
|
|
// 获取内容(兼容 editor 组件的接口)
|
||
|
|
getContents(successFn) {
|
||
|
|
if (successFn) {
|
||
|
|
successFn({
|
||
|
|
text: this.textValue,
|
||
|
|
html: this.textValue,
|
||
|
|
delta: null
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
};
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style lang="scss" scoped>
|
||
|
|
.simple_editor_wrap {
|
||
|
|
position: relative;
|
||
|
|
width: 100%;
|
||
|
|
}
|
||
|
|
|
||
|
|
.simple_editor_textarea {
|
||
|
|
width: 100%;
|
||
|
|
min-height: 30px;
|
||
|
|
max-height: 120px;
|
||
|
|
background-color: #fff;
|
||
|
|
font-size: 14px;
|
||
|
|
line-height: 1.5;
|
||
|
|
padding: 4px;
|
||
|
|
word-break: break-all;
|
||
|
|
box-sizing: border-box;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
|