【element-tiptap】导出word
前言:前面的文章 【element-tiptap】导入word并解析成HTML 已经介绍过如何在 element-tiptap 中导入 word。这篇文章来探究一下怎么将编辑器的内容导出成word
(一)创建菜单项
1、图标
首先上 fontawesome 这个网站上找一个合适的图标,把它的 svg 复制下来
然后创建 src/icons/export.svg,增加 width
、height
、fill
属性
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="32" viewBox="0 0 576 512">
<path fill="currentColor" d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 128-168 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l168 0 0 112c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 64zM384 336l0-48 110.1 0-39-39c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l80 80c9.4 9.4 9.4 24.6 0 33.9l-80 80c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l39-39L384 336zm0-208l-128 0L256 0 384 128z"/>
</svg>
2、创建扩展
src/extensions/export.ts
import { Extension } from '@tiptap/core';
import { Editor } from '@tiptap/vue-3';
import ExportDropdown from '@/components/menu-commands/export.dropdown.vue';
export interface ExportOptions {
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
export: {
exportToWord: () => ReturnType;
};
}
}
const Export = Extension.create<ExportOptions>({
name: 'export',
addCommands() {
return {
exportToWord:
() =>
({ editor }) => {
// 这里可以添加导出 Word 的具体实现
return true;
},
exportToPdf:
() =>
({ editor }) => {
// 这里可以添加导出 PDF 的具体实现
return true;
},
};
},
addOptions() {
return {
button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) {
return {
component: CommandButton,
componentProps: {
command: () => {
editor.commands.exportToWord();
},
isActive: editor.isActive('export'),
icon: 'export',
tooltip: t('editor.extensions.Export.tooltip'),
},
};
},
};
},
});
export default Export;
2、在根组件中引入当前的扩展
老生常谈的步骤,这里就不讲了啦
以及定义菜单的提示语
src/i18n/locales/zh/index.ts
Export: {
tooltip: '导出文档',
},
(二)实现导出 word
使用开源库 prosemirror-docx。
1、安装依赖
npm install prosemirror-docx --save
后来,我发现源码里还是有很多不完善的地方,官网还有处于 open 状态的提交记录😂
其中的 #35 还是很关键的,是修复的图片尺寸检测失败的 bug。所以还是将源码中的 src 文件夹都放在自己的项目里面
另外还需要安装一些别的依赖,直接使用 npm install xxx
安装即可
"docx": "^9.0.3",
"file-saver": "^2.0.5",
"image-dimensions": "^2.3.0",
2、图片 buffer
根据 export-tiptap-content-to-microsoft-word 上给出的示例代码,
import {
writeDocx,
DocxSerializer,
defaultNodes,
defaultMarks
} from "prosemirror-docx";
import { saveAs } from "file-saver";
const nodeSerializer = {
...defaultNodes,
hardBreak: defaultNodes.hard_break,
codeBlock: defaultNodes.code_block,
orderedList: defaultNodes.ordered_list,
listItem: defaultNodes.list_item,
bulletList: defaultNodes.bullet_list,
horizontalRule: defaultNodes.horizontal_rule,
image(state, node) {
// No image
state.renderInline(node);
state.closeBlock(node);
}
};
const docxSerializer = new DocxSerializer(nodeSerializer, defaultMarks);
const write = useCallback(async () => {
const opts = {
getImageBuffer(src) {
return Buffer.from("Real buffer here");
}
};
const wordDocument = docxSerializer.serialize(editor.state.doc, opts);
await writeDocx(wordDocument, (buffer) => {
saveAs(new Blob([buffer]), "example.docx");
});
}, [editor?.state.doc]);
使用这个库需要将图片转成 buffer
使用 Buffer.from()
方法,需要传递一个 base64 字符串。当前的图片,是保存在服务器返回的地址上,需要转换成 base64。起先我想在 getImageBuffer
方法中异步加载远程的图片资源,然后转成 base64,后来发现这个方法只能是同步的,所以就需要先遍历文档所有节点,将所有的图片都转成 base64。
addCommands() {
return {
exportToWord:
() =>
async ({ editor }) => {
// 给编辑器中所有的图片的src转换为base64
const images: Node[] = [];
editor.state.doc.descendants((node) => {
if (node.type.name === 'image') {
images.push(node);
}
});
for (const image of images) {
const src = image.attrs.src;
if (!src.startsWith('data:')) {
try {
const response = await fetch(src);
const blob = await response.blob();
const reader = new FileReader();
const base64 = await new Promise<string>((resolve) => {
reader.onload = () => resolve(reader.result as string);
reader.readAsDataURL(blob);
});
const tr = editor.state.tr;
const pos = editor.state.doc.resolve(0);
editor.state.doc.nodesBetween(0, editor.state.doc.content.size, (node, pos) => {
if (node.type.name === 'image' && node.attrs.src === src) {
tr.setNodeMarkup(pos, undefined, {
...node.attrs,
src: base64
});
}
});
editor.view.dispatch(tr);
} catch (error) {
console.error('Failed to convert image to base64:', error);
}
}
}
const opts = {
getImageBuffer(src) {
// base64转 Buffer
return Buffer.from(src.split(',')[1], 'base64');
}
};
const wordDocument = docxSerializer.serialize(editor.state.doc, opts);
await writeDocx(wordDocument, (buffer) => {
saveAs(new Blob([buffer]), "example.docx");
});
return true;
},
};
},
3、源码bug修复
使用这个代码出现的一些问题:
1、图片尺寸检测不出来,需要根据 这个提交 去修改代码
2、图片会变得很大,是因为图片的宽高没有传进去,使用的是默认的宽高
3、图片默认居中,对齐方式显示成 left
即可
src/utils/prosemirror-docx/schema.ts
// Presentational
image(state, node) {
const { src, width, height } = node.attrs;
state.image(src, width, height);
state.closeBlock(node);
},
src/utils/prosemirror-docx/serializer.ts
image(
src: string,
width: number,
height: number,
widthPercent = 70,
align: AlignOptions = 'left',
imageRunOpts?: IImageOptions,
) {
const buffer = this.options.getImageBuffer(src);
const dimensions = imageDimensionsFromData(buffer);
if (!dimensions) return;
const aspect = dimensions.height / dimensions.width;
if(!width) {
width = this.maxImageWidth * (widthPercent / 100);
}
this.current.push(
new ImageRun({
data: buffer,
transformation: {
width,
height: height || width * aspect,
},
...imageRunOpts,
}),
);
return this.addParagraphOptions({
alignment: align,
});
}
以上就可以实现导出成 word 的功能啦!
看下效果:
文档如下:
导出word如下:
(三)todoList、文本颜色、背景颜色处理
经过实际验证,文档中的todoList,docx
库是解析不了的,控制台会报错
需要将它处理成 [] xxx
的形式。
另外,背景颜色和文字颜色没有带到 word 中
src/utils/prosemirror-docx/serializer.ts
export class DocxSerializer {
nodes: NodeSerializer;
marks: MarkSerializer;
constructor(nodes: NodeSerializer, marks: MarkSerializer) {
this.nodes = {
taskList: (state, node) => {
node.forEach((child, _, i) => {
state.render(child, node, i);
});
},
taskItem: (state, node) => {
state.current = [];
const checkbox = node.attrs.checked ? '[x] ' : '[ ] ';
state.text(checkbox);
state.renderContent(node);
state.closeBlock(node);
},
...nodes,
this.marks = {
...marks,
textStyle: (state, node, mark) => ({
color: mark.attrs.color,
}),
highlight: (state, node, mark) => ({
shading: { fill: mark.attrs.color },
}),
};
}
}