前言
最近更新不太频繁,是因为我最近有点累了,稍微放松了一段日子,接下来我会继续更新文章的,上一篇手撸编辑器的文章,已经简单介绍了如何使用contenteditable属性实现一个简单的编辑器,这一篇文章,我会继续完善这个编辑器,让它更加完善。
复制文件
其实,这次最难的就是文件复制该怎么弄,一开始我的想法是自己搞一个div,然后写一堆样式,再把这个div插入进去,然后我发现我的光标定位会出现问题,因为这个div一样是可编辑的,光标会到这个div的子元素上面去,之后我又想到我可以搞一个canvas,但是我尝试了一下,canvas放进去之后输入框光标不见了,且无法再定位,正当我捉急的时候,到了中午吃饭的饭点,我灵光一现,把文件展示的div的dom转化成图片不就好了吗?说干就干。
图片转dom
大家在网上这个一搜其实一大把,这里我用了一个插件库dom-to-image的库
1
| npm install dom-to-image
|
1
| import domtoimage from "dom-to-image";
|
绘制dom转化成图片
这里插入到app节点是因为dom转图片这个dom必须存在于dom树中,否则无法转化,我给app节点加了overflow:hidden,这样就不会影响到页面的显示了。处理完节点转化成图片之后,我再把这个节点删除,避免没必要的内存泄漏,这里的aov是因为我的视频文件发送类型和普通的excel,word这些发送的类型是不一样,因此我区分开了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| const fileImageMap = ref<any>({});
const insertFile = (img: string, file: any, isAov = false) => { const fileElement = document.createElement("div") as any; const random = Math.floor(Math.random() * 0xffffff).toString(); fileElement.id = random; fileElement.setAttribute( "style", "display:flex;flex-direction:column;width:72px;height:100px;align-items:center;justify-content:end;" ); fileElement.innerHTML = ` <img src=${img} style="width: 46px;height: 52px;margin-bottom:6px"/> <div style="font-size:12px;width:72px;color:#7795C2;word-break:keep-all;white-space:nowrap;overflow:hidden;text-overflow:ellipsis; text-align:center"> ${file.name} </div> `; const appDom = document.querySelector("#app") as any; appDom.appendChild(fileElement); domtoimage .toSvg(document.getElementById(random) as any) .then(function (dataUrl: string) { const imgElement = document.createElement("img") as any; imgElement.src = dataUrl; imgElement.width = 72; imgElement.height = 100; imgElement.style.margin = "0 5px"; imgElement.id = random; if (isAov) { imgElement.setAttribute("type", "aov"); } fileImageMap.value[random] = file; insertContent(imgElement.outerHTML); appDom.removeChild(fileElement); }) .catch(function (error: any) { ElMessage.error("文件转化失败"); }); };
|
完善根据光标插入内容逻辑
这一次,我完善了根据光标插入内容的格式,这样就可以根据光标位置插入内容,如果光标未在div中,就在div末尾插入内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| const insertContent = (html: string) => { let sel, range; if (window.getSelection) { sel = window.getSelection(); if (sel && sel.rangeCount === 0 && savedRange.value !== null) sel.addRange(savedRange.value); if (sel && sel.rangeCount) range = sel.getRangeAt(0); if (["", null, undefined].includes(range)) { range = keepCursorEnd(true).getRangeAt(0); } else { const contentRange = document.createRange(); contentRange.selectNode(editor.value); const compareStart = range.compareBoundaryPoints( Range.START_TO_START, contentRange ); const compareEnd = range.compareBoundaryPoints( Range.END_TO_END, contentRange ); const compare = compareStart !== -1 && compareEnd !== 1; if (!compare) range = keepCursorEnd(true).getRangeAt(0); } let input = range.createContextualFragment(html); let lastNode = input.lastChild; range.insertNode(input); if (lastNode) { range = range.cloneRange(); range.setStartAfter(lastNode); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); } } };
const keepCursorEnd = (isReturn) => { if (window.getSelection) { editor.value.focus(); let sel = window.getSelection(); sel.selectAllChildren(editor.value); sel.collapseToEnd(); if (isReturn) return sel; } };
onMounted(() => { document.addEventListener("selectionchange", handleSelectionChange, false); }); const savedRange = ref<any>(null);
const handleSelectionChange = () => { let sel = window.getSelection && window.getSelection(); if (sel && sel.rangeCount) { savedRange.value = sel.getRangeAt(0); } };
|
文件上传加载进度条
同样的,这次我给上传文件加上了进度条,用了axios的一个onUploadProgress属性,这里创建了一个msgTag的原因是因为,为了消息流畅,我需要先将消息显示在页面上,然后去发送消息,然而,这样就会不可避免造成消息重复,虽然我加了根据消息id去重的逻辑,但是我们自己推上去页面的消息是没有id的,因此,我让后端加了这个字段,并且在请求成功之后把消息id返回,这样的话我就可以根据msgTag把这个返回的消息id赋值给这条消息,这样的上拉滚动加载下一页的时候就不会出现消息重复的情况了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| const handleUploadMuch = ( param: { type: number; file: any; accept: string; url: string }[] ) => { const formData = new FormData(); const paramList: any = []; param.forEach((item) => { const random = Math.floor(Math.random() * 0xffffff).toString(); formData.append("files", item.file); const param = { msgContent: item.file.name, msgType: item.type + 1, roomId: store.curRow.id, sendOpenId: store.openId, attachment: item.type !== 1 ? item.url : item.file.name, isError: false, source: "web", msgTag: random, }; paramList.push(param); changeMsgAndShowList([ { ...param, createTime: formatDate(Date.now())[6], } as msgItem, ...msgList.value, ]); nextTick(() => { currentPage.value = 1; sourceReady(true, [param as msgItem]); }); }); formData.append("roomId", store.curRow.id); axios .post(`${baseURL}chat/file/files`, formData, { onUploadProgress(e: any) { curFileProgressShow.value = true; curFileProgress.value = Math.round((e.loaded * 100) / e.total); }, }) .then((res) => { const list = res.data.data; curFileProgressShow.value = false; curFileProgress.value = 0; if (list && list.length > 0) { list.forEach((item: string, index: number) => { const needReplaceIndex = msgList.value.findIndex( (citem) => citem.msgTag === paramList[index].msgTag ); msgList.value[needReplaceIndex].attachment = item; if (item.url) { window.URL.revokeObjectURL(url); } handleSend("", 0, "", { ...paramList[index], attachment: item }); }); } }) .catch((error) => { handleError(error); }); };
|
消息发送
这一块我就不过多阐述了,和之前发送图片是一样的。