携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情
先说明一下项目需求,PC做了一个客服系统,聊天框用textare进行文本输入,但是后续需要对接微信公众号,需要发送微信表情,以及复制粘贴图片等功能。
所以,尽管我很不愿意,还是只能用contenteditable来实现一个简单的富文本编辑器。(基于vue3实现)
<div
class="rich-textarea-content"
v-bind="$attrs"
ref="textDom"
id="rsvp-textarea"
contenteditable
:disabled="disabled"
:placeholder="placeholder"
@keyup.stop="keyup"
@keydown.stop="keydown"
@blur="$emit('blur')"
></div>
先看看样子
但是实现过程中,还是有很多的坑。
1.placeholder提示以及disabled
disabled的实现比较简单,只需要根据disabled状态更改contenteditable的值就行。
至于样式,可以通过属性选择器来实现。
[disabled="true"] {
background-color: #f5f7fa;
cursor: not-allowed;
}
[disabled="false"] {
background-color: #fff;
cursor: default;
}
placeholder根据传入参数,动态改变,这个一直没思路。
后来发通过attr,给伪元素的content赋值,来实现。(但是..., 这个在ie中,光标会有点不正常,话说ie官方都不维护了,还兼容它干啥)
&:empty {
&::before {
content: attr(placeholder);
color: gray;
}
}
2.设置初始值并将光标移到最后
设置初始值可以使用 document.execCommand("insertHTML", true, ''),也可以直接设置innerText。
注意:(execCommand方法,需要编辑框处于focus状态下才能使用,如果需要点击操作栏,而又不想编辑框失去焦点,可以拦截mousedown事件)
移动光标到最后
moveEnd() {
var selection = window.getSelection();
var range: any = document.createRange();
range.selectNodeContents(this.$refs.textDom as HTMLDivElement);
range.collapse(false);
selection && selection.removeAllRanges();
selection && selection.addRange(range);
},
3.监听粘贴事件
(this.$refs.textDom as HTMLDivElement).addEventListener(
"paste",
async (e) => {
e.preventDefault();
e.stopPropagation();
let items = e.clipboardData?.items || [];
for (let i = 0; i < items?.length; i++) {
const item = items[i];
if (item.kind === "string" && item.type === "text/plain") {
item.getAsString(function (str) {
document.execCommand("insertHTML", true, str);
});
} else if (
item.kind === "file" &&
item.type.indexOf("image") !== -1 &&
!this.isAccount
) {
const pasteFile = item.getAsFile();
if (pasteFile && pasteFile.size < 4 * 1024 * 1024) {
const form = new FormData();
form.append("file", pasteFile);
// 可以上传接口,也可以直接转base64
document.execCommand(
"insertHTML",
true,
`<img src=${url} style="max-width: 240px; max-height: 240px;vertical-align: bottom"></img>`
);
}
}
}
}
);
4.文本获取
发送的时候,如果有图片,需要以图片位置为分割,分为多条消息发送。
比如这样的一张图,需要分成4条消息发送 1.表情你好 2.图片1 3.呵呵 4.图片2
所以获取内容不能单纯的获取innerText。
实现方式为: 通过遍历childNodes判断分割位置,以及内容。
const dom = this.$refs.textDom as HTMLDivElement;
let m = "";
let arr: any = [];
for (let i = 0; i < dom.childNodes.length; i++) {
// console.log("dom", dom[i]);
if (dom.childNodes[i].nodeName === "IMG") {
const c = dom.childNodes[i] as HTMLImageElement;
// 说明一下,这里加了个表情的判断,因为输入框的表情其实也是图片,所以需要转成微信能识别的编码
if (c.className.indexOf("qqface") !== -1) {
// @ts-ignore
const index = c.attributes.data.value;
m += Object.keys(wexinEmo)[index];
} else {
if (m) {
arr.push({
type: "text",
content: m,
});
}
m = "";
arr.push({
type: "image",
content: (dom.childNodes[i] as any).currentSrc,
});
}
} else {
m +=
dom.childNodes[i].nodeName === "#text"
? (dom.childNodes[i] as any).data
: (dom.childNodes[i] as any).innerText;
}
}
if (m) {
arr.push({
type: "text",
content: m,
});
}