【element-tiptap】如何把分隔线改造成下拉框的形式?

当前的分隔线只有细横线这一种形式
在这里插入图片描述
但是咱们可以看一下wps中的分隔线,花里胡哨的
这些在wps里都需要使用快捷键打出来,真没找到菜单在哪里
在这里插入图片描述
那么这篇文章咱们就来看一下如何改造分隔线组件,改造成下拉框的形式,并且把咱们想要的分隔线都放进去
分隔线扩展是这个 HorizontalRule
src/extensions/horizontal-rule.ts

1、创建下拉框组件

项目中有好几个下拉框组件,首先,咱们需要仿照它们,创建分隔线的下拉框组件
仿照上一篇文章研究的 FontFamilyDropdown.vue,先大致写一下,后面再详细补充

<template>
    <el-dropdown placement="bottom" trigger="click" @command="insertHorizontalRule">
        <command-button :enable-tooltip="enableTooltip" tooltip="插入分隔线" icon="horizontal-rule" />
        <template #dropdown>
            <el-dropdown-menu>
                <el-dropdown-item v-for="rule in horizontalRules" :key="rule.value" :command="rule.value">
                    {{ rule.label }}
                </el-dropdown-item>
            </el-dropdown-menu>
        </template>
    </el-dropdown>
</template>
<script lang="ts">
import {defineComponent, inject} from 'vue';
import {Editor, getMarkAttributes} from '@tiptap/vue-3';
import {ElDropdown, ElDropdownMenu, ElDropdownItem} from 'element-plus';
import CommandButton from './CommandButton.vue';

export default defineComponent({
  name: 'FontFamilyDropdown',

  components: {
    ElDropdown,
    ElDropdownMenu,
    ElDropdownItem,
    CommandButton,
  },
  props: {
    editor: {
      type: Editor,
      required: true,
    },
  },

  setup() {
    const t = inject('t');
    const enableTooltip = inject('enableTooltip', true);
    const isCodeViewMode = inject('isCodeViewMode', false);
    return {t, enableTooltip, isCodeViewMode};
  },

  computed: {
    horizontalRules() {
      return [
            { label: '细线', value: '---' },
            { label: '粗线', value: '___' },
            { label: '星号线', value: '***' },
        ];
    },
  },
  methods: {
    insertHorizontalRule(rule: string) {
      this.editor.commands.setHorizontalRule();
    },
  },
});
</script>

2、在扩展中应用分隔线下拉框组件

src/extensions/horizontal-rule.ts

import type { Editor } from '@tiptap/core';
import TiptapHorizontalRule from '@tiptap/extension-horizontal-rule';
import HorizontalRuleDropdown from '@/components/MenuCommands/HorizontalRuleDropdown.vue';

const HorizontalRule = TiptapHorizontalRule.extend({
  addOptions() {
    return {
      // 保留父扩展的所有选项
      ...this.parent?.(),
      button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) {
        return {
          component: HorizontalRuleDropdown,
          componentProps: {
            editor,
          },
        };
      },
    };
  },
});

export default HorizontalRule;

此时模样已经出来了
在这里插入图片描述
点击菜单选项都能够插入
看一下此时插入的分隔线,其实是一个 <hr/> 标签
在这里插入图片描述
显然这个方法满足不了我们的需求,因为 editor.commands.setHorizontalRule() 这个方法不允许传递参数,给元素增加类名或者其他属性。那我们只能重新写一个函数

3、探索插入的实现

向文档中插入节点,有一个方法是 editor.commands.insertContentAt
可以看一下这个函数的定义
node_modules/@tiptap/core/dist/commands/insertContentAt.d.ts

insertContentAt: (
	 /**
	  * 插入内容的位置。
	  */
	 position: number | Range, 
	 /**
	  * 要插入的 ProseMirror 内容。
	  */
	 value: Content, 
	 /**
	  * 可选的选项
	  */
	 options?: {
	     /**
	      * 解析内容的选项。
	      */
	     parseOptions?: ParseOptions;
	     /**
	      * 插入内容后是否更新选区。
	      */
	     updateSelection?: boolean;
	     /**
	      * 插入内容后是否应用输入规则。
	      */
	     applyInputRules?: boolean;
	     /**
	      * 插入内容后是否应用粘贴规则。
	      */
	     applyPasteRules?: boolean;
	     /**
	      * 内容无效时是否抛出错误。
	      */
	     errorOnInvalidContent?: boolean;
	 }) => ReturnType;

可选的选项咱们可以先不管,先看下前两个参数

  • position
    可以传数字或者 Range,传数字表示索引,传 Range 就是在固定位置插入
  • value: Content
    具体内容
export type Content = HTMLContent | JSONContent | JSONContent[] | null;

HTMLContentstring 类型,直接这样写就行

'<h1>Example</h1>'

JSONContent 的定义如下

export type JSONContent = {
  type?: string;
  attrs?: Record<string, any>;
  content?: JSONContent[];
  marks?: {
    type: string;
    attrs?: Record<string, any>;
    [key: string]: any;
  }[];
  text?: string;
  [key: string]: any;
};

JSONContenttype 属性,我原以为会有一个常量的列表,但是我太天真了,找了半天没找到。但是经过我的实验,可以确定的是,如果你想往文档里插入一个 div,那么你注定会失败,例如我想用下面代码插入一个 div 标签:

editor.commands.insertContentAt(selection.from,
{
    type: 'div',
    text: 'dsadsa'
})

结果执行完了之后,长这样:
在这里插入图片描述
合理猜测,这个 type 属性,只允许在编辑器中定义好的节点类型。哭唧唧
但是我们还有有路可以走,比如,探索一下其他的节点是怎么插入的,然后模仿并且超越。

不如就来看一下图片是怎么插入的!
一个图片插入功能,其实需要好几个文件来支撑

  • src/utils/image.ts 文件,这个文件定义加载和缓存图片的方法、以及提供了图像显示方式的枚举,可以认为一些基础方法、枚举值都在这个文件夹的 ts 文件中定义

  • src/extensions/image.ts,这个文件是用来扩展 tiptap 图像节点的,并且集成了插入图片的组件,提供了自定义图像属性的方法和渲染HTML的方法

  • src/components/ExtensionViews/ImageView.vue,是图像展示的组件,用于渲染和交互式调整图像
    在这个文件中,我们可以看到,实际的插入的图像内容是被一个标签 node-view-wrapper 包裹起来的,所以咱们待会构建分割符组件的时候也要用这个标签把我们实际要插入的内容包裹起来
    在这里插入图片描述

  • src/components/MenuCommands/Image/ImageDisplayCommandButton.vue,是一个弹出菜单修改图像显示方式的组件
    在这里插入图片描述

  • src/components/MenuCommands/Image/InsertImageCommandButton.vue,插入图像的按钮,下拉框有两个选项
    在这里插入图片描述

  • src/components/MenuCommands/Image/EditImageCommandButton.vue,编辑图像的组件
    在这里插入图片描述

  • src/components/MenuCommands/Image/RemoveImageCommandButton.vue 删除图像的组件,其实就一个小按钮
    在这里插入图片描述

好吧,万幸,插入分割线功能没有这么的复杂,在插入之后就不需要修改了。那我们来梳理一下我们需要创建几个文件来插入分割线。

1、src/utils/horizontal-rule.ts 定义分割线类型和html之间的对应关系
2、src/extensions/horizontal-rule.ts 调用 tiptap 的API增加扩展项
3、src/components/MenuCommands/HorizontalRuleDropdown.vue 定义下拉菜单,用来选择分割线的类型
4、src/components/ExtensionViews/HorizontalRuleView.vue 定义插入分割线渲染出来的组件

接下来,咱们就挨个文件看,我这里主要仿照两个组件,一个是图片相关的,一个是Iframe相关的

4、src/utils/horizontal-rule.ts

这里定义为数组,在下拉框中,我们需要直接展示出来分割线,但是点击分割线的时候,需要把分割线的类型取出来,给 setHorizontalRule 方法;但是当插入的时候,又要根据分割线的类型去找对应的分割线的html。所以说,分割线的类型,其实也可以叫做唯一标识,与分割线的html之间是需要双向转换的。使用对象的话,反向查找就会有一些不方便,所以直接定义成数组。下面的html是经过我测试的,大家在开发的时候可以先写一些简单的测试数据,我的数据效果是这样子的:
在这里插入图片描述

export const horizontalRules = [
  {
    borderStyle: 'solid',
    html: `<hr style="border: none; border-top: 1px solid black;">`
  },
  {
    borderStyle: 'dotted',
    html: `<hr style="border: none; border-top: 1px dotted black;">`
  },
  {
    borderStyle: 'dashed',
    html: `<hr style="border: none; border-top: 1px dashed black;">`
  },
  {
    borderStyle: 'double',
    html: `<hr style="border: none; height: 6px; border-top: 1px solid black; border-bottom: 3px solid black;">`
  },
  {
    borderStyle: 'triple',
    html: `<div style="display: flex; flex-direction: column; gap: 2px;"><hr style="border: none; border-top: 1px solid black; margin: 0;"><hr style="border: none; border-top: 2px solid black; margin: 0;"><hr style="border: none; border-top: 1px solid black; margin: 0;"></div>`
  },
];

export default horizontalRules;


5、src/extensions/horizontal-rule.ts

还是来看 src/extensions/horizontal-rule.ts 文件,参考 src/extensions/image.ts 文件
① 分割线需要一个属性表示分割线的类型,那么就需要 addAttributes 方法,直接仿照图片扩展里面的代码写就行

addAttributes() {
  return {
    ...this.parent?.(),
    'border-style': {
      parseHTML: (element) => {
        const borderStyle = element.getAttribute('borderStyle');
        return borderStyle;
      },
      renderHTML: (attributes) => {
        return {
          'border-style': attributes['border-style'],
        };
      },
    },
  };
},

② 需要 addNodeView 为扩展添加节点视图

addNodeView() {
  return VueNodeViewRenderer(HorizontalRuleView);
},

③ 需要 parseHTMLrenderHTML 用来解析和渲染HTML

// 为扩展添加解析HTML
parseHTML() {
  return [
    {
      tag: 'div',
    },
  ];
},
// 为扩展添加渲染HTML
renderHTML({ HTMLAttributes }) {
  return [
    'div',
    HTMLAttributes
  ];
},

④ 添加命令。由于tiptap提供的 setHorizontalRule 方法满足不了需求,所以我们需要重写一下这个方法

addCommands() {
  return {
    setHorizontalRule:
      (options) =>
      ({ commands }) => {
        return commands.insertContent({
          type: this.name,
          attrs: {
            'border-style': options.borderStyle,
          },
        });
      },
  };
},

完整代码

import type { Editor } from '@tiptap/core';
import TiptapHorizontalRule from '@tiptap/extension-horizontal-rule';
import HorizontalRuleDropdown from '@/components/MenuCommands/HorizontalRuleDropdown.vue';
import { mergeAttributes, VueNodeViewRenderer } from '@tiptap/vue-3';
import HorizontalRuleView from '@/components/ExtensionViews/HorizontalRuleView.vue';

const HorizontalRule = TiptapHorizontalRule.extend({
  // 返回的数据,第一个是继承的父级的属性
  // 后面的是自己的属性
  addAttributes() {
    return {
      ...this.parent?.(),
      'border-style': {
        parseHTML: (element) => {
          const borderStyle = element.getAttribute('borderStyle');
          return borderStyle;
        },
        renderHTML: (attributes) => {
          return {
            'border-style': attributes['border-style'],
          };
        },
      },
    };
  },
  // 为扩展添加选项
  addOptions() {
    return {
      // 保留父扩展的所有选项
      ...this.parent?.(),
      button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) {
        return {
          component: HorizontalRuleDropdown,
          componentProps: {
            editor,
          },
        };
      },
    };
  },
  // 为扩展添加节点视图
  addNodeView() {
    return VueNodeViewRenderer(HorizontalRuleView);
  },
  // 为扩展添加解析HTML
  parseHTML() {
    return [
      {
        tag: 'div',
      },
    ];
  },
  // 为扩展添加渲染HTML
  renderHTML({ HTMLAttributes }) {
    return [
      'div',
      HTMLAttributes
    ];
  },
  // 为扩展添加命令
  addCommands() {
    return {
      setHorizontalRule:
        (options) =>
        ({ commands }) => {
          return commands.insertContent({
            type: this.name,
            attrs: {
              'border-style': options.borderStyle,
            },
          });
        },
    };
  },
});

export default HorizontalRule;

6、src/components/MenuCommands/HorizontalRuleDropdown.vue

这个方法咱们已经实现的很成熟了,首先数据在这里我们不需要再定义一遍了,需要从咱们刚定义的文件中引入

import horizontalRules from '@/utils/horizontal-rule';

在 setup 函数中返回 horizontalRules:

return { t, enableTooltip, isCodeViewMode, horizontalRules };

循环的模版代码要修改一下,因为我们现在使用的是数组了

<el-dropdown-item v-for="rule in horizontalRules" :key="rule.borderStyle" :command="rule.borderStyle">
   	<div contenteditable="false" class="horizontal-rule-item" v-html="rule.html"></div>
</el-dropdown-item>

要把 borderStyle 传到 setHorizontalRule 命令里面

insertHorizontalRule(borderStyle: string) {
    this.editor.commands.setHorizontalRule({ borderStyle });
},    

7、src/components/ExtensionViews/HorizontalRuleView.vue

这个文件就是插入分割线的时候实际插入的内容,模版需要使用 node-view-wrapper 标签包裹,数据也从我们定义的常量文件中获取

<template>
  <node-view-wrapper as="div" class="horizontal-rule">
    <div
     class="horizontal-rule__line" 
     v-html="getHorizontalRuleHtml(borderType)"
     >
    </div>
  </node-view-wrapper>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import { NodeViewWrapper, nodeViewProps } from '@tiptap/vue-3';
import horizontalRules from '@/utils/horizontal-rule';

export default defineComponent({
  name: 'HorizontalRuleView',

  components: {
    NodeViewWrapper,
  },

  props: nodeViewProps,

  computed: {
    borderType(): string {
      return this.node!.attrs['border-style'];
    },
  },

  methods: {
    getHorizontalRuleHtml(borderStyle: string): string {
      const rule = horizontalRules.find(rule => rule.borderStyle === borderStyle);
      return rule ? rule.html : '';
    },
  },
});
</script>

<style scoped>
.horizontal-rule__line {
  width: 100%;
}
</style>

8、看看效果
首先,点击下拉框按钮,弹出菜单
在这里插入图片描述
然后,点击菜单项,就会插入分割线
在这里插入图片描述
边距之类的样式可以自己在调整调整 耶耶耶耶耶
通过这篇文章,也掌握了tiptap大致的扩展节点的方法就是需要那么几个目录文件
1、src/utils/xx.ts 定义常量
2、src/extensions/xx.ts 定义扩展,可以创建新的节点,也可以继承、重写已有的节点
3、src/compoents/MenuCommands/xx.vue 定义菜单项
4、src/components/ExtensionViews/xx.vue 定义实际插入的内容,需要使用node-view-wrapper 标签包裹

上一篇:10月22日纯血鸿蒙正式版发布意味着什么?


下一篇:TCP的三次握手和四次挥手