【element-tiptap】字体放大缩小

今天这个功能很简单哦耶耶耶。格式刷快给我整自闭了,快来搞一搞简单的组件,增加一下信心
就是下面两个图标按钮的效果
这是 wps在线网站 WPS在线编辑
在这里插入图片描述
先看一下WPS实现的效果,和字号是联动的,点击放大的时候,就找上一个字号;缩小的时候就找下一个字号
在这里插入图片描述

一、字体放大组件

(一)首先先把这图标扒下来,放到项目中

src/icons/font-size-increase.svg

<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke-width="1.5"><g id="group-0" stroke="#333333" fill="#333333"><path d="M2 13.5L3.42163 10.0881M3.42163 10.0881L6.72308 2.1646C6.82564 1.91844 7.17436 1.91844 7.27692 2.1646L10.5784 10.0881M3.42163 10.0881H10.5784M10.5784 10.0881L12 13.5" stroke-linecap="round" stroke-linejoin="miter" fill="none" vector-effect="non-scaling-stroke"></path></g><g id="group-1" stroke="#0A6CFF" fill="#0A6CFF"><path d="M11 2.75H14.4765M12.75 4.54895V1" stroke-linecap="round" stroke-linejoin="miter" fill="none" vector-effect="non-scaling-stroke"></path></g></svg>

(二)创建扩展

我们来分析一下这个扩展需要做什么。
模版只需要一个按钮,点击按钮的时候,我们需要获取当前选中的文字的样式,取到其中的字体,然后在字体列表中,找一个更大的字体,给当前选中的文字设置样式。
上一篇文章 【element-tiptap】如何添加格式刷扩展? 中,有一个方法,_getSelectionEle 就是获取当前选中的节点的,这个方法目前是私有方法,我们可以把这个方法导出,这样就不用重写了

1、获取当前节点

src/extensions/format-painter.ts

class FormatCopy {

  // 复制选中元素的格式
  Copy(once: boolean, getJSON: () => JSONContent, editor: Editor) {
    // 使用新的command获取选中的元素
    const selectedNode = editor.commands.getSelectedElement()
  }
}

declare module "@tiptap/core" {
  interface Commands<ReturnType> {
    formatPainter: {
      /** 获取当前选中的元素 */
      getSelectedElement: () => ReturnType;
    };
  }
}

const FormatPainter = Extension.create({
  addCommands() {
    return {
      // 获取当前选中的元素命令
      getSelectedElement: () => ({ editor }) => {
        const s = window.getSelection();
		if (!s || !s.focusNode) return null;
		const range = s.getRangeAt(0)
		const endContainer = range.endContainer
		const selection = editor.state.selection;
		const matchedNode = selection.$to.nodeBefore;
		return matchedNode
      }
    };
  },
});
export default FormatPainter;

没有格式刷组件的童鞋,就把代码中的方法摘出来放到字体放大扩展中就可以

2、扩展文件 src/extensions/font-size-increase.ts
import type { Editor } from '@tiptap/core';
import { Extension } from '@tiptap/core';
import CommandButton from '@/components/menu-commands/command.button.vue';
import { DEFAULT_FONT_SIZES } from '@/utils/font-size';

const FontSizeIncrease = Extension.create({
  name: 'fontSizeIncrease',

  addOptions() {
    return {
      ...this.parent?.(),
      button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) {
        return {
          component: CommandButton,
          componentProps: {
            command: () => {
              editor.commands.increaseFontSize({
                chain: editor.chain(),
                editor: editor,
              });

            },
            icon: 'font-size-increase',
            tooltip: t('editor.extensions.FontSizeIncrease.tooltip'),
          },
        };
      },
    };
  },

  addCommands() {
    return {
      increaseFontSize: () => ({ chain, editor }) => {
        // 获取当前字体大小
        const currentEle = editor.commands.getSelectedElement()
        let currentFontSize = currentEle.marks[0]?.attrs.fontSize;
        if (!currentFontSize) {
          currentFontSize = '16px';
        }
        // 将DEFAULT_FONT_SIZES 从大到小排序
        const sortedFontSizes = [...DEFAULT_FONT_SIZES].sort((a, b) => parseFloat(b.value) - parseFloat(a.value));
        let currentIndex = sortedFontSizes.findLastIndex(size => size.value === currentFontSize);
        if (currentIndex === 0) {
          return;
        }
        while(sortedFontSizes[currentIndex]?.value == currentFontSize) {
          currentIndex--;
        }
        const newFontSize = sortedFontSizes[currentIndex]?.value;
        return chain().setMark('textStyle', { fontSize: newFontSize }).run();
      },
    };
  },
});

export default FontSizeIncrease; 
4、DEFAULT_FONT_SIZES

src/utils/font-size.ts

export const DEFAULT_FONT_SIZES = [
  { name: '初号', value: '56px' },
  { name: '小初', value: '48px' },
  { name: '一号', value: '34.67px' },
  { name: '小一', value: '32px' },
  { name: '二号', value: '29.33px' },
  { name: '小二', value: '24px' },
  { name: '三号', value: '21.33px' },
  { name: '小三', value: '20px' },
  { name: '四号', value: '18.67px' },
  { name: '小四', value: '16px' },
  { name: '五号', value: '14px' },
  { name: '小五', value: '12px' },
  { name: '六号', value: '10px' },
  { name: '小六', value: '8.67px' },
  { name: '七号', value: '7.33px' },
  { name: '八号', value: '6.67px' },
  // 数字字号
  ...generateNumericFontSizes(['5', '5.5', '6.5', '7.5', '8', '9', '10', '10.5', '11', '12', '14', '16', '18', '20', '22', '24', '26', '28', '30', '36', '48', '56', '72', '96']),
];

export const DEFAULT_FONT_SIZE = 'default';

const SIZE_PATTERN = /([\d.]+)px/i;

export function convertToPX(styleValue: string): string {
  // Convert numeric values to px
  return `${styleValue}px`;
}

function generateNumericFontSizes(sizes: string[]): { name: string, value: string }[] {
  return sizes.map(size => ({
    name: size,
    value: convertToPX(size),
  }));
}
5、src/extensions/index.ts
export { default as FontSizeIncrease } from './font-size-increase';
6、提示语定义

src/i18n/locales/zh/index.ts

FontSizeIncrease: {
  tooltip: '增大字号',
},

src/i18n/locales/zh-tw/index.ts

FontSizeIncrease: {
  tooltip: '增大字體',
},
7、添加到页面菜单中

这里我的代码结构已经做了一些调整,所以就不展示了,也挺简单,大家自行调整吧🌿🌿🌿

二、字体缩小组件

src/icons/font-size-decrease.svg

<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke-width="1.5"><g id="group-0" stroke="#333333" fill="#333333"><path d="M2 13.5L3.42163 10.0881M3.42163 10.0881L6.72308 2.1646C6.82564 1.91844 7.17436 1.91844 7.27692 2.1646L10.5784 10.0881M3.42163 10.0881H10.5784M10.5784 10.0881L12 13.5" stroke-linecap="round" stroke-linejoin="miter" fill="none" vector-effect="non-scaling-stroke"></path></g><g id="group-1" stroke="#0A6CFF" fill="#0A6CFF"><path d="M11.043 2.75H14.2164" stroke-linecap="round" stroke-linejoin="miter" fill="none" vector-effect="non-scaling-stroke"></path></g></svg>

其他的就和上面一样一样的,就不赘述了哈哈哈哈哈(偷懒)

三、后记改造

笑死家人们,自己又来推翻自己了🤣🤣🤣
上面的代码是有问题的,因为它会把所有的字体都设置成一样的,而观察一下WPS,它是每一段文字都在自身的基础上放大缩小。
所以选中的节点,我们需要遍历进行处理。
真的好复杂哇!
另外要说明的是,如果元素在 h1 – h6 级标题里面,建议就不要改大小了,标题的大小就保持咱们规定的大小即可。
语言描述一下代码的执行过程:
1、点击放大按钮,触发命令
2、获取选中的节点,由于文本节点是没有属性来标记自己是不是在标题里面,所以这里我们需要一个递归函数,给所有标题的后代节点都加上一个属性 isInHeading,标识节点在标题里面,方法我在这里先放一下,这样子大家可以看的比较直观

/**
 * 递归标记文档中的标题节点
 */
function markHeadingNodes(node: ProseMirrorNode, isInHeading = false): void {
  if (!node) return;

  const newAttrs: NodeAttributes = {
    ...(node.attrs || {}),
    isInHeading: isInHeading || node.type?.name === 'heading'
  };

  node.attrs = newAttrs;

  if (node.content?.content) {
    node.content.content.forEach(child =>
      markHeadingNodes(child, node.attrs.isInHeading)
    );
  }
}

3、需要调用 editor.state.doc.slice 方法,这个方法是获取我们实际选中的范围的节点,因为我们不可能选的都是完整的节点,可能会从节点的中间开始选,所以要用这个方法获取我们实际选中的内容
4、 这里有一个很重要的知识点!上面的代码中,设置样式用的是 setMark,但是这个方法不得行,它会把选区中的内容的样式全部一起修改,显然这不是我们想要的效果。那么我们只能用底层的 ProseMirror 提供的 addMark 方法。这个方法的使用,可以参考项目中依赖中的源码,大致是下面这样:

const tr = editor.view.state.tr;
const mark = editor.schema.marks.textStyle.create({ fontSize: '88px' });
tr.addMark(from, to, mark);
editor.view.dispatch(tr);    

5、我们选中的内容,也是嵌套的结构,一层一层的,所以还是需要递归进行处理,要获取节点的起始位置和结束位置,这样才能传递给 addMark 来增加样式。但是咱们并不能直接从节点上获取起始位置和结束位置,只能通过 editor.state.doc.nodesBetween() 获取 选区中所有的节点,然后和我们选中的已经截取好的内容进行对比,逐个找出 from 和 to 属性。ok,我知道这玩意儿,属实是有点难理解,看代码吧!
6、上面我们提到,需要递归对每一个节点进行 addMark 操作,但是,editor.view.dispatch(tr); 需要放在递归的外面,因为这样才会保存为一次历史记录,否则的话,点击撤回按钮,它就会一个节点一个节点的撤回~~
7、由于放大和缩小方法很类似,所以我把这两个按钮放在了一个扩展里面,这样也避免了很多重复代码,和 History 类似。其他文件的修改我就不一一列举了,仿照 History 即可

src/extensions/font-size-adjust.ts

import type {Editor} from '@tiptap/core';
import {Extension} from '@tiptap/core';
import CommandButton from '@/components/menu-commands/command.button.vue';
import {DEFAULT_FONT_SIZES} from '@/utils/font-size';
import {Node as ProseMirrorNode} from 'prosemirror-model';
import {Transaction} from 'prosemirror-state';

// 类型定义
interface FontSizeAdjustOptions {
  button: (params: { editor: Editor; t: (...args: any[]) => string }) => CommandButtonConfig[];
  bubble: boolean;
}

interface CommandButtonConfig {
  component: typeof CommandButton;
  componentProps: {
    command: () => void;
    tooltip: string;
    icon: string;
  };
}

type AdjustType = 'increase' | 'decrease';  // 字体调整类型:增大或减小

interface NodeAttributes {
  isInHeading?: boolean;  // 标记节点是否在标题内
  [key: string]: any;
}

/**
 * 计算新的字体大小
 * @param currentFontSize 当前字体大小
 * @param type 调整类型(增大/减小)
 * @returns 新的字体大小值
 */
function getNewFontSize(currentFontSize: string, type: AdjustType): string {
  if (!currentFontSize || currentFontSize === 'px') {
    currentFontSize = '16px';
  }

  const sortedFontSizes = [...DEFAULT_FONT_SIZES].sort((a, b) =>
    type === 'decrease'
      ? parseFloat(a.value) - parseFloat(b.value)
      : parseFloat(b.value) - parseFloat(a.value)
  );

  let currentIndex = sortedFontSizes.findLastIndex(size => size.value === currentFontSize);
  if (currentIndex === 0) return currentFontSize;

  while (sortedFontSizes[currentIndex]?.value === currentFontSize) {
    currentIndex--;
  }

  return sortedFontSizes[currentIndex]?.value || currentFontSize;
}


/**
 * 递归标记文档中的标题节点
 */
function markHeadingNodes(node: ProseMirrorNode, isInHeading = false): void {
  if (!node) return;

  // 标记节点是否在标题内
  const newAttrs: NodeAttributes = {
    ...(node.attrs || {}),
    isInHeading: isInHeading || node.type?.name === 'heading'
  };

  node.attrs = newAttrs;

  if (node.content?.content) {
    node.content.content.forEach(child =>
      markHeadingNodes(child, node.attrs.isInHeading)
    );
  }
}

/**
 * 处理文本节点的字体大小调整
 */
function processTextNode(
  node: ProseMirrorNode,
  editor: Editor,
  tr: Transaction,
  type: AdjustType
): void {
  const currentFontSize = node.marks[0]?.attrs.fontSize;
  const newFontSize = getNewFontSize(currentFontSize, type);

  if (!newFontSize) return;
  // node是截好的内容,需要找到它在原文中的位置
  editor.state.doc.nodesBetween(
    editor.state.selection.from,
    editor.state.selection.to,
    (n, pos) => {
      // n是完整的节点
      if (n.type.name == 'text' && (n === node || n.textContent.includes(node.textContent))) {
        const mark = editor.schema.marks.textStyle.create({fontSize: newFontSize});
        // 匹配 n.textContent中,所有的 node.textContent 的位置
        const matches = n.textContent.matchAll(node.textContent).toArray();
        let startIndex = 0;
        let endIndex = 0;
        for (const match of matches) {
          if (match.index + pos >= editor.state.selection.from) {
            startIndex = match.index
            endIndex = startIndex + node.textContent.length
            break
          }
        }

        const from = pos + startIndex;
        const to = pos + endIndex;
        tr.addMark(from, to, mark);
      }
    }
  );
}

/**
 * 递归处理节点及其子节点
 */
function traverseNodes(
  node: ProseMirrorNode,
  editor: Editor,
  tr: Transaction,
  type: AdjustType
): void {
  if (node.attrs?.isInHeading) return;

  if (node.type?.name === 'text') {
    processTextNode(node, editor, tr, type);
  }

  if (node.content?.content?.length > 0) {
    node.content.content.forEach(childNode => {
      traverseNodes(childNode, editor, tr, type);
    });
  }
}

/**
 * 获取选中的元素
 */
function getSelectedElements(editor: Editor) {
  const {from, to} = editor.state.selection;
  markHeadingNodes(editor.state.doc);
  return editor.state.doc.slice(from, to);
};


// Tiptap 扩展定义
const FontSizeAdjust = Extension.create<FontSizeAdjustOptions>({
  name: 'fontSizeAdjust',

  addOptions() {
    return {
      ...this.parent?.(),
      button({editor, t}: { editor: Editor; t: (...args: any[]) => string }): CommandButtonConfig[] {
        return [
          {
            component: CommandButton,
            componentProps: {
              command: () => {
                editor.commands.adjustFontSize({
                  chain: editor.chain(),
                  editor: editor,
                  type: 'increase',
                });
              },
              tooltip: t('editor.extensions.FontSizeAdjust.tooltip.increase'),
              icon: 'font-size-increase',
            },
          },
          {
            component: CommandButton,
            componentProps: {
              command: () => {
                editor.commands.adjustFontSize({
                  chain: editor.chain(),
                  editor: editor,
                  type: 'decrease',
                });
              },
              tooltip: t('editor.extensions.FontSizeAdjust.tooltip.decrease'),
              icon: 'font-size-decrease',
            },
          },
        ];
      },
      bubble: true,
    };
  },

  addCommands() {
    return {
      adjustFontSize: (argument: { type: AdjustType }) =>
        ({chain, editor}: { chain: any; editor: Editor }) => {
          // 获取选中的元素,截取实际选区的内容
          const eles = getSelectedElements(editor);
          // 创建一个transaction
          const tr = editor.view.state.tr;
          // 遍历选中的元素,修改字体大小
          traverseNodes(eles, editor, tr, argument.type);
          // 应用transaction
          editor.view.dispatch(tr);
          return true;
        },
    };
  },
});

export default FontSizeAdjust;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值