文本配音 富文本实现
背景
文本驱动,需要实现用户输入文本中穿插 自定义的HTML 片段。需要使用富文本来实现。简单的input 输入框满足不了业务需求。项目目前用的还是vue2 的框架
思路
受控富文本比较难处理,异常case 比较多,首先考虑的还是开源的富文本组件。
- draft (react 项目)
- slate (react 项目)
- Quill (实现了自定义embed ,最后还是放弃这种方案)
- tinyMCE (功能过剩,也不太符合,业务自定义难度大)
富文本参考 https://juejin.cn/post/7084046542994145294
Quill
Quill 实现了受控,光标位置管理,以及数据流model 模型 。刚开始看起来是比较吻合业务需求的。 问题:
- 实现了自定义 Embed 块之后。插入自定义embed 块总是换行了,最后没找到解决方案
- 统计字数不准,业务涉及到输入文本字符串数量限制,其中有插入新字符但是不做字数统计的需求,但是quill 中没法很好实现
最后还是选择手动实现。通过contenteditable=true 来实现 解决方案 受控组件,则需要把用户输入转化成 json 最后通过json 再渲染到富文本编辑框中 [图片] 获取用户输入文本,渲染到富文本编辑框 需要监听 beforeinput 时间,并且 阻止默认事件
<div :contenteditable="true" @beforeinput="beforeInput" @input="handleInput"></div>
获取用户输入,转成受控文本
beforeInput(e) {
e.preventDefault();
const { data, inputType } = e;
this.json.push({
type: data ? 'text' : 'linebreak',
text: data,
uid: Date.now(),
});
},
渲染出用户输入文本
<!-- 受控文本展示 -->
<template v-for="item in json">
<span v-if="item.type == 'text'" :key="item.uid">{{ item.text }}</span>
<div v-else-if="item.type == 'linebreak'" :key="item.uid"></div>
</template>
受控输入后重新渲染,导致光标跳到第一个字符
// 需要保证渲染后再重制光标位置
this.$nextTick(() => {
moveCursorToEnd(e.target);
});
// element 元素为 富文本根组件 contenteditable 属性的元素
export function moveCursorToEnd(element) {
const range = document.createRange();
const selection = window.getSelection();
range.selectNodeContents(element);
range.collapse(false); // 将光标定位到节点内容的末尾
selection.removeAllRanges();
selection.addRange(range);
}
输入多个空格后,vue中只展示一个,并且光标不会向后移动 渲染的时候增加 pre-wrap 样式
<span v-if="item.type == 'text'" :key="item.uid" style="white-space: pre-wrap">{{ item.value }}</span>
Js 处理文本的时候,直接空格追加到文本后
lastEl.value += " ";
删除元素
没实现 ❌❌❌ Beforeinput 里面控制光标位置,找到要删除的字符是哪个,并且手动维护好光标位置???????? 找到要删的受控元素 维护光标位置
字数限制
Input 事件 Input 事件是在用户输入事件完成后触发的,这个时候文本已经输入到富文本中了。只能获取到文本,再重新修改 innerHTML 内容 弊: 富文本中元素比较复杂的话,需要遍历完整个文档去统计字数,再删除不需要的内容,比较繁琐,最好是输入前判断,直接不让输入
handleInput(e) {
// console.log(e);
// e.preventDefault();
// 输入结束后修改
editor.innerHTML = editor.innerHTML.slice(0,100 )
}
beforeinput
事件 beforeinput
事件中 e.preventDefault();
能阻止输入事件 但是不能阻止输入中文,输入中文会触发 insertCompositionText
事件 insertCompositionText
事件中 preventDefault()
没有效果
compositionUpdate(e) {
console.log('compositionUpdate', e);
this.checkWordCount(e);
},
compositionStart(e) {
console.log('compositionstart', e);
this.checkWordCount(e);
},
beforeInput(e) {
const { data, inputType } = e;
console.log('beforeinput', inputType, data);
switch (inputType) {
case 'deleteContentBackward':
break;
case 'deleteContentForward':
break;
case 'insertText':
if (this.wordCount >= this.maxLength) {
e.preventDefault();
}
break;
case 'insertCompositionText': // beforeinput 阻止不了改事件,该事件在 beforeinput 之前触发
if (this.wordCount >= this.maxLength) {
e.preventDefault();
}
break;
case 'insertParagraph':
if (this.wordCount >= this.maxLength) {
e.preventDefault();
}
default:
e.preventDefault();
break;
}
},
Input 事件获取到dom 转成json 问题: 文字重复,需要清理掉innerHTML 里面原始的内容