功能实现效果
创建注册超链接插件
原本的tiptap中的超链接组件:
this.editor.commands.insertContent({
type: "text",
text: text,
marks: [
{
type: "link",
attrs: {
href: link,
}
}
]
});
修改之后将会应用新写的插入超链接commands方法
this.editor.commands.setEditLinkText({text:text,link:link})
使用Node新建插件,并书写上面的方法在这个插件之中。命名为LinkItem
import {Node, mergeAttributes} from '@tiptap/core'
import {VueNodeViewRenderer} from '@tiptap/vue-2'
import LinkEditPanel from "../component/link-text/link-edit-panel.vue";
export default Node.create({
name: 'LinkItem',
group: 'inline',
inline: true,
draggable: true,
// 定义 editable 属性为 true
editable: true,
addAttributes() {
return {
text: {
default: null,
},
link: {
default: null,
},
settingVisible :{
default : false, // 默认不展示设置
}
}
},
parseHTML() {
return [
{
tag: 'LinkDiv',
},
]
},
renderHTML({HTMLAttributes}) {
return ['LinkDiv', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]
},
addCommands() {
return {
setEditLinkText: attr => ({commands}) => {
return commands.insertContent({
type: "LinkItem",
attrs:
{
text:attr.text,
link: attr.link,
},
marks :[]
});
},
};
},
addNodeView() {
return VueNodeViewRenderer(LinkEditPanel)
},
})
其中引用的LinkEditPanel 是自定义的组件,也就是超链接的自己写的超链接代替。
其中自定义的插件必须 <node-view-wrapper > 包围,因为上面的JS也是采取了使用Node的方式来书写这个插件。
写完js之后,将这块js在editor中组册使用,在editor的extensions中引用。
以下附上这组件的代码:
<template>
<node-view-wrapper draggable="true"
class="date-p"
style="position: relative"
data-drag-handle>
<a class="text" @mouseenter="openSetting"
@mouseleave="closeSetting" @click="toLink">{{this.node.attrs.text}}</a>
<div @mouseenter="openSetting"
@mouseleave="closeSetting">
<div id="details" v-if="settingVisible">
<el-tooltip class="tooltip" effect="dark" content="取消链接" placement="bottom">
<img src="./icon/notLink.svg" @click="unLink" class="icon"/>
</el-tooltip>
<el-tooltip class="tooltip" effect="dark" content="编辑" placement="bottom">
<img id="editIcon" src="./icon/pan.svg" @click="dialogShow" class="icon"/>
</el-tooltip>
<el-tooltip class="tooltip" effect="dark" content="复制链接" placement="bottom">
<img src="./icon/copy.svg" @click="copy" class="icon"/>
</el-tooltip>
<el-divider direction="vertical" style="color: #0d0d0d1f;"/>
<a @click="toLink" class="link_inner_text" :title="node.attrs.link">{{this.node.attrs.link}}</a>
</div>
</div>
<div v-if="showEditValue">
<link-text-panel id="editTextLink" class="editTextLink" ref="linkText" @handleOk="updateLinkText"/>
</div>
</node-view-wrapper>
</template>
<script>
import {NodeViewWrapper, nodeViewProps} from "@tiptap/vue-2";
import ClickOutside from 'vue-click-outside'
import LinkTextPanel from "./link-text-panel";
export default {
name: "link-edit-panel",
directives: {
ClickOutside
},
props: {
nodeViewProps
},
components: {
NodeViewWrapper,
LinkTextPanel
},
data() {
return {
myTimer: null,
text: null,
link: null,
settingVisible: false,
showEditValue: false,
editIcon:null,
}
},
mounted() {
let that = this;
setTimeout(() => {
if (that.node.attrs.user && that.$store.state.member.userId == that.node.attrs.user) {
// 创建的用户,并且是第一次进入,直接打开选择框
that.updateAttributes({
user: null,
});
that.open();
}
})
},
methods :{
unLink(){
if (!this.editor || !this.editor.isEditable) {
this.$message.info("查看模式不允许修改编辑!")
return
}
let that = this;
that.deleteNode()
that.$message({
message: "链接取消成功",
type: "success",
});
that.editor.commands.insertContent({
type: "text",
text: that.node.attrs.text
})
},
dialogShow(){
if (!this.editor || !this.editor.isEditable) {
this.$message.info("查看模式不允许修改编辑!")
return
}
this.editIcon = document.getElementById("editIcon");
this.showEditValue = true;
var that = this;
that.settingVisible = false;
this.$nextTick(() => {
this.$refs.linkText.openEdit(this.node.attrs.text, this.node.attrs.link);
});
// 添加点击事件监听器
if (that.showEditValue){
document.addEventListener("click", this.clickListener)
}
},
clickListener(event) {
var popup = document.getElementById("editTextLink");
var targetElement = event.target; // 点击事件的目标元素
console.log(popup);
console.log(targetElement);
if (targetElement === this.editIcon){
return
}
if (targetElement !== popup && !popup.contains(targetElement)) {
this.showEditValue = false;
this.settingVisible = false;
document.removeEventListener("click", this.clickListener)
}
},
updateLinkText(text, link){
this.node.attrs.text = text
this.node.attrs.link = link
this.showEditValue = false;
this.settingVisible = false;
let that = this;
that.updateAttributes({
text: text,
link: link,
});
that.$message.success("修改成功!")
},
copy(){
var textarea = document.createElement("textarea");
textarea.value = this.node.attrs.link;
textarea.style.position = "fixed"; // 确保 textarea 在视觉上不显示出来
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
document.body.removeChild(textarea);
this.$message.success("链接已复制")
},
openSetting(){
clearTimeout(this.myTimer);
let that = this
this.myTimer = setTimeout(function() {
that.settingVisible = true;
}, 300);
},
closeSetting(){
clearTimeout(this.myTimer);
let that = this;
this.myTimer = setTimeout(function() {
that.settingVisible = false;
}, 300);
},
toLink(){
window.open(this.node.attrs.link);
}
}
}
</script>
<style scoped>
.link_inner_text {
width: calc(100% - 120px);
color: #080e17;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.link_inner_text:hover {
color: #0a6cff;
}
.text {
color: #227aff;
text-decoration: none;
}
.text:hover {
background-color: #d9e5fe;
}
#details {
padding-left: 8px;
padding-right: 8px;
display: flex;
position: absolute;
z-index: 50;
background: #fff;
border-radius: 0.5rem;
margin-bottom: 10px;
box-shadow: #cccccc 0px 0px 10px;
width: 250px;
height: 42px;
top: -51px;
left: 0;
flex-direction: row-reverse;
align-items: center;
justify-content: space-around;
}
.editTextLink {
position: absolute;
background: #fff;
z-index: 9999;
display: flex;
flex-direction: column;
border-radius: 0.5rem;
margin-bottom: 10px;
box-shadow: #cccccc 0px 0px 10px;
width: 435px;
height: 140px;
padding: 16px 16px 3px 16px;
top: -168px;
}
.icon {
display: block;
width: 16px;
height: 16px;
padding: 5px;
margin-right: 8px;
border-radius: 6px;
}
.icon:hover{
background: #f0f0f0;
}
.date-p {
display: inline-block;
line-height: inherit;
}
</style>
同时也写了个添加超链接的组件
效果:
代码如下
<template>
<div class="body">
<div class="text-input">
<span class="title">文本</span>
<el-input
id="input1"
class="input"
ref="textInputRef"
placeholder="输入文本"
clearable
v-model="text"
@keydown.capture.tab.prevent="handleTabKey"
>
</el-input>
</div>
<div class="link-input">
<span class="title">链接</span>
<el-input
id="input2"
class="input"
ref="linkInputRef"
placeholder="输入链接"
clearable
v-model="link"
@keydown.capture.tab.prevent="handleTabKey"
>
</el-input>
</div>
<div class="buttons-area">
<el-button type="primary" @click="submit()">
确认
</el-button>
</div>
</div>
</template>
<script>
export default {
name: "link-text-panel",
components: {},
data() {
return {
beforeEnter:true,
text: "",
link: "",
}
},
mounted() {
document.addEventListener("keydown", this.handleTabKey);
this.beforeEnter = true;
},
beforeUnmount() {
console.log("移除监听1");
document.removeEventListener("keydown", this.handleTabKey);
},
beforeDestroy() {
console.log("移除监听2");
document.removeEventListener("keydown", this.handleTabKey);
},
methods: {
openEdit(text, link){
var that = this;
that.beforeEnter = false;
that.text = text;
that.link = link;
that.$nextTick(() => {
that.$refs.textInputRef.focus(); // 将焦点设置在第一个输入框上
});
document.getElementById("input1").focus(); // 切换到第一个输入框
},
submitBefore(){
this.beforeEnter = false;
},
closeView(){
this.beforeEnter = true;
this.text = "";
this.link = "";
document.removeEventListener("keydown", this.handleTabKey);
},
focusFirst(){
var that = this;
that.$nextTick(() => {
that.text = "";
that.link = "";
that.$refs.textInputRef.focus(); // 将焦点设置在第一个输入框上
document.getElementById("input1").focus(); // 切换到第一个输入框
that.beforeEnter = false;
});
},
submit() {
if (this.text === "") {
this.$message.error("文本不能为空!");
return;
} else if (this.link === "") {
this.$message.error("链接不能为空!");
return;
} else if (!this.checkUrl(this.link)) {
this.$message.error("请输入正确链接格式!");
return;
}
this.$emit("handleOk", this.text, this.link)
},
checkUrl(url) {
const reg = /^https?:\/\//;
return reg.test(url)
},
handleTabKey(event) {
if (event.key === "Tab") {
event.stopPropagation(); // 阻止事件冒泡到父父组件
event.preventDefault(); // 阻止默认Tab键行为
if (event.target.id === "input1") {
document.getElementById("input2").focus(); // 切换到第二个输入框
} else {
document.getElementById("input1").focus(); // 切换到第一个输入框
}
}
// 回车 键盘监听
else if (event.key === "Enter") {
console.log("回车" + this.beforeEnter);
if (!this.beforeEnter){
this.submit();
}
event.stopPropagation(); // 阻止事件冒泡到父父组件
}
},
},
}
</script>
<style lang="scss" scoped>
.text-input {
margin-bottom: 10px;
}
.title {
font-size: 14px;
line-height: 22px;
margin-right: 9px;
display: inline-block;
color: #000;
}
.input {
width: 388px !important;
font-size: 14px !important;
height: 32px !important;
}
::v-deep .el-input__inner {
height: 32px !important;
background: #ffffff !important;
}
.buttons-area {
display: block;
text-align: right;
}
.el-button {
border-radius: 6px;
width: 70px;
height: 30px;
padding: 5px 16px;
border: none;
margin-top: 18px;
}
</style>
<style lang="scss">
</style>
其中包含键盘的键盘,键盘监听设置不冒泡到父组件,以及悬浮展示的效果,悬浮在超链接上,然后进行工具栏显示。
键盘监听冒泡拦截
event.stopPropagation(); // 阻止事件冒泡到父父组件
通过上面的语句进行拦截。应该写在子组件的键盘监听之中。
handleTabKey(event) {
if (event.key === "Tab") {
// 对应的自定义键盘操作
event.stopPropagation();// 阻止冒泡
}
}