Tiptap:支持多个编辑框

1 支持多个编辑框:

1.1 新建单个编辑框方法:

用到的组件:

import { Editor, EditorContent } from "@tiptap/vue-2";

// in template
<template>
    //...
    <editor-content :editor="editor" />
    //...
</template>

//in script
<script>
//...
import { Editor, EditorContent } from '@tiptap/vue-3'

export default {
  components: {
    EditorContent,
  },

  data() {
    return {
      editor: null,
    }
  },

  mounted() {
    this.editor = new Editor({
      // 插件
      extensions: [
        StarterKit,
      ],
      // editor内容:string|HTML
      content: `Write something...`,
    })
  },

  beforeUnmount() {
    this.editor.destroy()
  },
}
//...
</script>

1.2 动态创建编辑框:

需要的前置知识:Vue2动态添加组件(见第二节)

  1. 创建EditorContent组件构造器
  2. 创建editor实例
  3. 创建EditorContent组件,editor实例作为参数传入
  4. 挂载EditorContent组件
// in template
<template>
    <button @click="newEditorContent">动态创建编辑框</button>
    <div id="editor-container"></div>
</template>

// in script
<script>
import { Editor, EditorContent } from "@tiptap/vue-2";

export default {
  components: {
    EditorContent,
  },

  data() {
    return {
      editor: null,
    }
  },
  
  methods:{
    newEditorContent() {
      var editorContent = Vue.extend(EditorContent);
      this.editor = 
        new Editor({
          content: `<p>This is my editor. 🎉</p>`,
          extensions: [
            StarterKit
          ]
        })
   

      //VueComponent
      var newEditor = new editorContent({
        propsData: {
          editor: this.editor,
        },
      }).$mount();

      //newEditor.$el-DOM
      document.getElementById("editor-container").appendChild(newEditor.$el);
    },
  }
}
</script>

1.3 支持多个编辑框

  1. storage:数组[]Editor, 用于存放editor实例
  2. count:记录编辑框个数
  3. currentEditor:当前聚焦的编辑框
// in template

import { Editor, EditorContent } from "@tiptap/vue-2";

export default {
  components: {
    EditorContent,
  },

  data() {
    return {
        editors: [],
        count: 0,
        currentEditor: {}
    }
  },
  
  methods:{
    newEditorContent() {
      var editorContent = Vue.extend(EditorContent);
      let _this = this;
      this.editors.push(
        new Editor({
          content: `<p>This is my editor. 🎉</p>`,
          extensions: [
            StarterKit
          ],
          onFocus({ editor, event }) {
            _this.currentEditor = editor;
          },
        })
      );

      //VueComponent
      var newEditor = new editorContent({
        propsData: {
          editor: this.editors[this.count],
        },
      }).$mount();

      //newEditor.$el-DOM
      document.getElementById("editor-container").appendChild(newEditor.$el);
    },
  }
}


2 Vue2动态添加组件

参考:

https://juejin.cn/post/6890072682864476168

https://blog.csdn.net/m0_37941483/article/details/90237575

https://developer.aliyun.com/article/1052928

2.1 Vue.extend(options): 基础Vue构造器

https://www.jianshu.com/p/ed58f070684c

options参数是一个Vue组件对象(data属性必须是函数类型),返回一个扩展实例构造器, (注意是构造器并不是实例本身

源码如下:

/**
     * Each instance constructor, including Vue, has a unique
     * cid. This enables us to create wrapped "child
     * constructors" for prototypal inheritance and cache them.
     */
    Vue.cid = 0;
    var cid = 1;
    /**
     * Class inheritance
     */
    Vue.extend = function (extendOptions) {
        extendOptions = extendOptions || {};
        var Super = this;
        var SuperId = Super.cid;
        var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
        if (cachedCtors[SuperId]) {
            return cachedCtors[SuperId];
        }
        var name = getComponentName(extendOptions) || getComponentName(Super.options);
        if (process.env.NODE_ENV !== 'production' && name) {
            validateComponentName(name);
        }
        var Sub = function VueComponent(options) {
            this._init(options);
        };
        Sub.prototype = Object.create(Super.prototype);
        Sub.prototype.constructor = Sub;
        Sub.cid = cid++;
        Sub.options = mergeOptions(Super.options, extendOptions);
        Sub['super'] = Super;
        // For props and computed properties, we define the proxy getters on
        // the Vue instances at extension time, on the extended prototype. This
        // avoids Object.defineProperty calls for each instance created.
        if (Sub.options.props) {
            initProps(Sub);
        }
        if (Sub.options.computed) {
            initComputed(Sub);
        }
        // allow further extension/mixin/plugin usage
        Sub.extend = Super.extend;
        Sub.mixin = Super.mixin;
        Sub.use = Super.use;
        // create asset registers, so extended classes
        // can have their private assets too.
        ASSET_TYPES.forEach(function (type) {
            Sub[type] = Super[type];
        });
        // enable recursive self-lookup
        if (name) {
            Sub.options.components[name] = Sub;
        }
        // keep a reference to the super options at extension time.
        // later at instantiation we can check if Super's options have
        // been updated.
        Sub.superOptions = Super.options;
        Sub.extendOptions = extendOptions;
        Sub.sealedOptions = extend({}, Sub.options);
        // cache constructor
        cachedCtors[SuperId] = Sub;
        return Sub;
    };
  1. Vue.extend 返回方法VueComponent,核心代码如下:

    var Sub = function VueComponent(options) {
        this._init(options);
    };
    
  2. _init源码如下(解析与简化版本[1]

    let uid = 0;
    export function initMixin(Vue: Class<Component>) {
      Vue.prototype._init = function(options?: Object) {
        const vm: Component = this;
    
        vm._uid = uid++; // 当前实例的 _uid 加 1
    
    	//  a flag to avoid this being observed
        // 用 _isVue 来标识当前实例是 Vue 实例, 这样做是为了后续被 observed
        vm._isVue = true;
    
        // merge options 合并options 
        if (options && options._isComponent) { // _isComponent 标识当前为 内部Component
          // 内部Component 的 options 初始化
          initInternalComponent(vm, options); 
        }
        else { // 非内部Component的 options 初始化
          vm.$options = mergeOptions(
            resolveConstructorOptions(vm.constructor),
            options || {},
            vm
          );
        }
    
        // 在render中将this指向vm._renderProxy
        if (process.env.NODE_ENV !== 'production') {
          initProxy(vm);
        }
        else {
          vm._renderProxy = vm;
        }
        // expose real self
        vm._self = vm;
        initLifecycle(vm); // 初始化生命周期
        initEvents(vm); // 初始化事件
        initRender(vm); // 初始化渲染函数
        callHook(vm, 'beforeCreate'); // 回调 beforeCreate 钩子函数
        initInjections(vm); // resolve injections before data/props
        // 初始化 vm 的状态
        initState(vm);
        initProvide(vm); // resolve provide after data/props
        callHook(vm, 'created'); // vm 已经创建好 回调 created 钩子函数
    
        if (vm.$options.el) { // 挂载实例
          vm.$mount(vm.$options.el);
        }
      }
    
    

创建组件:

var componentConstructor = Vue.extend(Component);

var vueComponent = new componentConstructor(options)

入参options用来配置组件属性,返回一个Vue组件实例

2.2 Vue(options).$mount()

手动挂载组件[2]

$mount 方法对组件进行了手动渲染,但它仅仅是被渲染好了,并没有挂载到节点上[3]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5Tlj9F7b-1689932814069)(assets/image-20230718171653-sg7xse6.png)]

  1. VueComponent.$mount() 对组件实例挂载,提取this.$el(DOM元素)为实例根节点
  2. $mount()返回组件实例本身
  3. $mount()不传参数(被挂载的dom)则新建一个div返回,如果传入dom,则会将对应的dom对象替换成新的div,所以el不能是body或者html,因为body和html是不适合替换的。

$mount() 和 appendChildren(Node) 区别

VueComponent.$mount() 对组件实例挂载,提取this.$el(DOM元素)为实例根节点

appendChildren(Node),需要传入DOM元素,也就是挂载后解析出的this.$el, 传入非DOM元素会报错:

“TypeError: Failed to execute ‘appendChild’ on ‘Node’: parameter 1 is not of type ‘Node’.”

2.3 练习:自定义弹窗组件,使其可以全局使用

具体分为一下几步:

  1. 定义一个弹框组件
  2. 定义一个方法,用来构造弹框组件并将其挂载
  3. 将方法放在原型上
  4. 在vue中调用
// MyDialog.vue
<template>
<TransitionRoot appear :show="showDialog" as="template">
    <Dialog as="div" class="relative z-10">
      <TransitionChild
        as="template"
        enter="duration-300 ease-out"
        enter-from="opacity-0"
        enter-to="opacity-100"
        leave="duration-200 ease-in"
        leave-from="opacity-100"
        leave-to="opacity-0"
      >
        <div class="fixed inset-0 bg-black bg-opacity-25" />
      </TransitionChild>

      <div class="fixed inset-0 overflow-y-auto">
        <div
          class="flex min-h-full items-center justify-center p-4 text-center"
        >
          <TransitionChild
            as="template"
            enter="duration-300 ease-out"
            enter-from="opacity-0 scale-95"
            enter-to="opacity-100 scale-100"
            leave="duration-200 ease-in"
            leave-from="opacity-100 scale-100"
            leave-to="opacity-0 scale-95"
          >
            <DialogPanel
              class="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all"
            >
              <DialogTitle
                as="h3"
                class="text-lg text-center font-medium leading-6 text-gray-900"
              >
                {{title.text}}
              </DialogTitle>
              <div class="mt-2">
                <p :class="'text-sm text-gray-500 ' + `text-${align}`" v-for="(contentItem,index) in content" :key="index">
                  {{contentItem}}
                </p>
              </div>

              <div class="mt-4 flex justify-around">
                <button
                  v-show="cancelText"
                  type="button"
                  class="w-1/2 mr-10 inline-flex justify-center rounded-md border border-gray-200 bg-white-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-gray-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
                  @click="cancelBak"
                >
                  {{cancelText}}
                </button>
                <button
                  type="button"
                  class="w-1/2 inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
                  @click="confirm"
                >
                  {{buttonText}}
                </button>
              </div>
            </DialogPanel>
          </TransitionChild>
        </div>
      </div>
    </Dialog>
  </TransitionRoot>
</template>

<script>
import {
  TransitionRoot,
  TransitionChild,
  Dialog,
  DialogPanel,
  DialogTitle,
  DialogDescription,
} from "@headlessui/vue";
export default {
  name: "dialog",
  components:{Dialog, DialogTitle, DialogDescription, DialogPanel, TransitionRoot, TransitionChild},
  data() {
    return {
      showDialog: false,
      buttonText:"我知道了",
      cancelText:"",
      title:{
          text:"提示",
          img:""
      },
      content:"",
      callback:null,
    };
  },
  created(){
      this.show();
  },
  methods: {
    show() {
      this.$nextTick().then(() => (this.showDialog = true));
    },
    confirm() {
        console.log('confirm');
        this.callback();
        this.cancelBak();
    }
  },
};
</script>

<style>
</style>
// main.js
import Vue from "vue";
import App from "./App.vue";
import dialog from "./components/MyDialog.vue";
Vue.config.productionTip = false;

function showDialog(options) {
  let Dialog = Vue.extend(dialog);
  let dialogComponent = new Dialog(options).$mount();
  for (let key in options) {
    dialogComponent[key] = options[key];
  }
  document.body.appendChild(dialogComponent.$el);
}
Vue.prototype.$dialog = showDialog;
new Vue({
  render: (h) => h(App),
}).$mount("#app");

// 使用
this.$dialog({
  title: { img: null, text: "出错了" },
  content: "系统异常,请稍后再试",
  align: "center",
  buttonText: "我知道了",
  callback: () => {},
});
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值