今天这个功能很简单哦耶耶耶。格式刷快给我整自闭了,快来搞一搞简单的组件,增加一下信心
就是下面两个图标按钮的效果
这是 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;