tiptap编辑器新建自定义插件|改造tiptap原本超链接|新增在超链接上悬浮可以显示一个编辑复制取消超链接的工具栏|键盘监听设置,不冒泡到父组件的键盘监听,在子组件进行拦截

功能实现效果

创建注册超链接插件

原本的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();// 阻止冒泡
    }
}

  • 11
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值