今天写项目的时候,遇到一个问题,分享给大家。
场景
我有一个A区域,还有一个B区域。A区域内的Vue组件可以通过Vuedraggable这个框架来拖拽到B区域中。B区域内的Vue组件在标题上使用了element-tiptap组件(用来高级编辑)。然而如果B区与内存在使用了element-tiptap组件的组件。就会出现把 A区域中组件的文本内容拖拽到element-tiptap中。
其实我想要的创建新的组件,而它把我拖拽的组件的文本内容添加到了组件中的element-tiptap中。
问题分析
HTML元素设置了contenteditable="true"之后,或者input输入框之间,默认都是可以把选中的文字拖拽到其他的input的元素或者contenteditable="true"的元素中。
我要做的就是禁止其他页面元素把文本内容拖拽到element-tiptap的组件中,element-tiptap组件的原理就是用的contenteditable="true"来实现高级编辑。
我自己写了一个组件案例,用来验证是否contenteditable="true"的情况下,能通过重写drop事件禁止拖拽:
<script>
//导入自定义的组件
import DragDIV from "@/components/DragDIV";
/* eslint-disable */
export default {
name: 'App',
components:{
DragDIV
},
mounted() {
//根据class类名动态的添加drop事件
const divs = document.getElementsByClassName('content')
console.log(divs.length)
for (var i=0;i<divs.length; i++) {
let c_item=divs[i]
console.log(c_item.innerHTML)
c_item.addEventListener("drop",(e)=>{
console.log('chesse')
e.preventDefault()
})
}
}
}
</script>
<template>
<!--引用自定义的组件-->
<DragDIV />
</template>
自定义组件:DragDiv.vue
<template>
<div contenteditable="true" class="content"><p>
姐姐,为什么要告诉这楚枫你的身份?风铃对里雾问道他今日既然帮了忙,之前的恩怨,自然要化解。
与其日后被他发现,还不如我先告诉他。里雾说道我记得你说过他,但你当时不是说,他是你在祖武天河,遇到的小角色吗?
那为何这次遇到,你却又突然告知于我,他可以帮我破阵呢?风铃对里雾问道她与楚枫第一次见面,便是在那行宫之内,但其实在她见到楚枫之前
就已经收到了里雾的通知告关于楚枫要去那行宫,以及楚枫有些特别,风铃可以尝试利用楚枫破解行宫考验等事刚好诅咒之力发作,
楚枫出手相助,虽然只是化解了表面症状,但能做到这一点已经很不简单。</p>
</div>
<div contenteditable="true" @click="handleClick" class="content">
<p>人生何处不相逢</p>
</div>
</template>
<script>
export default {
name: "DragDIV",
methods:{
handleClick(e){
console.log('handleClick')
// e.currentTarget.setAttribute("contenteditable",true)
},
handleMouse(e){
// e.currentTarget.setAttribute("contenteditable",false)
}
}
}
</script>
<style scoped>
.content{
width:400px;
height:800px;
-webkit-user-drag: none;
/*margin:0 auto;*/
float: left;
border:2px solid lightgrey;
}
</style>
实际是生效的,可以通过这种方式来解决这个问题。所以我应该找的是drop事件相关的配置。这里我想了很多方法:
1. 查找element-tiptap 官方文档和源代码,看看有没有相关的组件配置,用来防止其他页面元素拖拽文件进来。onDrop方法前后打印当前的值,结果是相同的,无法解决当前问题。
2. csdn站内包括其他网站的解决方案:把HTML元素的draggable="false",我加上试了没有生效。并没有修改drop事件的处理函数。element-tiptap中自定义了drop事件的处理函数。
3. 实在没办法,只能通过浏览器Debug去分析它的加载流程,包括事件的处理顺序,找到了element-tiptap中,设置contenteditable="true"的div,类名是:ProseMirror,它上面加了两个drop事件的处理函数,对应到它框架代码:
editHandlers.drop = (view, _event) => {
let event = _event;
let dragging = view.dragging;
view.dragging = null;
if (!event.dataTransfer)
return;
let eventPos = view.posAtCoords(eventCoords(event));
if (!eventPos)
return;
let $mouse = view.state.doc.resolve(eventPos.pos);
let slice = dragging && dragging.slice;
if (slice) {
view.someProp("transformPasted", f => { slice = f(slice); });
}
else {
slice = parseFromClipboard(view, event.dataTransfer.getData(brokenClipboardAPI ? "Text" : "text/plain"), brokenClipboardAPI ? null : event.dataTransfer.getData("text/html"), false, $mouse);
}
let move = !!(dragging && !event[dragCopyModifier]);
if (view.someProp("handleDrop", f => f(view, event, slice || Slice.empty, move))) {
event.preventDefault();
return;
}
if (!slice)
return;
event.preventDefault();
let insertPos = slice ? dropPoint(view.state.doc, $mouse.pos, slice) : $mouse.pos;
if (insertPos == null)
insertPos = $mouse.pos;
let tr = view.state.tr;
if (move)
tr.deleteSelection();
let pos = tr.mapping.map(insertPos);
let isNode = slice.openStart == 0 && slice.openEnd == 0 && slice.content.childCount == 1;
let beforeInsert = tr.doc;
if (isNode)
tr.replaceRangeWith(pos, pos, slice.content.firstChild);
else
tr.replaceRange(pos, pos, slice);
if (tr.doc.eq(beforeInsert))
return;
let $pos = tr.doc.resolve(pos);
if (isNode && NodeSelection.isSelectable(slice.content.firstChild) &&
$pos.nodeAfter && $pos.nodeAfter.sameMarkup(slice.content.firstChild)) {
tr.setSelection(new NodeSelection($pos));
}
else {
let end = tr.mapping.map(insertPos);
tr.mapping.maps[tr.mapping.maps.length - 1].forEach((_from, _to, _newFrom, newTo) => end = newTo);
tr.setSelection(selectionBetween(view, $pos, tr.doc.resolve(end)));
}
view.focus();
view.dispatch(tr.setMeta("uiEvent", "drop"));
};
这里可以看到它的处理流程还是很复杂,我的需求里面没有拖拽文本到B区域中组件的需求,所以这个事件处理函数可以在它的框架代码中删除。
但是项目下有很多的依赖,暂时只能通过如下方法来解决这个问题:(跳过element-tiptap中的drop事件处理函数)
prosemirror-view/dist/index.js:3403行下加入return;
总结
1. 前端框架的很多高级特性都基于HTML中不常见的属性来实现,在使用框架过程中,应该多去看看它的实现原理,分析关键代码。
2. 编写简洁的代码。
3. 虽然是前端的框架,但是现在很多框架都是用TypeScript来实现,也是有OOP的实践在里面,要多考虑对象的行为和特性,不能把对象属性都暴露,也不能都不暴露。
4. 复用的可能,如果一个框架中组件的复用度不高,引入这个框架,对于项目的后期维护非常不利,想弃用,有依赖 。想重写,需要兼容。
参考:
GitHub - Leecason/element-tiptap: 🌸A modern WYSIWYG rich-text editor using tiptap and Element UI for Vue2 (🚀 tiptap2 and Vue3 is in alpha)
https://github.com/ueberdosis/tiptap
draggable - HTML(超文本标记语言) | MDN
contenteditable - HTML(超文本标记语言) | MDN
欢迎大家私信,留言学习交流。