Skip to content

编辑插件 ​

编辑插件在改稿时运行。它可以读文稿、听翻页和选区、通过撤销栈改文稿,并加上自己的面板或按钮。

官方编辑插件随编辑器提供,id 以 official: 开头。不登录也能用。

插件id作用
形状统计official:slide-stats统计全稿、当前页和选区里的形状
演讲者备注official:speaker-notes显示当前页的演讲者备注

导入插件是你加到账号里的文件。要先登录。每条导入插件只属于这个账号,而且不能改成公开。导入之后默认不勾选,要在列表里勾上才会运行。

打开 工作区 → 加载项。上面是官方插件,下面是 我的插件。导入… 选择一个 .js 文件。删除 会从账号里去掉它。

加载项:先是官方编辑插件,下面是导入到这个账号的插件。

写一个 ​

文件里调用一次 defineEditorPlugin。编辑器加载文件时,defineEditorPlugin 和 definePlaybackPlugin 已经在作用域里,不用自己 import。

id 以字母开头,后面可以是字母、数字、.、_、-,最长 64 个字符。不能含 :,这样就不会和官方 id 混在一起。文件最长 96 × 1024 个字符。name 保留到 80 个字符,description 保留到 240 个字符。同一个 id 再导入一次会覆盖原来的文件。

js
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(),它在这个函数之后调用,没有参数。

插件对象 ​

字段类型必填含义
idstring是稳定 id。字母开头,随后是字母、数字、.、_、-。不含 :。最长 64 个字符
namestring否加载项列表里的名字。缺省用 id。截到 80 个字符
descriptionstring否名字下面的一行说明。截到 240 个字符
versionstring否你自己的版本号。编辑器不比较它
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.sharedobject这次编辑会话里,所有编辑插件共用的一个对象
ctx.localestring"en" 或 "zh-CN"
ctx.log(...args) => voidconsole.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()—numberslides.length
gotoSlide(index)index: numbervoid显示这一页。下标和 getSlideIndex() 同一套
getSelectedShapeIds()—string[]当前页上选中的形状 id
execute(cmd)cmd: EditCommandvoid执行 cmd,并压进撤销栈
undo()—boolean撤销一条。栈空时返回 false
redo()—boolean重做一条。没有可重做的命令时返回 false
stackInfo()—{ canUndo, canRedo, undoLabel?, redoLabel? }能不能撤销、重做,以及命令名
stageElement()—HTMLElement画布
hostElement()—HTMLElement编辑器根节点。用它的 ownerDocument 创建节点

EditCommand:

字段类型含义
labelstring出现在历史列表里,也会跟着 command 事件发出去
apply(pres)(pres) => void做出修改。pres 是正在编辑的文稿
revert(pres)(pres) => void撤回这次修改
js
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 与 cbvoid
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:

字段类型必填含义
titlestring是面板标题。再点一次会收起内容
render(body: HTMLElement) => void是往内容区里放节点。创建面板时调用一次
idstring否记在面板元素上
initialOpenboolean否默认展开。设成 false 则一开始收起
ordernumber否可以传入,但不会用来排序

面板按 addPanel 的调用顺序往下排。

addRibbonButton:

字段类型必填含义
labelstring是按钮文字
onClick(e: MouseEvent) => void是点击回调
tooltipstring否title。缺省用 label

编辑插件不会进导出的 HTML 放映。放映用的是 放映插件。

协作房间开着时,execute 改过的形状、文本、幻灯片、动画和批注会同步到其他人,并出现在对方的撤销栈里。面板、按钮和 ctx.shared 留在这一台浏览器上。放映里的计票、激光点和白板笔迹留在那场直播放映里。要写进文稿,就再执行一条命令。见 协作编辑。