canvas-editor 控件命令(Control Commands)API 全解析:从 getControlValue 到 executeJumpControl 的实战指南

原创2026-10-02 19:57:37143 阅读
文章标签:前端UI组件富文本

canvas-editor 控件命令(Control Commands)API 全解析:从 getControlValue 到 executeJumpControl 的实战指南

导读

canvas-editor 是一套基于 Canvas/SVG 的富文本编辑器,其「控件」体系(文本、下拉、复选、单选、日期、数字等交互单元)是构建表单式文档的核心能力。本篇技术指南以官方文档 docs/en/guide/control/command.md 为主体骨架,逐条拆解 instance.command 上全部 12 个控件命令的用法、参数、返回值与底层实现,并结合仓库源码与单元测试给出可验证的依据。读完本文,你将能熟练完成控件值的读写、批量赋值、删除、定位、高亮与跳转,独立编写一套可靠的控件数据操作代码。

一、控件命令总览:Command 门面与 CommandAdapt 适配层

在 canvas-editor 中,所有对外命令统一挂在 instance.command 上。控件相关命令集中定义在 Command.ts,它们并非直接实现,而是通过 CommandAdapt 中转绑定,避免外部直接接触编辑器内部上下文(源码注释明确写有“通过 CommandAdapt 中转避免直接暴露编辑器上下文”)。

从 Command.ts 可以看到,每个命令在构造时还会经过 wrap 包装:若开发者通过 setInterceptor 注册了拦截器,则每次调用命令前都会触发 interceptor(name, args) 回调,可用于命令埋点与审计。控件命令因此天然支持统一的调用追踪。

控件命令可分成两组:

分组 命令
获取数据(Get Data) getControlValue、getControlList
执行动作(Execute) executeRemoveControl、executeSetControlValue、executeSetControlValueList、executeSetControlExtension、executeSetControlExtensionList、executeSetControlProperties、executeSetControlPropertiesList、executeSetControlHighlight、executeLocationControl、executeInsertControl、executeJumpControl

所有动作型命令在 CommandAdapt.ts 中都有对应实现,且最终统一收敛到绘制层管理器 Control.ts 的 getValueById / setValueListById / setExtensionListById / setPropertiesListById / setHighlightList / getList / initNextControl 等方法上。下面按官方文档顺序逐一展开。

二、获取数据(Get Data)

2.1 getControlValue:获取控件值

官方用法(与 接口定义 对齐后):

const result = await instance.command.getControlValue(payload: IGetControlValueOption)

调用后返回一个结果数组(IGetControlValueResult),每一项包含:

{
  // 控件除 value 外的完整配置(type、placeholder、conceptId、groupId、extension……)
  ...control,
  value: string | null       // 控件“值”
  innerText: string | null   // 控件“内部文本”(人眼可读内容)
  zone: EditorZone           // 所在区域:HEADER / MAIN / FOOTER
  elementList?: IElement[]   // 文本类控件对应的元素列表(TEXT/DATE/NUMBER 才有)
}

IGetControlValueOption 支持四个定位维度,均为可选:

参数 类型 说明
id string 控件唯一 id,精确匹配单个控件
groupId string 控件组 id,批量命中一组控件
conceptId string 语义概念 id,跨 id 批量命中
areaId string 区域 id,按区域过滤

实现细节:CommandAdapt.getControlValue 直接委托给 draw.getControl().getValueById(payload)(见 CommandAdapt.ts)。而 Control.ts 的 getValueById 会同时遍历页眉(HEADER)、正文(MAIN)、页脚(FOOTER)三块数据,并递归下钻表格单元格(trList → tdList → td.value),因此表格内的控件也能被正确查到。

返回值按控件类型有所区分(源码 L1243-L1274):

  • TEXT / DATE / NUMBER:value 与 innerText 均为拼接后的文本,elementList 返回控件值对应的元素数组;
  • SELECT / CHECKBOX / RADIO:value 为以逗号分隔的 code 字符串(如 "a,b"),innerText 由 valueSets 中匹配到的显示文本拼接而成,不返回 elementList。

测试佐证:control.test.ts 中通过 getControlValue({ conceptId }) 断言 isOverwrite: false 场景下旧值不被覆盖、空控件被填入默认值;该文件 L221-L225 还验证了留痕开启时日期控件连续两次 executeSetControlValue 后 getControlValue 返回最新值 2026-03-03。

2.2 getControlList:获取所有控件

官方用法:

const controlList = await instance.command.getControlList()

返回 IElement<a href="https://link.gitcode.com/i/00ce4582240214962bd3a78741f922fd" target="_blank">],即文档中所有控件元素(含 control 配置与 controlId)的列表。底层实现见 [CommandAdapt.ts,调用 draw.getControl().getList()。与 getControlValue 的区别在于:getControlList 返回的是控件“元素本身”,适合遍历后再用 getControlValue 按 id 精确取值,或直接读取 element.control 配置。

三、执行动作(Execute)

3.1 executeRemoveControl:删除控件

官方用法:

instance.command.executeRemoveControl(payload?: IRemoveControlOption)

IRemoveControlOption 为 { id?: string; conceptId?: string }。实现分两条路径(见 CommandAdapt.ts):

  1. 传入 id 或 conceptId:从页眉、正文、页脚三块数据中递归查找匹配控件(同样会下钻表格),命中后调用 draw.deleteElementList 删除并重渲染;
  2. 不传参:删除当前光标所在位置的控件——要求光标为折叠态(startIndex === endIndex),且光标处元素必须带 controlId,随后通过 control.removeControl(startIndex) 删除并重新定位光标。

空参且不在控件上时命令静默返回,不会抛错,适合无脑兜底调用。

3.2 executeSetControlValue:设置控件值

官方用法:

instance.command.executeSetControlValue(payload: ISetControlValueOption)

ISetControlValueOption(接口定义):

参数 类型 说明
id / groupId / conceptId / areaId string 控件定位维度(同 getControlValue)
value string | IElement[] | null 新值;传字符串时按类型解析,传 IElement[] 可携带格式(如加粗文本),传 null 清空
isSubmitHistory boolean 是否提交撤销历史,默认按内部逻辑
isOverwrite boolean 是否覆盖已有值;false 时已有值不被改写,仅填充空控件

实现细节:CommandAdapt.setControlValue 将单个 payload 包装成数组后调用 setValueListById(<a href="https://link.gitcode.com/i/16ab0820e7c387b29378905dd947de89" target="_blank">payload])([CommandAdapt.ts),即单条赋值是批量赋值的特例。

测试佐证:

  • 对不存在的 id 调用不抛错(control.test.ts);
  • isOverwrite: false 时,已有值控件保持“旧值”,空控件写入“默认值”(control.test.ts);
  • 空值清空控件时占位符(placeholder)只替换不叠加,该用例直接对应 issue #1473 的回归修复(control.test.ts)。

3.3 executeSetControlValueList:批量设置控件值

官方用法:

instance.command.executeSetControlValueList(payload: ISetControlValueOption[])

与单条版本共用同一套 ISetControlValueOption,一次调用可混合使用 id / groupId / conceptId / areaId 命中多个控件并分别赋值。底层直接透传 setValueListById(payload)(CommandAdapt.ts),在一次渲染周期内完成,适合表单回填等需要“一键批量赋值”的场景。isOverwrite: false 的批量语义同样生效,见上文测试用例。

3.4 executeSetControlExtension / executeSetControlExtensionList:设置/批量设置控件扩展值

官方用法:

instance.command.executeSetControlExtension(payload: ISetControlExtensionOption)
instance.command.executeSetControlExtensionList(payload: ISetControlExtensionOption[])

ISetControlExtensionOption(接口定义)为 { id?, groupId?, conceptId?, areaId?, extension: unknown }。extension 是控件配置上的自由扩展字段(IControlBasic.extension?: unknown,见 Control.ts 接口),可用于挂载任意业务元数据(如审批状态、来源单据号),不影响控件渲染与取值。

实现细节:单条/批量分别调用 setExtensionListById(<a href="https://link.gitcode.com/i/bebf00b66b4bbc134279e71b277b616a" target="_blank">payload]) 与 setExtensionListById(payload)([CommandAdapt.ts),在 Control.ts 的 setExtensionListById 中完成查找与回写。

3.5 executeSetControlProperties / executeSetControlPropertiesList:设置/批量设置控件属性

官方用法:

instance.command.executeSetControlProperties(payload: ISetControlProperties)
instance.command.executeSetControlPropertiesList(payload: ISetControlProperties[])

ISetControlProperties(接口定义):

{
  id?: string
  groupId?: string
  conceptId?: string
  areaId?: string
  properties: Partial<Omit<IControl, 'value'>>  // 除 value 外的任意控件配置
  isSubmitHistory?: boolean
}

properties 可携带 IControl 中除 value 外的全部字段,例如 placeholder、disabled、required、underline、border、minWidth、prefix/postfix、validation 等(完整字段见 IControlBasic/IControlRule 接口)。这使其成为「动态改控件规则」的入口:例如将 disabled 置为 true 冻结输入、切换 placeholder 提示文案。

实现细节:分别调用 setPropertiesListById(<a href="https://link.gitcode.com/i/f7588089e72fa55ab58f9c281baf2a2a" target="_blank">payload]) 与 setPropertiesListById(payload)([CommandAdapt.ts)。

3.6 executeSetControlHighlight:设置控件高亮(按关键词)

官方用法:

instance.command.executeSetControlHighlight(payload: ISetControlHighlightOption)

ISetControlHighlightOption 即 IControlHighlight<a href="https://link.gitcode.com/i/a6162ac3bbd1616ac50959592e92693a" target="_blank">]([接口定义),结构与关键词规则相关:

{
  ruleList: {
    keyword: string      // 要高亮的关键词
    alpha?: number       // 背景透明度
    backgroundColor?: string  // 背景色
  }[]
  id?: string            // 限定到某个控件
  conceptId?: string     // 或按语义概念批量命中
}

实现细节:CommandAdapt.setControlHighlight 调用 draw.getControl().setHighlightList(payload) 后以 isSubmitHistory: false 重渲染(CommandAdapt.ts),即高亮属于“非历史”的视觉状态,不进入撤销栈。底层由控件搜索模块 ControlSearch.ts 维护高亮规则(setHighlightList)并负责 Canvas 上的匹配与绘制(computeHighlightList / renderHighlightList,见 Control.ts)。

测试佐证:control.test.ts 验证了无任何控件时调用 executeSetControlHighlight([{ ruleList: [{ keyword: 'test' }] }]) 不抛错。

3.7 executeLocationControl:定位并激活控件

官方用法:

instance.command.executeLocationControl(controlId: string, options?: ILocationControlOption)

ILocationControlOption 为 { position: LocationPosition },LocationPosition 枚举定义于 Common.ts:

取值 含义
before 控件内部最前
after 控件内部最后
outer-before 控件外面最前
outer-after 控件外面最后

实现细节:CommandAdapt.locationControl 按「页眉 → 正文 → 页脚」顺序查找首个 controlId 匹配的控件(表格内同样递归下钻),随后依次:切换所在区域(setZone)→ 写入位置上下文(position.setPositionContext)→ 用 range.replaceRange 折叠光标到目标位置 → 重渲染。因此调用后光标会精确落到控件内部或外部指定位置,相当于“编程式聚焦控件”,常用于表单校验后定位到出错的控件。

3.8 executeInsertControl:插入控件

官方用法:

instance.command.executeInsertControl(payload: IElement)

payload 是一个完整的控件元素,典型结构:

{
  value: '',
  control: {
    type: ControlType.TEXT,   // TEXT / SELECT / CHECKBOX / RADIO / DATE / NUMBER
    value: null,              // 初始值;也可传入 [{ value: '控件值' }]
    placeholder: '请输入……',
    // 其他 IControl 配置(conceptId、groupId、prefix、postfix、validation……)
  }
}

实现细节与限制(CommandAdapt.ts):

  • 编辑器处于只读(readonly)或禁用(disabled)状态时直接返回,不执行插入;
  • 对 payload 做深拷贝,并从光标所在锚点元素复制表格、行、列表、区域等上下文属性,保证插入后排版上下文正确;
  • 一个重要的嵌套约束:仅允许 TEXT 控件作为外层嵌套其他控件。若当前光标位于非 TEXT 控件内部又试图插入控件元素,会被拒绝——这是源码明确实现的校验逻辑(L2711-L2718)。

测试佐证:control.test.ts 覆盖了插入空值文本控件与插入带值控件(value: [{ value: '控件值' }])两种路径,均断言不抛错。

3.9 executeJumpControl:跳转到上/下一个控件

官方用法:

instance.command.executeJumpControl(payload?: { direction?: MoveDirection })

MoveDirection 枚举定义于 Observer.ts:

export enum MoveDirection {
  UP = 'top',
  DOWN = 'down',
  LEFT = 'left',
  RIGHT = 'right'
}

实现细节:CommandAdapt.jumpControl 调用 draw.getControl().initNextControl({ direction })(CommandAdapt.ts),由 Control.ts 的 initNextControl 依据当前光标上下文计算下一个控件的定位信息并激活。该命令非常适合实现“按 Enter/Tab 在多个控件间流转”的表单导航体验——例如在最后一个控件按下回车自动跳到下一个空控件。

四、命令速查表

命令 签名 核心参数 返回
getControlValue (payload: IGetControlValueOption) => Promise<IGetControlValueResult> id / groupId / conceptId / areaId { value, innerText, zone, elementList? }[]
getControlList () => Promise<IElement[]> 无 控件元素数组
executeRemoveControl (payload?: IRemoveControlOption) => void id / conceptId(空参删光标处控件) —
executeSetControlValue (payload: ISetControlValueOption) => void value、isSubmitHistory、isOverwrite —
executeSetControlValueList (payload: ISetControlValueOption[]) => void 同上,数组批量 —
executeSetControlExtension (payload: ISetControlExtensionOption) => void extension: unknown —
executeSetControlExtensionList (payload: ISetControlExtensionOption[]) => void 同上,数组批量 —
executeSetControlProperties (payload: ISetControlProperties) => void properties: Partial<Omit<IControl, 'value'>>、isSubmitHistory —
executeSetControlPropertiesList (payload: ISetControlProperties[]) => void 同上,数组批量 —
executeSetControlHighlight (payload: ISetControlHighlightOption) => void ruleList[{keyword, alpha?, backgroundColor?}]、id / conceptId —
executeLocationControl (controlId: string, options?: ILocationControlOption) => void position: before/after/outer-before/outer-after —
executeInsertControl (payload: IElement) => void 控件元素(仅 TEXT 可作外层嵌套) —
executeJumpControl (payload?: { direction?: MoveDirection }) => void top/down/left/right —

五、组合实战:一套完整的控件数据流

结合以上 API,可轻松实现“表单回填 → 校验定位 → 流转导航”的完整链路:

const editor = new Editor(container, options)

// 1. 批量回填表单(不覆盖已有值)
editor.command.executeSetControlValueList([
  { conceptId: 'OutpatientId', value: 'OP-2026-001', isOverwrite: false },
  { conceptId: 'ChestPainType', value: 'a,b' },          // SELECT/CHECKBOX 按 code 写入
  { groupId: 'ExamGroup', value: [{ value: '正常', bold: true }] } // 带格式写入
])

// 2. 读取回填结果(含所在区域与内部文本)
const result = await editor.command.getControlValue({ groupId: 'ExamGroup' })
console.log(result[0].value, result[0].innerText, result[0].zone)

// 3. 校验失败时定位到出错的控件(光标落入控件内部最前)
editor.command.executeLocationControl(errorControlId, {
  position: 'before'
})

// 4. 按回车在控件间流转
document.addEventListener('keydown', (evt) => {
  if (evt.key === 'Enter') {
    editor.command.executeJumpControl({ direction: 'down' })
  }
})

几点实战提示:

  • 定位维度:id 精确、conceptId 语义化批量、groupId 分组、areaId 限定区域,四者可在一次调用中组合使用,实现“按区域 + 按概念”的交叉过滤;
  • 历史记录:赋值类命令通过 isSubmitHistory 控制是否进入撤销栈;高亮、定位类命令默认不写入历史,避免污染撤销链路;
  • 只读保护:executeInsertControl 在只读/禁用状态下静默返回,业务侧如需提示可先通过 getOptions 检查当前编辑状态;
  • 与级联/校验协同:executeSetControlProperties 可动态修改 required、disabled、hide 等规则,与文档同目录下的 级联规则、校验规则 配合,可实现“控件 A 的值变化 → 动态隐藏/必填控件 B → 整体校验”的联动表单;控件的基础配置与外观选项可进一步参考 控件选项。

六、结语

控件命令是 canvas-editor 面向业务集成的关键入口:getControlValue / getControlList 负责读取,executeSetControlValue(List) / executeSetControlExtension(List) / executeSetControlProperties(List) 负责按值、扩展字段、配置属性三个粒度写入,executeRemoveControl / executeInsertControl / executeJumpControl / executeLocationControl / executeSetControlHighlight 负责结构、导航与视觉状态管理。所有命令均可在 Command.ts 与 CommandAdapt.ts 中溯源,并由 control.test.ts 提供行为级回归保障。开发者可直接将上文的示例代码接入自己的表单场景,实现稳定、可追踪的控件数据驱动。

登录后查看全文
canvas-editor