编辑插件
编辑插件在改稿时运行。它可以读文稿、听翻页和选区、通过撤销栈改文稿,并加上自己的面板或按钮。
官方编辑插件随编辑器提供,id 以 official: 开头。不登录也能用。
| 插件 | id | 作用 |
|---|---|---|
| 形状统计 | official:slide-stats | 统计全稿、当前页和选区里的形状 |
| 演讲者备注 | official:speaker-notes | 显示当前页的演讲者备注 |
导入插件是你加到账号里的文件。要先登录。每条导入插件只属于这个账号,而且不能改成公开。导入之后默认不勾选,要在列表里勾上才会运行。
打开 工作区 → 加载项。上面是官方插件,下面是 我的插件。导入… 选择一个 .js 文件。删除 会从账号里去掉它。

写一个
文件里调用一次 defineEditorPlugin。编辑器加载文件时,defineEditorPlugin 和 definePlaybackPlugin 已经在作用域里,不用自己 import。
id 以字母开头,后面可以是字母、数字、.、_、-,最长 64 个字符。不能含 :,这样就不会和官方 id 混在一起。文件最长 96 × 1024 个字符。name 保留到 80 个字符,description 保留到 240 个字符。同一个 id 再导入一次会覆盖原来的文件。
defineEditorPlugin({
id: "acme.wordCount",
name: "字数",
description: "当前页的字符数。",
version: "1.0.0",
activate(ctx) {
const line = document.createElement("div");
const panel = ctx.ui.addPanel({
title: "字数",
render(body) {
body.append(line);
},
});
const refresh = () => {
const pres = ctx.editor.getPresentation();
const slide = pres.slides[ctx.editor.getSlideIndex()];
let n = 0;
for (const layer of slide?.layers ?? []) {
for (const shape of layer.shapes) {
if (shape.kind !== "text") continue;
for (const para of shape.paragraphs ?? []) {
for (const run of para.runs ?? []) n += run.text?.length ?? 0;
}
}
}
line.textContent = String(n);
};
const offSlide = ctx.events.on("slidechange", refresh);
const offPres = ctx.events.on("presentationchange", refresh);
refresh();
return () => {
offSlide();
offPres();
panel.remove();
};
},
});activate 可以是 async。返回一个函数,用来卸掉监听。取消勾选时,面板和按钮会被拿掉。如果写了 deactivate(),它在这个函数之后调用,没有参数。
插件对象
| 字段 | 类型 | 必填 | 含义 |
|---|---|---|---|
id | string | 是 | 稳定 id。字母开头,随后是字母、数字、.、_、-。不含 :。最长 64 个字符 |
name | string | 否 | 加载项列表里的名字。缺省用 id。截到 80 个字符 |
description | string | 否 | 名字下面的一行说明。截到 240 个字符 |
version | string | 否 | 你自己的版本号。编辑器不比较它 |
activate | (ctx) => cleanup | void | Promise<cleanup | void> | 是 | 勾选时调用。cleanup 是 () => void |
deactivate | () => void | 否 | 卸掉之后调用,取消勾选或被覆盖时都会走到 |
activate(ctx)
| 字段 | 类型 | 用途 |
|---|---|---|
ctx.presentation | 文稿 | 和 ctx.editor.getPresentation() 是同一份正在编辑的文稿 |
ctx.editor | 编辑 API | 读文稿、切页、跑可撤销的命令 |
ctx.events | 事件 | 翻页、选区、命令、文稿替换 |
ctx.ui | 界面 | 面板、按钮,以及你自己的节点 |
ctx.shared | object | 这次编辑会话里,所有编辑插件共用的一个对象 |
ctx.locale | string | "en" 或 "zh-CN" |
ctx.log | (...args) => void | console.log,前缀是 [editor-plugin:<id>] |
插件通常读的文稿字段
ctx.presentation 是正在编辑的那一份,不是副本。坐标是幻灯片像素(size.width × size.height,常见 1280 × 720)。
| 字段 | 含义 |
|---|---|
id | 文稿 id |
size.width、size.height | 幻灯片宽高,像素 |
slides | 按文件顺序的页。下标 0 是第一页 |
slides[i].id、.name | 这一页的 id 和名字 |
slides[i].notes | 演讲者备注。这一页没有备注时没有这个字段 |
slides[i].show | 放映里隐藏这一页时为 false |
slides[i].layers[].shapes | 这一层上的形状,从底到顶 |
形状上有 id、name、kind、bbox(x、y、w、h)、visible、opacity。kind 是 text、image、rect、ellipse、line、path、polygon、group、chart、video、audio、ole、model3d、webEmbed 之一。text 形状有 paragraphs[].runs[].text。group 有 children。
改文稿用 ctx.editor.execute,这样可以撤销。直接改字段不会进撤销栈。
ctx.editor
| 函数 | 参数 | 返回 | 作用 |
|---|---|---|---|
getPresentation() | — | 文稿 | 正在编辑的文稿 |
getSlideIndex() | — | number | 当前页下标,从 0 起 |
getSlideCount() | — | number | slides.length |
gotoSlide(index) | index: number | void | 显示这一页。下标和 getSlideIndex() 同一套 |
getSelectedShapeIds() | — | string[] | 当前页上选中的形状 id |
execute(cmd) | cmd: EditCommand | void | 执行 cmd,并压进撤销栈 |
undo() | — | boolean | 撤销一条。栈空时返回 false |
redo() | — | boolean | 重做一条。没有可重做的命令时返回 false |
stackInfo() | — | { canUndo, canRedo, undoLabel?, redoLabel? } | 能不能撤销、重做,以及命令名 |
stageElement() | — | HTMLElement | 画布 |
hostElement() | — | HTMLElement | 编辑器根节点。用它的 ownerDocument 创建节点 |
EditCommand:
| 字段 | 类型 | 含义 |
|---|---|---|
label | string | 出现在历史列表里,也会跟着 command 事件发出去 |
apply(pres) | (pres) => void | 做出修改。pres 是正在编辑的文稿 |
revert(pres) | (pres) => void | 撤回这次修改 |
ctx.editor.execute({
label: "Rename slide",
apply(pres) {
pres.slides[ctx.editor.getSlideIndex()].name = "Renamed";
},
revert(pres) {
pres.slides[ctx.editor.getSlideIndex()].name = "Slide 1";
},
});ctx.events
| 函数 | 参数 | 返回 |
|---|---|---|
on(event, cb) | event 是下表里的名字。cb 收到该事件的数据 | () => void,调用即取消订阅 |
off(event, cb) | 和 on 时相同的 event 与 cb | void |
emitCurrentState() | — | void。再用当前页和选区发一次 selectionchange 和 slidechange |
activate 常常发生在第一次翻页事件之后。自己先刷新一次,或者调用 emitCurrentState()。
| 事件 | 数据 | 何时 |
|---|---|---|
slidechange | { slideIndex: number, total: number } | 当前页变了。total 是页数 |
selectionchange | { slideIndex: number, shapeIds: string[] } | 选区变了 |
command | { label: string, kind: "apply" | "undo" | "redo" } | 一条命令被执行、撤销或重做 |
presentationchange | 无 | 文稿对象被换掉 |
destroy | 无 | 编辑器要关掉。清理函数在这之后运行 |
ctx.ui
每次调用都返回 { remove(): void }。
| 函数 | 参数 | 出现在哪 |
|---|---|---|
addPanel(opts) | 见下表 | 画布左下角的浮动面板 |
addRibbonButton(opts) | 见下表 | 画布右上角的按钮 |
mount(node) | node: HTMLElement | 把 node 加到编辑器根节点上。位置由你自己定 |
addPanel:
| 字段 | 类型 | 必填 | 含义 |
|---|---|---|---|
title | string | 是 | 面板标题。再点一次会收起内容 |
render | (body: HTMLElement) => void | 是 | 往内容区里放节点。创建面板时调用一次 |
id | string | 否 | 记在面板元素上 |
initialOpen | boolean | 否 | 默认展开。设成 false 则一开始收起 |
order | number | 否 | 可以传入,但不会用来排序 |
面板按 addPanel 的调用顺序往下排。
addRibbonButton:
| 字段 | 类型 | 必填 | 含义 |
|---|---|---|---|
label | string | 是 | 按钮文字 |
onClick | (e: MouseEvent) => void | 是 | 点击回调 |
tooltip | string | 否 | title。缺省用 label |
编辑插件不会进导出的 HTML 放映。放映用的是 放映插件。
协作房间开着时,execute 改过的形状、文本、幻灯片、动画和批注会同步到其他人,并出现在对方的撤销栈里。面板、按钮和 ctx.shared 留在这一台浏览器上。放映里的计票、激光点和白板笔迹留在那场直播放映里。要写进文稿,就再执行一条命令。见 协作编辑。