完善公式
上一节我们简单讲述了公式的使用,并没有给出完整的样例,下面还是完善下相关步骤,我们是默认支持公式的编辑功能的哈,因此,我们只需要提供必要的符号即可:
符号所表达的含义是 mathlive 的command命令符号:
当我们点击符号的时候,执行 executeCommand('insert')即可:
const svgs = menuItem.querySelectorAll("svg");
svgs.forEach((svg) => {
svg.addEventListener("click", () => {
// 获取对应的 latex 命令
const command = <string>svg.dataset?.command;
latex.executeCommand("insert", command);
latex.focus();
});
});
如果大家使用时,有些公式显示异常,应该是字体文件没有引入,需要将全部字体放到 public/fonts 下(字体文件在 node_modelus/mathlive下有的):
导出的实现,就是基于html-to-image 库:
// 1. 将公式转为 HTML(一定先转HTML哈)
const html = convertLatexToMarkup(latex.value);
// ... 还需要将生成的 html 插入页面,并做好隐藏
// 调用 html-to-image 库
toBlob(html).then((blob) => {
// ... 生成的文件转成 konva 图片即可
});
拖拽排序
这个功能在很多网站都有,网上也有好多教程,大家搜一下就行了,给下大致实现思路:
/** 拖拽开始 - 添加拖拽样式 */
private imageBoxDragStart(e: DragEvent) {
const thumbItem = <HTMLDivElement>e.target;
nextTick(() => {
thumbItem.classList.add("thumb-draging");
});
}
private imageBoxDragEnter(e: DragEvent) {
// 父元素在 thumbBox 中,需要根据这个获取顺序,因为拖拽过程中 顺序是变得哦
const childrenList = [...thumbBox.children];
const dragingBox = thumbBox.querySelector(".thumb-draging")!;
const dragingBoxParent = <HTMLElement>dragingBox.parentNode;
const dragingIndex = childrenList.indexOf(dragingBoxParent);
// 被进入元素 index
const enterParent = <HTMLElement>target.parentNode;
const enterIndex = childrenList.indexOf(enterParent);
if (dragingIndex > enterIndex) {
thumbBox.insertBefore(dragingBoxParent, enterParent);
} else {
thumbBox.insertBefore(dragingBoxParent, enterParent.nextElementSibling);
}
}
private imageBoxDragEnd(e: DragEvent) {
// 完成之后 更新 layerManager 的 layerList
const root = this.draw.getRootBox();
const thumbBox = root.querySelector(".konva-root-container-thumb")!;
const order = [];
for (let i = 0; i < thumbBox.children.length; i++) {
const item = <HTMLDivElement>thumbBox.children[i];
order.push(parseInt(item.dataset.index!));
}
this.draw.getLayerManager().updateLayerList(order);
}
富文本实现
为了丰富文本展示形式,利用quill进行富文本编辑,使用 html-to-image 转成konva图片的形式,实现富文本.
// 1. 初始化 quill
const quill = new Quill(".richtext-editor #editor", {
placeholder: "input your content...",
modules: {
toolbar: "#toolbar-container",
},
theme: "snow",
});
// 2. 确认按钮转 blob
this.confirmHandle = () => {
return new Promise<void>((resolve) => {
toBlob(quill.root).then((blob) => {
// 为了下次编辑,还需要将当前的 Delta 转存到 shape 中
// 下次编辑 setContents(delta: Delta, source: string = 'api'): Delta
this.result = { blob, delta: quill.getContents() };
resolve();
});
});
};
元素的复制粘贴
我们的元素统一封装为 Group,因此,复制时,只需要拿到图形的ID属性,然后clone 一个新的图形,添加到画布上即可:
// 复制 - 当前选中的元素
public copy() {
const selected = this.draw.getKonvaGraph().getSelected();
if (!selected.length) return;
const data = selected.map((g) => g.clone());
const layerManager = this.draw.getLayerManager();
layerManager.setCopyGroupList(data);
}
// 粘贴
public async paste() {
const layerManager = this.draw.getLayerManager();
const copyGroupList = layerManager.getCopyGroupList();
const layer = this.draw.getLayer();
if (!copyGroupList.length || !layer) return;
this.draw.clearTransformer();
const newGroupList = <Konva.Group[]>[];
// 不然 直接将 groupList 的group 修改 ID 及 x y 重新添加到当前 layer 上
copyGroupList.forEach((group) => {
const newGroup = group.clone();
newGroup.setAttrs({
id: getUniqueId(),
x: newGroup.x() + 20,
y: newGroup.y() + 20,
});
newGroupList.push(newGroup.clone());
layer.add(newGroup);
groupTransformer(this.draw, newGroup.children[0]);
});
// 实现持续的复制粘贴
layerManager.setCopyGroupList(newGroupList);
this.draw.render();
}
剪切的思路与上复制粘贴一致,就是先执行 group.destroy ,然后再将数据放置到copyGroupList 中即可。
实现文件的导入导出
文件导入使用的是 pptxtojson这个库,
在线体验地址:https://pipipi-pikachu.github.io/pptxtojson/
文件导出使用的是 pptxgenjs这个库,
官网地址:Quick Start Guide | PptxGenJS
具体用法大家自行参考案例哈,这里不做细说了~
文件导出这里我重点说一下哈:
我们需要设置幻灯片尺寸,也可以自定义,但是!!!
真实的元素在页面上的位置,用的是像素!!!而不能直接转换成幻灯片的位置关系,这里建议使用 百分比 来处理,比较简单:
/** 工具函数 - 转换成 百分比 显示 */
function getPercent(type: "w" | "h" | "x" | "y", v: number) {
let result = null;
if (type === "h" || type === "y") result = (v / stageHeight) * 100 + "%";
else result = (v / stageWidth) * 100 + "%";
return <PptxGenJS.Coord>result;
}
同时,底层库不支持直接式添加文本,例如:
// 不支持直接式添加文本
slide.addShape("rect", {
text:"直接式添加文本",
rotate,
fill: { color: getColor(fill) },
x: getPercent("x", x),
y: getPercent("y", y),
w: getPercent("w", realWidth || width),
h: getPercent("h", realHeight || height),
});
而是需要将文本节点单独添加:
// 创建文本节点
slide.addText(text, {
valign: "middle",
align: "center",
rotate,
x: getPercent("x", x),
y: getPercent("y", y),
w: getPercent("w", realWidth || width),
h: getPercent("h", realHeight || height),
color: getColor(fill),
fontSize,
});
实现拖拽上传
拖拽上传的核心事件是dragover、drop,在释放时,可以通过 dataTransfer 读取拖拽的内容,具体的使用可以看MDN dataTransfer :
container.addEventListener("dragover", e => {
e.preventDefault(); ! important
});
container.addEventListener("drop", e => {
// 读取内容
const data = e.dataTransfer?.getData("text");
if (data) {
// 存在则是 string 文本
}else {
// 不存在则读取文件
const files = e.dataTransfer?.files;
}
});
总结
这篇主要丰富了Unipptx的功能,支持富文本、公式导出、拖拽上传及幻灯片排序等,还有难度较大的PPTX导入导出实现。整体来说,目前已经可完全编辑了,后面主要实现的功能有 预览、元素动画实现、协同实现。
最近工作较忙哈,更新较慢,大家多多谅解,欢迎大家fork代码,一起创作开发~
gitee:https://gitee.com/wfeng0/uni-pptx(未完全开发版)
官方文档:https://wf0.github.io/unippt.html(非完整文档)
大家有啥新的想法、创意,页可以留言讨论实现方案,一起完善相关功能~