返回

TipTap Vue 3 字体大小选择:扩展TextStyle轻松实现

vue.js

TipTap + Vue 3:轻松实现字体大小选择功能

用 TipTap (Vue 3 版) 搞富文本编辑器,感觉挺顺手?各种加粗、斜体、标题都安排上了。但是,到了调整字体大小这块儿,是不是有点卡壳了?官方好像没直接给个 FontSize 扩展,StarterKit 里也没瞅见。看隔壁 FontFamily 用得飞起,心里有点痒痒,咱也想给字号安排个下拉菜单!

别急,这事儿能搞定。虽然 TipTap 没有现成的 FontSize 扩展包,但它的 TextStyle 扩展是个宝藏,很多行内样式都能靠它实现。咱们的目标就是利用 TextStyle 来添加和控制 font-size

为啥没有专门的 FontSize 扩展?

你可能有点纳闷,既然有 FontFamily 扩展,为啥字体大小就没个专门的待遇?

这其实跟 TipTap 的设计思路有关。TextStyle 扩展被设计成一个通用的工具,用来处理各种行内文本样式,比如颜色、背景色,当然也包括字体和字号。@tiptap/extension-font-family 只是 TextStyle 的一个 “特化版”,它专门帮你处理了 font-family 相关的逻辑,比如从 HTML 解析 font-family 样式,以及提供 setFontFamily() 这样的命令。

对于字体大小,TipTap 团队可能觉得它足够通用,可以通过配置 TextStyle 来实现,没必要再单独封装一个扩展。好处是灵活性高,坏处就是咱得多写几行配置代码。

解决方案:扩展 TextStyle 支持字体大小

核心思路就是:告诉 TextStyle 扩展,嘿,你得认识 fontSize 这个属性,并且知道怎么在 HTML 里读写它。

操作步骤

1. 安装必要的依赖

确保你已经安装了 TipTap 核心库、Vue 3 绑定以及 TextStyle 扩展。如果你还没装 TextStyle,可以通过 npm 或 yarn 安装:

# 使用 npm
npm install @tiptap/extension-text-style

# 使用 yarn
yarn add @tiptap/extension-text-style

当然,@tiptap/vue-3 和可能用到的 @tiptap/starter-kit 或其他基础扩展(如 Document, Paragraph, Text)也要确保已安装。

2. 自定义 TextStyle 扩展配置

咱们需要稍微改造一下 TextStyle,让它支持 fontSize。这需要用到 TipTap 的 Extension.create() 或直接在 new Editor() 配置里进行。更推荐的方式是创建一个继承自 TextStyle 的新扩展,专门添加 fontSize 属性。

首先,引入必要的模块:

import TextStyle from '@tiptap/extension-text-style';
import { Extension } from '@tiptap/core';

// 定义一个类型,让 TypeScript 知道 TextStyleAttributes 现在包含 fontSize
// 如果你没用 TypeScript,可以跳过这步,但在 addAttributes 里定义好就行
// declare module '@tiptap/extension-text-style' {
//   interface TextStyleAttributes {
//     fontSize?: string;
//   }
// }

// 创建一个自定义扩展,继承 TextStyle 并添加 fontSize 属性
const FontSizeExtension = Extension.create({
  name: 'fontSize', // 给个名字,方便调试

  // 从 TextStyle 继承,并扩展它的功能
  addOptions() {
    return {
      ...this.parent?.(), // 继承父级的 options (如果有)
      types: ['textStyle'], // 确保这个扩展作用于 textStyle mark
    };
  },

  addGlobalAttributes() {
    return [
      {
        // 要应用的扩展名称,这里是 'textStyle'
        types: ['textStyle'],
        // 定义属性
        attributes: {
          fontSize: {
            default: null, // 默认值,null 表示不设置
            // 解析 HTML:当编辑器加载或粘贴 HTML 时,如何读取 font-size
            parseHTML: element => element.style.fontSize?.replace(/['"]+/g, ''),
            // 渲染 HTML:当编辑器输出 HTML 时,如何应用 font-size
            renderHTML: attributes => {
              if (!attributes.fontSize) {
                return {}; // 如果没有 fontSize 属性,返回空对象
              }
              return {
                style: `font-size: ${attributes.fontSize}`, // 应用样式
              };
            },
          },
        },
      },
    ];
  },

  // 添加命令,方便操作
  addCommands() {
    return {
      setFontSize: (fontSize) => ({ chain }) => {
        return chain()
          .setMark('textStyle', { fontSize: fontSize }) // 设置 fontSize
          .run();
      },
      unsetFontSize: () => ({ chain }) => {
        // 注意:unsetMark('textStyle') 会移除所有 textStyle 属性 (包括颜色等)
        // 更精细的做法是 setMark('textStyle', { fontSize: null })
        return chain()
          .setMark('textStyle', { fontSize: null }) // 将 fontSize 设为默认值 null
          // .removeEmptyTextStyle() // TipTap v2.1 之后可能有类似方法,检查文档
          .run();
      },
    };
  },
});

解释一下上面这段代码:

  • declare module...: 这是 TypeScript 的模块扩展语法,告诉编译器 TextStyleAttributes 接口现在多了一个可选的 fontSize 字符串属性。如果不用 TS,这行可以删掉。
  • FontSizeExtension = Extension.create({...}): 我们创建了一个新的、匿名的 TipTap 扩展。
    • name: 'fontSize',便于识别。
    • addOptions: 声明这个扩展作用于 textStyle 标记。
    • addGlobalAttributes: 这是核心!我们在这里给 textStyle 这个 Mark 添加了一个新的 attributesfontSize
      • default: null: 默认情况下,不应用任何字体大小。
      • parseHTML: 定义了如何从 HTML 元素的 style 属性中解析出 font-size 值。比如,如果遇到 <span style="font-size: 16px;">,它会提取出 16px。这里用 .replace(/['"]+/g, '') 清理一下可能存在的引号。
      • renderHTML: 定义了如何将 fontSize 属性渲染回 HTML。如果 fontSize 有值(比如 16px),它会生成 { style: 'font-size: 16px' },TipTap 会将其应用到相应的 <span> 标签上。
    • addCommands: 添加了两个方便的命令:
      • setFontSize(fontSize): 调用 editor.chain().setFontSize('16px').run() 就能把选中文字的字号设为 16px。它内部是调用 setMark 来设置 textStylefontSize 属性。
      • unsetFontSize(): 用于取消设置的字体大小,恢复默认。我们通过 setMark('textStyle', { fontSize: null }) 来实现,这样更安全,不会误删其他 textStyle(如颜色)。

3. 在 Editor 中注册扩展

现在,在创建 Editor 实例时,把我们刚写的 FontSizeExtension 和基础的 TextStyle 都加进去。同时,确保 Document, Paragraph, Text 这些基础扩展也包括在内(StarterKit 会自动包含它们)。

import { ref, onBeforeUnmount, watch } from 'vue';
import Document from '@tiptap/extension-document';
import Paragraph from '@tiptap/extension-paragraph';
import Text from '@tiptap/extension-text';
import TextStyle from '@tiptap/extension-text-style'; // 必须引入基础 TextStyle
import { Editor, EditorContent } from '@tiptap/vue-3';
// 引入我们上面定义的 FontSizeExtension
// (假设你把上面的代码放在一个单独的文件比如 './FontSizeExtension.js' 并 export default)
// import FontSizeExtension from './FontSizeExtension.js';
// 或者,如果你把代码直接写在 setup 里,就直接用变量名 FontSizeExtension

// ... 省略之前的 FontSizeExtension 定义代码 ...

const editor = new Editor({
  extensions: [
    Document,
    Paragraph,
    Text,
    TextStyle, // 必须先包含基础 TextStyle
    FontSizeExtension, // 再包含我们的字号扩展
    // 如果你之前用了 FontFamily,也要包含进来
    // FontFamily,
    // 如果用了 StarterKit,可以这样配置:
    // StarterKit.configure({
    //   textStyle: false, // 禁用 StarterKit 自带的 TextStyle,避免冲突或重复
    // }),
    // TextStyle, // 手动添加基础 TextStyle
    // FontSizeExtension, // 添加我们的扩展
  ],
  content: `<p>在这里尝试调整字体大小吧!</p>`,
});

onBeforeUnmount(() => {
  editor.destroy();
});

注意点:

  • 务必同时包含 TextStyleFontSizeExtension。我们的 FontSizeExtension 依赖于 TextStyle
  • 如果你使用了 StarterKit,它默认会包含 TextStyle。为避免潜在冲突或重复加载,最好配置 StarterKit 禁用它自带的 TextStyle (textStyle: false),然后手动添加 TextStyleFontSizeExtension。不过,根据 TipTap 版本和具体实现,直接添加 FontSizeExtension 可能也能正常工作(它通过 addGlobalAttributes 作用于 textStyle),但显式配置更稳妥。

4. 创建 Vue 组件 UI (下拉菜单)

现在来做个类似字体选择的下拉菜单,用 Vuetify 3 的 v-select 举例。

<template>
  <!-- FONT SIZE -->
  <v-select
    density="compact"
    variant="outlined"
    :items="fontSizes"
    label="字号"
    :model-value="selectedFontSize"
    @update:modelValue="changeFontSize"
    max-width="100px"
    class="mx-2 custom-small-select"
    dense
    hide-details
  />

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

<script setup>
  // ... 省略 import 和 editor/FontSizeExtension 定义 ...

  // 定义可选的字号列表
  const fontSizes = ref(['12px', '14px', '16px', '18px', '24px', '32px', '默认']); // 添加一个 "默认" 选项
  const selectedFontSize = ref('默认'); // 初始显示 "默认"

  // 改变字号的函数
  const changeFontSize = (size) => {
    if (size === '默认') {
      editor.chain().focus().unsetFontSize().run(); // 调用我们定义的取消命令
      selectedFontSize.value = '默认'; // 更新 UI
    } else {
      editor.chain().focus().setFontSize(size).run(); // 调用我们定义的设置命令
      selectedFontSize.value = size; // 更新 UI
    }
  };

  // 监听编辑器光标位置的 textStyle 属性变化,更新下拉菜单的显示
  watch(
    // 监听函数现在检查 fontSize 属性
    () => editor?.getAttributes('textStyle').fontSize,
    (newSize) => {
      // 如果 newSize 有值且在我们的列表中,就选中它
      if (newSize && fontSizes.value.includes(newSize)) {
        selectedFontSize.value = newSize;
      } else {
        // 否则,显示 "默认"
        // 这覆盖了多种情况:没设置字号(null)、设置了但不在列表里、多选区字号不一导致返回复杂对象等
        selectedFontSize.value = '默认';
      }
    },
    { immediate: true } // 立即执行一次,初始化下拉菜单状态
  );

  onBeforeUnmount(() => {
    editor.destroy();
  });

</script>

<style>
/* 可以加点样式让 v-select 小巧些 */
.custom-small-select .v-field__input {
  padding-top: 2px;
  padding-bottom: 2px;
  min-height: auto !important;
}
.custom-small-select .v-select__selection-text {
    font-size: 0.875rem;
}
</style>

代码解释:

  • fontSizes: 定义了一个包含常用字号值(字符串形式,带单位 px)和 "默认" 选项的数组。
  • selectedFontSize: 用 ref 存储当前下拉菜单选中的值。
  • changeFontSize: 当 v-select 的值变化时触发。
    • 如果选的是 "默认",调用 editor.chain().focus().unsetFontSize().run() 来清除字号设置。
    • 如果选的是具体的字号值(如 '16px'),调用 editor.chain().focus().setFontSize(size).run() 应用字号。
  • watch: 监听 editor.getAttributes('textStyle').fontSize。当光标移动或选区变化时,TipTap 会提供当前位置的 textStyle 属性。我们检查 fontSize
    • 如果有值并且这个值存在于我们的 fontSizes 列表中,就把 selectedFontSize 更新为这个值。
    • 其他所有情况(包括 fontSizenullundefined,或者是一个不在我们列表里的自定义值,或者选区包含多种字号导致无法获取单一值),都将下拉菜单重置为 "默认"。
  • immediate: true:确保 watch 在组件挂载后立即执行一次,根据编辑器的初始内容或状态设置下拉菜单的初始值。

安全建议

  • 限制可选字号 :使用预定义的字号列表(像上面例子里的 fontSizes ref)是最佳实践。避免让用户输入任意 CSS 值,以防注入不安全的代码或破坏布局。虽然 font-size 本身风险不高,但养成过滤输入的习惯总没错。
  • 使用合理单位 :推荐使用 px, rem, 或 empx 最直观,rem/em 则有助于响应式布局和可访问性。确保你 parseHTMLrenderHTML 能正确处理你选择的单位。上面的例子只处理了 px,如果需要支持其他单位,需要调整解析逻辑。

进阶使用技巧

  • 支持相对单位 (rem/em) : 如果想用 remem,修改 fontSizes 列表,并在 parseHTML/renderHTML 中确保单位能正确处理。注意 em 是相对于父元素,可能会导致嵌套复杂性。rem 相对于根元素,通常更易于管理。
  • 动态获取默认字号 : 不直接硬编码 "默认" 的对应行为(unsetFontSize),可以尝试获取编辑器容器的计算样式 font-size 作为参照,但这会增加复杂性。通常 unsetMark 或设置 null 就足够代表 "继承父级/浏览器默认"。
  • UI/UX 优化 :
    • 在下拉菜单中实时预览字号效果。
    • 对于不在列表中的字号(例如从外部粘贴进来的内容),可以在下拉菜单中显示原始值,或者仍然显示 "默认" 但保留实际样式。当前代码选择了后者。
    • 考虑为常用字号提供快捷键。
  • 整合到工具栏组件 : 把字号选择、字体选择、颜色选择等功能封装到一个统一的 Toolbar 组件里,使代码结构更清晰。

现在,你应该拥有一个功能完备的字体大小选择器了,就像控制字体一样方便!这个方法虽然比直接安装扩展多几步,但让你更深入地理解了 TipTap 的样式系统,也更有掌控力。