设计器
<xh-print-designer> 是可视化的模板设计器:画布按纸张显示一张页面模板的页眉、正文与页脚,旁边是物料、属性与问题列表。它只吃模板与数据、吐模板与事件,模板存在哪里、谁能改,都由宿主决定。属性、方法与事件的完整列表见设计器参考。
接入
pnpm add @xihan-print/designer @xihan-print/runtime @xihan-ui/styles @xihan-ui/designer-shell设计器用到 XiHan.UI 的设计器零件,样式要四份一起引入。definePrintDesigner() 注册设计器、它内部用到的预览与 UI 元素,重复调用不报错:
import type { PrintDesignerEventMap } from '@xihan-print/designer'
import type { DataField } from '@xihan-ui/designer-kit'
import { definePrintDesigner } from '@xihan-print/designer'
import { data, schema } from '../templates/delivery'
import '@xihan-ui/styles/index.css'
import '@xihan-ui/designer-shell/style.css'
import '@xihan-print/runtime/style.css'
import '@xihan-print/designer/style.css'
definePrintDesigner()把模板交给 schema,可绑定的字段树交给 fields:属性面板里选字段时从这棵树里选,物料面板也多出「字段」一组,字段可以直接拖上画布(见下文「字段物料」):
const fields: DataField[] = [
{ name: 'customer', label: '客户', kind: 'object', children: [{ name: 'name', label: '名称', kind: 'value' }] },
{ name: 'date', label: '日期', kind: 'value' },
{
name: 'items',
label: '明细',
kind: 'list',
children: [
{ name: 'name', label: '品名', kind: 'value' },
{ name: 'qty', label: '数量', kind: 'value' },
{ name: 'price', label: '单价', kind: 'value' },
{ name: 'amount', label: '金额', kind: 'value' },
],
},
]const designer = document.createElement('xh-print-designer')
designer.schema = schema
designer.fields = fields
designer.sampleData = data
designer.data = datasampleData是画布上显示用的示例数据,给了就按它显示实际值,不给时字段显示成{path}占位。data是切到预览态与打印时用的数据。
界面
外框从上到下是菜单栏、工具栏、中间三栏(左边物料与页面结构、中间画布、右边属性)、下方的问题列表与最底下的状态栏。各块面板停在哪一栏、显不显示、收没收起、浮没浮出,记在一份面板布局里,见下文「面板布局」;预览态只留菜单栏、工具栏与画布。
| 部分 | 位置 | 面板 id | 作用 |
|---|---|---|---|
| 菜单栏 | 最上面 | — | 文件、编辑、视图、插入、页面、帮助几张菜单,见下文「菜单栏」;正中放宿主的模板标题,右边一段放宿主别的常显内容。常开 |
| 工具栏 | 菜单栏下面 | — | 撤销重做;剪切、复制、粘贴、创建副本、删除;对齐(左、水平居中、右、顶端、垂直居中、底端)、分布(水平、垂直)与层级(置顶、上移一层、下移一层、置底),每条一颗图标钮;拖动工具、缩放、网格与吸附开关;设计与预览、页面设置、打印;末组是面板,设置与帮助在菜单栏里 |
| 物料 | 左栏 | palette | 按分组列出元素:每组是组名一行,下面是元素的方形小卡片,种类的图标在上、名称在下,按面板宽度自动排成几列;悬停或用键盘走到一张卡片时提示它的名称。点一下把元素加到视口中央,拖到画布上放在落点。「字段」一组按字段树一行一个地显示,见下文「字段物料」。扩展的物料来源另加的几组排在后面,见下文「扩展」 |
| 页面结构 | 左栏,物料下面 | outline | 按页面模板列出页眉、正文与页脚里的元素,同一处最上层的排最前;点一项选中它,在同一处里调层级,见下文「页面结构」 |
| 画布 | 中间 | — | 按纸张显示当前页面模板的页眉、正文与页脚,区的标签上切换页眉页脚的变体 |
| 标尺 | 画布上边与左边 | — | 毫米标尺;从标尺拖出参考线,参考线存进页面模板,只用于对位、不打印。设置里可以关掉 |
| 翻页条 | 画布右侧中间 | pages | 画布上的浮动件,只能显示或隐藏。上一页、页码、下一页,新增、复制、删除页面模板。页码钮上写「第几个 / 共几个」,点它列出全部页面模板,点一项切过去,下面是上移、下移页面 |
| 小地图 | 画布左下角 | minimap | 画布上的浮动件,只能显示或隐藏。按页显示:当前页面模板的纸一块淡色,当前显示的页眉页脚变体与正文里的元素各一块,框出画布此刻看到的范围;在上面按下或拖动,视口中心移到那里。键盘上,画布没有选中元素时按方向键平移视口 |
| 选中工具条 | 选中框上方 | — | 选中元素时浮出:层级四颗钮、创建副本、复制与删除;文字类元素多出字号(钮上写当前字号)、文字样式与左中右对齐,多选多出对齐六颗与分布两颗,见「画布操作」 |
| 尺寸徽标 | 选中框下方 | — | 选中元素时常驻宽 × 高(mm),单选时可以直接改,见「画布操作」 |
| 属性 | 右栏 | properties | 编辑选中的元素,多选时只显示共同的属性;没有选中时编辑页面设置;表格编辑态里编辑选中的表格部位 |
| 历史记录 | 右栏,属性下面 | history | 缺省隐藏,从「面板」菜单打开。按先后列出撤销历史的每一步,当前位置那一行选中,选一行就跳到那一步;已撤销的一步字色减淡。面板头是「清空历史」,先确认再清空。只读与预览态时只能看 |
| 问题 | 下方 | issues | 校验问题列表,点一下切到问题所在的页面模板并定位到元素;错误、警告数在面板头上,点一下只看或不看这一级 |
| 状态栏 | 最下面 | — | 左边写选中的对象:没有选中时是当前页面模板与上面的元素个数,选中一个是种类与位置尺寸(mm),选中多个是个数;右边写打印客户端的连接状态(给了 printClient 时)与缩放,点缩放回到实际尺寸。设置里可以关掉 |
物料、页面结构、属性、历史记录与问题是外框里的卡片:面板头写图标与标题,属性面板的标题下面写正在编辑的对象,「?」里是这块面板的用法。
面板布局
- 面板头上的「×」隐藏这块面板,收起钮(或双击面板头)收成只剩标题条,「更多」菜单里换到别的栏、调先后、浮出成小窗或停靠回去。按住面板头拖:拖到外框边上换栏,拖到两块面板之间调先后,拖到画布上浮出成小窗;小窗不出外框。
- 工具栏的「面板」菜单列出物料、属性、问题、翻页条、小地图、页面结构与历史记录,勾选的显示;末尾是「显示 / 隐藏所有面板」(Ctrl / ⌘ + \)、「还原面板显隐」(Ctrl / ⌘ + Shift + \,位置与大小不动)与「恢复默认布局」。
- 布局一改,设计器就把整份布局写进设置的
layout.panels(DESIGNER_SETTING_KEYS.layout),派发settings-change,宿主存起来、下次随settings赋回即可;给了settingsStorageKey时设计器自己存。拖分隔条只在松手时写一次。 - 宿主用
defaultLayout给缺省布局:用户没存过布局时用它,「恢复默认布局」也回到它。用settingsPolicy管控:layout.panels锁定整份布局,layout.panels.<面板 id>写hidden就没有这块面板、写locked就固定这块面板。面板 id 见包入口导出的PRINT_DESIGNER_PANELS。
export function wireLayout(designer: XhPrintDesigner): void {
// 缺省收起问题列表;宿主的业务里用不到翻页条,也不许用户把它开出来
designer.defaultLayout = { version: 1, hidden: [PRINT_DESIGNER_PANELS.issues] }
designer.settingsPolicy = { [`${DESIGNER_SETTING_KEYS.layout}.${PRINT_DESIGNER_PANELS.pages}`]: 'hidden' }
}窗口窄时左右两栏收起,从「面板」菜单打开时以抽屉盖在画布上;浮出的面板在窄屏时按停靠位置收进抽屉。
工具栏缺省只显示图标,设置里可以换成文字或图标加文字;翻页条与选中工具条只显示图标。没有图标的按钮写一段短文字,比如页码钮写「1 / 3」、字号钮写当前字号,按钮的名字里含着这段字。按钮的名字与快捷键报给读屏,悬停或键盘聚焦到按钮时提示命令的文字与快捷键;会删掉内容的命令按危险色显示。工具栏放不下时,末尾的按钮收进「更多」。
有几条命令点开的不是一个动作,而是锚在按钮上的弹层或菜单:
- 「页面设置」是弹层,改当前页面模板的纸张、方向与页边距,与属性面板里「纸张」一组是同一批属性,改动同样记撤销。
- 对齐、分布与层级每条一颗钮,一字排开,工具栏或选中工具条放不下时多的收进它们各自的「更多」。翻页条上的页码与选中工具条上的字号是菜单。
扩展登记的面板也放进外框,同样记在面板布局里,见下文「面板」。各区的属性见元素,属性面板的分组与中文名与它一致。
菜单栏
菜单栏在外框最上面,菜单按命令表排:
| 菜单 | 条目 |
|---|---|
| 文件 | 保存;导入字段;打印;设置 |
| 编辑 | 撤销、重做;剪切、复制、粘贴、删除、创建副本;全选;编辑 JSON(开发者模式开着时) |
| 视图 | 设计、预览;放大、缩小、适合窗口、实际大小;拖动;网格、吸附;面板(子菜单,就是工具栏上「面板」那一张)、外观(子菜单:显示工具栏与状态栏) |
| 插入 | 各种元素,按物料分组各成一组,扩展的元素排在它的分组里;页眉、页脚(子菜单,见下) |
| 页面 | 纸张、方向(子菜单,单选)、页面设置;上一页、下一页;新增、复制、删除页面;上移、下移页面 |
| 帮助 | 开始引导、帮助、快捷键;关于 |
- 条目写图标、名称与快捷键,开关式的命令(设计与预览、拖动、显示开关)带勾,此刻不可用的灰掉;没有条目的菜单不出现。预览态照样有菜单栏,从「视图」切回设计。
- 插入菜单里选一种元素,与点物料一样放在视口中央并选中。页眉、页脚的子菜单与画布上区标签的变体菜单做同一件事:上半是已有的变体,单选,当前显示的打勾;下半是还没有的变体,写「新增变体 · 首页」这样,选了新增并切过去;显示的不是常规时,末尾是「删除变体」。
- 页面菜单的纸张列出各种纸张预设,方向是纵向与横向,当前页面模板的打勾,选中写进当前页面模板、记一步撤销;自定义纸张与页边距在「页面设置」里改:它取消选择,在属性面板里编辑当前页面模板,属性面板藏着时把它显示出来。只读与预览态时插入菜单与纸张、方向不可用,只读时「页面设置」照样能看。
- 「快捷键」「关于」打开设置对话框的那一页。「视图 → 外观」的两条显示开关改的是设置(
toolbar.visible、general.statusBar),和在对话框里改一样派发settings-change;settingsPolicy藏起或锁定了那一条时它不可用。 - 键盘:焦点在设计器里时按 F10 进菜单栏,焦点落到第一张菜单上;左右键换菜单,下、Enter、Space 展开,Esc 收起;再按 F10 或 Esc 回到进来前的地方。不用 Alt 与 Alt + 字母,它们留给浏览器。
- 窗口窄时整条收成一颗「菜单」钮,点开后每张菜单是一个子菜单。
- 菜单栏常开,设置里没有关掉它的开关。
- 宿主的内容写成设计器元素的子元素,按
slot放进外框,后来加的也一样:- 模板标题写
slot="menubar-center",按整条菜单栏居中,放不下截断、再窄就藏起,窄屏收成「菜单」钮时同样居中。 - 保存状态写
slot="status-end",在状态栏右端、打印自己的状态项(打印客户端连接状态、缩放比例)前面;设置里关掉状态栏时跟着不显示。 - 别的要常显的写
slot="menubar-end",在菜单栏右边一段。 - 上面的示例把标题与保存状态放在这两处,见下文「保存与事件」。
- 模板标题写
宿主的命令写成一个扩展的命令,placement 写 menubar:<菜单 id> 落进那张菜单(file、edit、view、insert、page、help;打印的两张菜单的 id 是导出的 PRINT_MENUBAR_MENUS),不在缺省排列里的自成一组,排在那张菜单收尾的一组之前,只有一组的菜单接在后面。要定位置时用 commandLayout['menubar:<菜单 id>'] 写整份排列,给空数组就不要这张菜单:
// 宿主自己的「新建」「返回列表」挂进文件菜单,新建同时上工具栏;再把文件菜单排成新建与保存、返回列表、设置三组
export function hostCommands(designer: XhPrintDesigner, actions: { readonly create: () => void, readonly back: () => void }): void {
designer.extensions = [{
id: '@acme/host',
commands: [
{ id: '@acme/host.new', label: '新建', placement: ['menubar:file', 'toolbar'], run: actions.create },
{ id: '@acme/host.back', label: '返回列表', placement: ['menubar:file'], run: actions.back },
],
}]
designer.commandLayout = { 'menubar:file': [['@acme/host.new', 'save'], ['@acme/host.back'], ['settings']] }
}页面结构
页面结构按页面模板分组,每个页面模板下分页眉、正文与页脚;页眉页脚加了首页、末页、奇偶页这些变体时,再按变体分。页眉、页脚里一个元素都没有时不列。同一处的元素按层级排,最上层的排最前;元素有名称时写名称、副文本是种类,没有名称时写「种类 · id」,行首是种类的图标。
- 点一项选中元素:它不在当前页面模板或当前显示的页眉页脚变体上时,先切过去再选中,并滚进可见区。悬停或用方向键走到一项时,画布上标出它。
- 能多选,与画布上的选择是同一份:按住 Ctrl(macOS 为 ⌘)点一项或按 Ctrl / ⌘ + Space 加选、取消,按住 Shift 点一项从上一次点的那项选到这一项,分组行不算。新选上的第一个滚进可见区,只取消几项时画布不动。选择只留当前页面模板与当前页眉页脚变体里的元素:加选别处的元素时切过去,原来的选择随之换掉。
- 拖动一项,或按 Alt 加上下键,在同一处里调层级,记一步撤销;要把元素从页眉挪到正文、或挪到别的页面模板,仍在画布上拖。锁定的元素不能挪、也不能从这里删。
- Delete 删除焦点所在的一项,焦点在选中的项上时删全部选中的,记一步撤销。
- 搜索框按名称与种类过滤。只读与预览态时只能选、不能删与挪。
字段物料
给了 fields 时,物料面板最后多出「字段」一组,按字段树显示:带子字段的可以展开、收起,键盘与读屏按树的约定,搜索也能按字段名称与路径找。点一下把绑好字段的元素加到视口中央,拖到画布上放在落点:
| 字段 | 生成 |
|---|---|
value,content 不写或为 text | 文本,内容绑定这个字段 |
value,content 为 image | 图片,地址取这个字段 |
value,content 为 barcode | 条码(缺省码制),内容绑定这个字段 |
value,content 为 qrcode | 二维码,内容绑定这个字段 |
list | 表格:dataField 指向它,各列取它的 value 子字段(进入对象、不进入嵌套的列表),列标题取字段名称,列宽均分表格宽度;content 为图片、条码、二维码的子字段对应同类单元格 |
object | 不生成元素,只作分组 |
- 字段写了
label时,新元素的名称就是它;条目上同时显示字段路径。 - 列表的子字段相对数组的一行,只作浏览、不能拖出:要放行里的字段,先放表格,再在属性面板里改列。
content是 XiHan.UI 字段树DataField的可选项,说明这个值代表什么;type是数据类型,与生成哪种元素无关。
导入字段
物料面板头的「导入字段」(也在文件菜单里)打开导入对话框,从一份 JSON 样例或一份 JSON Schema 生成字段树,不用宿主手写 fields:
- 「JSON 样例」「JSON Schema」两页各有一个输入框,粘贴进来,或把
.json文件拖到输入框下面、点它选一个,文件在本机读,不发请求。解析失败时写出第几行第几列。 - 预览表一个字段一行,可以勾掉不要的、改显示名与类型;值字段的「内容」选文本、图片、条码或二维码,决定拖上画布生成哪种元素(见上表)。从样例推断时,图片地址(
data:image/…或以图片扩展名结尾的链接)预先标成图片。 - 已有字段时可选「合并到现有字段」(缺省)或「替换现有字段」:合并时同名字段保留现有的显示名,种类不同的保留现有的并在表下列出;替换可能让模板里已有的绑定找不到字段,问题列表会报出来。
- 来自样例时可以勾上「同时作为示例数据」(缺省勾上),画布马上按这份样例显示实际值。
- 按「导入」后设计器换上新的
fields并派发fields-change { fields };带了示例数据的同样换上sampleData并派发sample-data-change { sampleData }。字段与示例数据仍由宿主持有,不写进模板:宿主存起来,下次赋回。不记撤销。 - 字段来自后端元数据、不许用户改时写
fields-readonly:面板头没有这颗钮,命令import-fields不可用。只读与预览态时同样没有。 openFieldImport(source?)从别处打开对话框,source是sample、json-schema或扩展字段来源的 id。
export function wireFields(designer: XhPrintDesigner, store: FieldStore, fromBackend: boolean): void {
designer.fields = store.fields
designer.sampleData = store.sample
// 字段来自后端元数据时不许在设计器里改
designer.fieldsReadonly = fromBackend
designer.addEventListener('fields-change', (event) => {
store.save({ fields: (event as CustomEvent<{ fields: readonly DataField[] }>).detail.fields })
})
designer.addEventListener('sample-data-change', (event) => {
store.save({ sample: (event as CustomEvent<{ sampleData: JsonValue }>).detail.sampleData })
})
}页码的格式是一行文本,行尾的「插入」菜单把当前页码 {{$page}} 或总页数 {{$pages}} 插到光标处,下面按第 1 页、共 3 页显示示例,数字写法跟着「数字写法」一栏。
边框分上、右、下、左四个页签,每边一个开关:打开时这一边照已有的边画线,还没有边时是 0.2 毫米黑色实线,下面可以改线宽、线型与颜色;四边都关掉就是没有边框。多选的元素边框不同时,从四边都不画开始改,改动对每个元素整体写入。
值格式先选种类:常用格式(数字、百分比、日期、人民币大写、数字字形)在下面填参数并显示示例;值映射是 XiHan.UI 设计器零件内建的选项表(options,不带禁用列),「数据值、显示文本」两列,可以新增、删除与上移下移,下面填没有命中时显示的文本;表达式以 value 引用待格式化的值,边输入边检查。换种类时整体换成这一种的缺省值,选「不设格式」即去掉格式。
画布操作
- 选择:点选元素;按住 Shift、Ctrl 或 ⌘ 点击切换某个元素的选中;在空白处拖动框选。
- 移动与变换:拖动元素移动,拖控制柄缩放;文本、图片、矩形、椭圆、条码与二维码还能旋转。锁定的元素不能拖动与缩放。
- 选中工具条:选中元素时,选中框上方浮着一条工具条:都有层级(置顶、上移一层、下移一层、置底)、创建副本、复制与删除;选中的全是文本、长文本、富文本或页码这类带文字样式的元素时,前面多出字号、粗体、斜体、下划线、删除线与左中右对齐,对选中的每个元素写同一个值,记一步撤销;选了几个元素、里面有别的种类时,多出对齐与分布。放不下的钮收进工具条自己的「更多」。贴顶放不下时翻到下方,拖动中与只读时收起。在画布上按 Alt+F10 把焦点交给它,在工具条里按 Esc 回到画布。排列怎么改见下文「命令」。
- 尺寸徽标:选中元素时,选中框下方常驻宽 × 高(mm)。单选、元素能缩放时是两个数字框:点加减钮或按上下方向键改 1 mm,Shift 加上下方向键改 10 mm,连续改并成一步撤销;输入数字后按 Enter 或离开数字框才改,单独一步撤销,按 Esc 放弃。改的时候左上角不动,不小于这种元素的最小尺寸,二维码改一边另一边跟着变。锁定的元素、多选与只读时只显示尺寸。在选中工具条里按 Tab 进到徽标。
- 换区:元素中心拖进页眉、正文或页脚,就放进那个区;表格、长文本与富文本只能留在正文区。
- 页眉页脚:在区的标签上切换当前编辑的变体(缺省、首页、末页、奇数页、偶数页),可以新增或删除变体;拖动区的边界改高度。
- 平移与缩放:设计器打开、换页面模板或重新装载模板时按实际大小显示(缩放 100%),纸的左上角落在画布左上角,上边与左边留出边距,左边让出页眉页脚标签;画布改尺寸时不重排。工具栏的「适合窗口」(
zoom-to-fit,键 0)把整张纸缩放到画布里,此后画布改尺寸时跟着重新适配,直到平移、缩放或在画布上操作。滚轮平移,Ctrl / ⌘ + 滚轮以指针为中心缩放;按住空格或鼠标中键拖动平移。工具栏的「拖动」(手形,键 H)开着时,在画布上哪里按住拖动都是平移,按在元素、控制柄与表格把手上也一样:不选中、不挪元素,只点一下不取消选择,双击不进表格编辑;选中工具条、尺寸徽标与页眉页脚变体菜单照常能点。拖动途中按 Esc 只取消这次拖动,视口回到原处;不在拖动时按 Esc 关上拖动工具。 - 吸附:拖动、新建与缩放元素时吸附到网格间距(设置「打印」一页,缺省 1 mm,写 0 不吸附网格);「吸附对齐」打开时还吸附其它元素、页边距、区边界与参考线,并显示对齐线。工具栏上的「网格」开关显示或收起网格线,「吸附」开关就是「吸附对齐」,两者改的都是设置。
- 表格编辑态:双击表格或选中后按 Enter 进入。可以分别选中列、表头、表体、表尾行与表尾单元格,在属性面板里编辑;拖列边界改列宽;右键菜单里插入、删除、左右移动列,插入、删除表尾行,合并与拆分表尾单元格。按 Esc 退出。
- 右键菜单:在空白处、元素上与表格编辑态里各有一套,带快捷键的条目在行尾显示快捷键。设计器先派发可取消的
blank-contextmenu、element-contextmenu或table-contextmenu(表格编辑态里,右键处的部位先被选中),宿主取消后不弹自带的菜单,可以换成自己的。
设置
设置是用户偏好,每人每机一份,不随模板保存。openSettings(section?) 打开设置对话框,左边一个分区一页:
- 「基础」(
general):主题(跟随页面、跟随系统、亮色、暗色)、主题色(跟随页面、八种预设与自定义颜色)、状态栏开关、自动保存、开发者模式与分页、渲染两条调试日志。开发者模式缺省关,关着时编辑 JSON 这类调试查看的入口都不出现,两条调试日志也不能开,见下文「调试日志」。 - 「语言」(
language):设计器的界面语言,缺省跟随页面;也可以跟随系统,或选 XiHan.UI 的十种语言之一。只作用于这台设计器:设计器自己、外壳零件与里面的元素(含弹出的菜单、对话框这些浮层)一起换,页面别处不变;跟随系统按浏览器的语言偏好挑最接近的一种,挑不到用英文。语言包选了才按需加载。键是language.locale(DESIGNER_SETTING_KEYS.language)。 - 「布局」(
layout):逐块面板一行,改显示、停在哪一栏、先后与收起,表下「恢复默认布局」;改的就是上文「面板布局」那一份。 - 「工具栏」(
toolbar):按钮的显示方式(图标、文字、图标和文字,缺省只显示图标)、显示工具栏,以及按钮表:藏起不用的按钮、换先后与分组,「恢复默认」回到宿主排好的样子。显示方式与显示工具栏只走设置,没有单独的属性;画布上的翻页条总是只显示图标。按钮表从宿主用commandLayout排好的工具栏起步,只管顶部工具栏,菜单栏、选中工具条与右键菜单不在里面。 - 「打印」(
print):打印方式、显示网格、网格间距、吸附对齐、显示边距线、标尺。打印方式只在给了printClient时才有,选本地客户端(缺省)还是浏览器:本地客户端直接交给打印机,连不上时退回浏览器的打印对话框;选浏览器时print()总是弹出浏览器的打印对话框。网格缺省不显示,间距缺省 1 mm,按当前缩放挤得太密时改按间距的整数倍画;边距线与标尺缺省显示。 - 「本地客户端」(
printClient):给了printClient才有这一页,写连接状态:已连接、未连接或未安装。「测试连接」调宿主给的printClient的connect()连一次;没找到客户端(CLIENT_UNAVAILABLE)时写未安装,这时也可能是装了没有运行。没连上时下面有「下载客户端」链接,缺省指向本站的本地打印客户端一页(PRINT_CLIENT_DOWNLOAD_URL),宿主用clientDownloadUrl(attributeclient-download-url)换成自己的地址。端口与来源授权仍由宿主建连接时与客户端管,设计器不自己建连接。 - 扩展登记的分区,排在后面。分区 id 与
general、language、layout、toolbar、help、shortcuts、about、print、printClient或别的扩展重名时,给extensions赋值就抛DesignerExtensionError。 - 「帮助」(
help):几段操作说明,下面是「开始引导」与「帮助」两颗钮,按了先关上对话框再开始引导或打开本站的设计器一页。 - 「快捷键」(
shortcuts):按命令表列出全部快捷键,分通用、编辑、打印设计器与扩展几组,第二快捷键写在同一行;打印设计器那组末尾补上不是命令的 Ctrl / ⌘ + C、X、V 复制剪切粘贴,Backspace 删除与 Enter 编辑表格;保存是外壳的命令,列在「通用」里;「通用」里方向键一行写明没有选中时平移画布,Esc 一行写明取消拖动、退出表格编辑或取消选择,其余照 designer-shell 的通用行(含 F10 进菜单栏)。 - 「关于」(
about):打印设计器的版本(包入口导出的PRINT_VERSION,与包版本一致)与许可证,后面接扩展给的几行。
export function openCanvasSettings(designer: XhPrintDesigner): void {
designer.openSettings(PRINT_SETTINGS_SECTION)
}值是扁平的键值,键是「分区 id.条目 id」,如 general.theme、print.showGrid,没写的键取缺省值。宿主给缺省、锁定条目或读值时用常量,不手写键:「打印」分区的 id 与各条目的键是包入口导出的 PRINT_SETTINGS_SECTION 与 PRINT_SETTING_KEYS,「本地客户端」分区的 id 是 PRINT_CLIENT_SETTINGS_SECTION,「基础」「布局」「工具栏」三页的键是 @xihan-ui/designer-shell 的 DESIGNER_SETTING_KEYS(如工具栏的显示方式 toolbar.display、显示工具栏 toolbar.visible、按钮表 toolbar.layout)。settings 赋值即生效;用户在对话框里、用「视图 → 外观」的显示开关或工具栏的网格、吸附开关改了一条,设计器换成新的一份并派发 settings-change,宿主存起来、下次赋回。settingsPolicy 隐藏或锁定条目,键是条目键或分区 id:hidden 不显示,一个分区的条目全藏了就没有这一页;locked 只读,说明换成「由所在系统管理」。
export function wireSettings(designer: XhPrintDesigner, saved: PrintSettings, store: (settings: PrintSettings) => void): void {
// 宿主自己的缺省压在下面,用户存过的值盖在上面
designer.settings = { [PRINT_SETTING_KEYS.gridSize]: 5, [PRINT_SETTING_KEYS.showGrid]: true, ...saved }
designer.settingsPolicy = { [DESIGNER_SETTING_KEYS.theme]: 'hidden', [DESIGNER_SETTING_KEYS.brand]: 'hidden', [PRINT_SETTING_KEYS.snap]: 'locked' }
designer.addEventListener('settings-change', (event) => {
store((event as CustomEvent<{ settings: PrintSettings }>).detail.settings)
})
}- 主题只作用于设计器:主题与主题色写在
<xh-print-designer>自己的data-theme、data-brand上,页面其余部分不变;设置对话框、菜单这些浮层跟着设计器变。主题色内置:跟随页面(缺省,不写data-brand)、XiHan.UI 的八种预设,或自定义一种颜色,宿主不用传预设;自定义的颜色注册成这台设计器自己的品牌,取不出色阶的颜色不生效。 - 扩展读设置:命令、面板与扩展从上下文的
settings读值,各分区没写的条目已经取了缺省值,如context.settings['print.gridSize']。
独立使用、没有宿主替你存时,给 settingsStorageKey:连上文档时若没赋 settings 就从根所在窗口的 localStorage 读,用户改了设置就写回去;标了 secret 的文本条目(如密钥)不存。读写失败派发 settings-storage-error,设置留在内存里。不给就不读写存储。
export function standalone(designer: XhPrintDesigner): void {
designer.settingsStorageKey = 'print-designer:settings'
designer.addEventListener('settings-storage-error', (event) => {
console.warn('设置没能读写本机存储,这次改的只在内存里', (event as CustomEvent<{ error: unknown }>).detail.error)
})
}调试日志
开发者模式开着时,「基础」里能打开分页调试日志(general.debugPagination)与渲染调试日志(general.debugRender),缺省都关。打开后设计器把记录写到浏览器的控制台,每条折叠成一组,标题以 [xh-print-designer] pagination 或 render 开头;只写控制台,不上报。关掉开发者模式,两条日志随即停。
- 分页:切到预览或打印时,每个页面模板开始排、按假定的页数选的页眉页脚变体不对而重排、表格这类跨页元素在哪页放不下续排到哪页、元素在哪页放不下整块挪到哪页、排完共几页,组里是这一步的数据。
- 渲染:设计器每次重画(改了哪些属性)与设计态排版的耗时;预览与打印的各环节——分页、生成输出页的 DOM、等图片与字体就绪、合成交给本地客户端的文档、送打印——各记一组耗时,组里写输出页数、资源个数或字节数;一次打印另记一组总耗时与结果。
分页步骤与打印环节来自运行时的 trace 回调,宿主自己用预览元素或 printDocument 时也能接,见运行时。
新手引导
设计器第一次在设计态显示出来(画布有了尺寸,放在没打开的页签里时等它打开)后,自动开始新手引导:先是各款共用的几步(欢迎、菜单栏、工具栏、物料、画布、属性、问题与状态栏),再是打印自己的几步——「插入」菜单、页眉与页脚、「页面」菜单、页面模板、字段物料与打印,最后一步是设置。一步高亮一块面板、一个区、菜单栏上的一张菜单或工具栏上的一颗钮,目标看不见(面板隐藏、按钮不在工具栏上)的那步自动跳过。
菜单栏一步高亮整条菜单栏,讲各张菜单里有什么、按 F10 进出菜单栏;「插入」「页面」两步高亮菜单栏上那张菜单的入口;设置一步高亮「文件」菜单的入口,说设置在里面、引导能从「帮助」重新开始。字段物料与打印两步的说明带一句它们也在「文件」菜单里。
窗口窄、菜单栏收成一颗「菜单」钮时,菜单栏一步高亮这一条,「插入」「页面」与设置几步都落在「菜单」钮上。
走完、跳过或关上之后,设计器把设置的
help.tourDone(DESIGNER_TOUR_DONE_KEY)写成true并派发settings-change,宿主存起来,下次就不再自动开始。引导限在设计器的外框里:遮罩只盖外框,高亮框裁在外框里,页面别处照常可读可点。同一页同时只有一个引导:别的设计器的引导开着时不自动开始。
autoTour(attributeauto-tour)写false关掉自动开始。startTour()从头开始,同一页里别的引导开着时先收起它,预览态或没有能走的步骤时返回false;命令start-tour与设置「帮助」页的「开始引导」做同一件事,缺省在帮助菜单里、不上工具栏,宿主可以经commandLayout放上去。扩展的
tourSteps往后面加步骤,order从 200 起;目标写{ panel }、{ region }、{ command }、{ selector },或{ menubar: '<菜单 id>' }(菜单栏上那张菜单的入口,窄屏时是「菜单」钮,菜单栏上没有这张菜单时跳过)。步骤 id 写扩展 id 加点,与别的扩展重名或用了shell.前缀时赋值即抛DesignerExtensionError。
快捷键
焦点在设计器里、且不在输入框与面板里时生效,只在设计态。
| 按键 | 作用 |
|---|---|
| Ctrl / ⌘ + Z | 撤销 |
| Ctrl / ⌘ + Shift + Z,Ctrl + Y | 重做 |
| Ctrl / ⌘ + C、X、V | 复制、剪切、粘贴,走系统剪贴板 |
| Ctrl / ⌘ + D | 创建副本 |
| Ctrl / ⌘ + A | 全选当前页面模板里看得见的元素 |
| Ctrl / ⌘ + S | 保存:执行外壳的 save 命令,派发 save |
| Ctrl / ⌘ + Shift + J | 打开编辑 JSON 的对话框(开发者模式开着时;关着时交给浏览器) |
| Ctrl / ⌘ + , | 打开设置对话框 |
| Ctrl / ⌘ + ]、[ | 上移一层、下移一层 |
| Ctrl / ⌘ + Shift + ]、[ | 置于顶层、置于底层 |
| Delete、Backspace | 删除选中的元素 |
| 方向键 | 选中的元素移动一格(网格间距,写 0 时 1 mm);没有选中时平移视口 |
| Shift + 方向键 | 选中的元素移动 10 mm |
| 按住空格拖动 | 平移画布;按住鼠标中键拖动同样 |
| H | 打开或关上拖动工具 |
| +、- | 放大、缩小 |
| 0 | 适合窗口 |
| Enter | 选中表格时进入表格编辑态 |
| Esc | 取消正在进行的拖动;在表格编辑态里退出;否则取消选择 |
| F10 | 焦点交给菜单栏;再按 F10 或 Esc 回到原处 |
| Alt + F10 | 焦点交给选中框上方的选中工具条(工具条没有按钮时交给尺寸徽标);在里面按 Esc 回到画布 |
复制的元素以 JSON 放进系统剪贴板,可以在两个设计器之间粘贴;粘贴时先按当前的元素登记表校验,有错误的内容不粘贴。别的内容(图片、旧模板等)粘贴或拖到画布上时交给扩展的导入器,见下文「导入」。
保存与事件
设计器不保存模板。每次修改派发 schema-change,detail.schema 是改完的模板;一次拖动、一次属性编辑合成一次。选「文件 → 保存」或在画布上按 Ctrl / ⌘ + S 时(都是外壳的 save 命令,预览态只能从菜单保存)派发 save,宿主保存成功后调用 markSaved():
// 写成设计器的子元素:slot="menubar-center" 的标题在菜单栏正中,slot="status-end" 的保存状态在状态栏右端
const title = document.createElement('span')
title.slot = 'menubar-center'
title.textContent = '送货单'
const status = document.createElement('span')
status.slot = 'status-end'
status.textContent = '没有修改'
designer.append(title, status)
designer.addEventListener('dirty-change', (event) => {
const { dirty } = (event as PrintDesignerEventMap['dirty-change']).detail
status.textContent = dirty ? '有未保存的修改,选「文件 → 保存」或按 Ctrl / ⌘ + S' : '已保存'
})
designer.addEventListener('save', () => {
// 把 designer.schema 交给后端保存,成功后标记为已保存
designer.markSaved()
})- 给
schema赋值即装载新模板:清空撤销历史与选择,按实际大小显示、纸的左上角落在画布左上角,并派发kind为load的schema-change。读取schema得到当前模板。 dirty表示有没有未保存的修改,变化时派发dirty-change;撤销回到保存时的状态,dirty也回到false。- 撤销历史的每一步带一句说明,如「删除文本 title」「修改「字号」」「移动 3 个元素」,随界面语言变;工具栏上撤销、重做的名字带上下一步的说明(「撤销:删除文本 title」)。拖动、缩放、旋转按下到松开是一步;属性面板里连续输入同一项是一步;方向键微移连续按、选中不变、相隔不超过 1 秒的并成一步,换了选中就另起一步。
- 「清空历史」(命令
clear-history)先弹确认框,按确定才清空撤销与重做,模板与dirty不变;它不在工具栏上,在历史记录面板的面板头上,宿主也可以经commandLayout放置。只读与预览态时撤销、重做与清空历史都不可用。 issues-change在模板改动后带上全部校验问题,宿主可以据此拦住有错误的模板,不让保存。- 导入字段派发
fields-change与sample-data-change,见上文「导入字段」。 - 其余事件:
selection-change、page-change、mode-change、viewport-change、element-dblclick。
也可以用方法操作:execute(operations) 执行一组编辑操作并记为一步撤销(操作见 @xihan-print/core 的 PrintOperation),addElement、select、copy、paste、align、reorder、zoomToFit、scrollToElement 等与界面上的命令一致。print() 按当前模板与 data 打印,给了 printClient 时交给本地打印客户端。
自动保存本机草稿
设置「基础」里打开自动保存(general.autoSave,缺省关)后,有未保存的修改时停手约一秒,设计器把整份模板存成这台浏览器里的草稿,连着改只存最后一次;保存了(宿主调 markSaved())草稿就删掉。下次宿主装进同一个草稿键的模板、和草稿不一样时,弹框问恢复还是丢弃:恢复把草稿整份换上,记一步撤销(草稿有错误级校验问题时不换,如扩展换了);丢弃即删;一样就悄悄删掉。只读时不问。
- 草稿键:设计器的
draftKey(attributedraft-key)写模板 id,区分同一页、同一种设计器里的不同模板;不写时按页面地址(路径、查询与锚点)区分。 - 草稿只存在本机,宿主不用存,也不用接新事件。要关掉这一项,用
settingsPolicy把general.autoSave藏起或锁住,或在settings里预先写开或关。
编辑 JSON
设置「基础」里开着开发者模式(general.developer,DESIGNER_SETTING_KEYS.developer)时,按 Ctrl / ⌘ + Shift + J,或在「编辑」菜单、画布空白处的右键菜单里选「编辑 JSON」,打开编辑整份模板 JSON 的对话框。开发者模式关着时这几处都没有它,快捷键交给浏览器;openJsonEditor() 照样能调。宿主要让用户用它,就在 settings 里预先写 'general.developer': true,或用 settingsPolicy 锁住这一条。
- 框里写出当前模板,缩进两格。边改边解析、按打印的校验报问题(与
validate()同一套:结构、元素登记表与表达式),问题列在框下,点一条光标跳到那个路径在文字里的位置。 - 解析失败或有错误级的问题时「应用」不可用;没有错误时按「应用」整份换上,记一步撤销「编辑 JSON」,派发
kind为execute的schema-change。撤销就回到改之前的那份。 - 框下还有「格式化」与「复制」。只读时只能看,不能应用。
- 整份换上走
@xihan-print/core的编辑操作{ type: 'replaceDocument', document },逆操作换回原来那份;新模板由调用方先校验。
只读与预览
readonly:可以选择、浏览、预览与打印,不能修改;物料不能拖出,属性面板只读。- 写成 attribute 时按三态读,与 XiHan.UI 元素的布尔 attribute 同一读法:不写或移除时不只读;写
readonly="false"也不只读;其余写法(readonly、readonly=""、readonly="true")只读。 - 这是行为变化:以前按原生布尔 attribute 读,只要写了就只读,
readonly="false"也算只读。 - 经 property 赋值(
designer.readonly = false)不受影响。
- 写成 attribute 时按三态读,与 XiHan.UI 元素的布尔 attribute 同一读法:不写或移除时不只读;写
mode:design设计态,preview预览态。预览态按data分页,用<xh-print-preview>显示全部输出页。
语言包
界面文字缺省是英文。把 XiHan.UI 与打印同一语言的语言包合并后全局设置,设计器、它内部的零件与预览都换成这种语言,见快速开始。用户也可以在设置的「语言」页里只给这台设计器换语言,见上文「设置」。
只改一个实例的几处文案时,给它的 translations 属性,压在全局语言包之上:
import type { XhPrintDesigner } from '@xihan-print/designer'
export function renamePanels(designer: XhPrintDesigner): void {
designer.translations = {
palette: '控件',
issues: '检查结果',
}
}全部文案键见设计器参考。属性面板里的属性名、选项名与单位也在里面,键是属性路径,如 style.fontSize。
扩展
extensions 收一组扩展对象:元素种类、编辑方式、校验规则、属性编辑器、命令、面板、设置分区、导入器、字段来源、物料来源、菜单栏的菜单、引导步骤与「关于」页的几行都经它登记。内置元素、设计器自带的编辑器与命令也是扩展,自动排在最前。同一页面上的几个设计器各给各的,互不影响。
- 扩展对象的
id全局唯一,用包名加功能名,如@acme/print-fonts。它也是模板里扩展数据的键:文档、页面与元素都可以带extensions: { [扩展 id]: { ... } },校验只查它是对象,编辑与复制粘贴原样带着走。 - 赋值时当场合并查重:扩展 id、元素类型、编辑方式、命令 id 与快捷键、字段来源 id、物料来源 id、菜单栏的菜单 id 与内置的或别的扩展重名时,抛
DesignerExtensionError(code为designer.extension-conflict),设计器保持赋值前的状态。 setup(context)在设计器连上文档时调用,它返回的函数在断开、或这个扩展被去掉时调用。同一个扩展对象换了引用,当作卸下再装上,宿主要保持引用稳定。rules是校验规则,收到文档与{ elements }(合并后的元素登记表),返回问题。问题码以扩展 id 的末段加点开头,结构没有 error 时才跑。controls是新的编辑方式:属性描述里用到这个kind时,按它的check校验取值;没登记的编辑方式报unknown-control。settings是扩展的设置分区,放进设置对话框,排在设计器自己的分区之后(order从 200 起),值同样在settings里、键是「分区 id.条目 id」。要地址和密钥的收费附加包(如云打印)登记自己的分区:密钥写成{ kind: 'text', secret: true }的条目,画成密码框,settingsStorageKey存设置时也不存它。本地客户端没有地址与密钥,「本地客户端」一页只写连接状态。tourSteps是新手引导的步骤,见上文「新手引导」;about是设置「关于」页里接在后面的几行{ label, value },收费附加包写自己的版本与授权。fieldSources是导入字段对话框里的来源页,排在「JSON 样例」「JSON Schema」之后,如从接口请求取一份样例;页拿到内容后交回对话框,推断、预览与确认走对话框自己的。形状见 XiHan.UI designer-kit 的「字段来源」。paletteSources是物料来源:往物料面板加几组条目,如收费附加包的「我的物料」、行业模板,排在内置分组与「字段」之后,几个来源之间按order。物料面板每次刷新都按上下文现取它的分组;条目被点一下时交给来源的insert(item, context),拖到画布上放下时多给一个落点{ x, y, region }:纸张坐标(mm),以及这一点落在页眉、正文还是页脚。插入什么、插在哪里由来源定;只读或不在设计态时不交给来源。分组或条目的 id 与物料面板里已有的重名时,刷新物料面板时抛TypeError。形状见 XiHan.UI designer-shell 的「物料来源」。menubarMenus往菜单栏加菜单{ id, label, order? }:label可以是收到上下文的函数,随语言变;order定先后,内置的文件 0、编辑 10、视图 20、帮助 90,打印的插入 30、页面 40,扩展写在 30–80 之间,不写按 50。命令写placement: ['menubar:<菜单 id>']落进来,没有命令的菜单不出现,见上文「菜单栏」。- 预览元素
<xh-print-preview>与paginate、validateDocument这些函数也收extensions,与设计器给同一份。
扩展元素
扩展的 elements 登记新的元素类型,renderers 给它们的渲染。原型与内置元素的一样,给出缺省值、尺寸下限、物料分组与属性描述;不跨页的元素(flow 为 false)另外必须给 layout:排版时按元素与数据算出画在元素框里的数据(只能是 JSON),元素框的位置与尺寸由排版填好。渲染收到排版盒与输出页所在的文档,返回要放进元素框的节点:
import type { ExtensionElementPrototype } from '@xihan-print/core'
import type { PrintDesignerExtension } from '@xihan-print/designer'
import { behaviorProperties, layoutProperties, property } from '@xihan-print/core'
const SVG = 'http://www.w3.org/2000/svg'
const seal: ExtensionElementPrototype = {
type: 'seal',
category: 'media',
order: 9,
flow: false,
minSize: { width: 10, height: 10 },
defaults: { type: 'seal', width: 40, height: 40, field: '', color: '#d40000' },
properties: [
...layoutProperties({ rotation: false }),
property(['field'], 'data', { kind: 'dataField', kinds: ['value'] }),
property(['color'], 'appearance', { kind: 'color' }),
...behaviorProperties(),
],
// 排版只给 JSON:从数据里取单位名,元素框由 core 填好
layout: (element, context) => ({
name: String(context.field(String(element.field)) ?? ''),
color: String(element.color),
}),
// 报出引用的字段:collectDocumentFields 汇总模板字段时不会漏掉印章
fields: (element, context) => context.field(String(element.field)),
}
export const sealExtension: PrintDesignerExtension = {
id: '@acme/print-seal',
elements: [seal],
palette: { seal: { label: '印章' } },
renderers: {
seal: (box, { document }) => {
const { name, color } = box.data as { name: string, color: string }
const svg = document.createElementNS(SVG, 'svg')
svg.setAttribute('viewBox', '0 0 100 100')
svg.setAttribute('width', '100%')
svg.setAttribute('height', '100%')
const ring = document.createElementNS(SVG, 'circle')
ring.setAttribute('cx', '50')
ring.setAttribute('cy', '50')
ring.setAttribute('r', '46')
ring.setAttribute('fill', 'none')
ring.setAttribute('stroke', color)
ring.setAttribute('stroke-width', '4')
const text = document.createElementNS(SVG, 'text')
text.setAttribute('x', '50')
text.setAttribute('y', '56')
text.setAttribute('text-anchor', 'middle')
text.setAttribute('fill', color)
text.textContent = name
svg.append(ring, text)
return svg
},
},
}- 属性描述可以用
layoutProperties、behaviorProperties与property拼,和内置元素用的是同一套。 - 引用了数据字段的元素给
fields:返回它引用的字段,collectDocumentFields汇总模板字段时才不会漏掉它。上下文带field、template、expression与text四个帮手,分别把字段路径、{{path}}模板串、表达式与文本内容来源换成引用,与内置元素同一套解析;引用的是列表时给rows,里面是相对列表一行的引用。字段的值在元素里当图片地址、条码或二维码的内容用时,在引用上写content(image、barcode、qrcode),汇总时一并记进字段树。 - 不跨页的元素没给
layout、跨页的元素没给prepare(见下文)时,赋值时抛TypeError。 palette给自己元素的物料项:名称与图标(@xihan-ui/core的IconRecord)。名称也用在画布与属性面板的可访问名和问题列表里,不给时显示类型名。palette里写了不是本扩展登记的元素类型时,赋值时抛TypeError。- 原型的
category可以是内置分组(text、media、data、shape、code),也可以是新的分组。新分组排在内置分组之后,按首次登记的顺序;名称写在扩展的categories里,没写时显示分组名。分组名与内置分组或别的扩展写的重名时,抛DesignerExtensionError。 - 名称随扩展给出,不在设计器的语言包里。扩展包要支持多种语言,就按语言给出不同的扩展对象,例如导出一个收文案的工厂函数。
- 渲染只能用标准 HTML 与 SVG:浏览器打印与本地打印客户端用的是另一份文档,自定义元素在那里不会升级。
- 扩展去掉后,文档里已有的这类元素照样保留:设计器把它画成写着类型的虚线占位,校验报
element.unknownType,再装上就恢复。
跨页的元素(flow 为 true)给 prepare 而不是 layout:按元素与数据给出按顺序的若干块,每块是高度(mm)与交给渲染的数据,另可给一块 repeat,排在每页那一段的开头(如表头)。prepare 收到的上下文比 layout 多一个 measureText(text, style, width),按文字样式与可用宽度折行量出各行与总高度:
import type { ExtensionElementPrototype, TextStyle } from '@xihan-print/core'
import type { PrintDesignerExtension } from '@xihan-print/designer'
import { behaviorProperties, layoutProperties, property } from '@xihan-print/core'
const style: TextStyle = {
fontFamily: 'sans-serif',
fontSize: 10,
bold: false,
italic: false,
underline: false,
lineThrough: false,
color: '#000000',
align: 'left',
verticalAlign: 'top',
lineHeight: 1.4,
letterSpacing: 0,
}
interface Approval {
name: string
comment: string
}
const approvals: ExtensionElementPrototype = {
type: 'approvals',
category: 'data',
order: 9,
flow: true,
minSize: { width: 40, height: 10 },
defaults: { type: 'approvals', width: 180, height: 20, field: 'approvals' },
properties: [
...layoutProperties({ rotation: false }),
property(['field'], 'data', { kind: 'dataField', kinds: ['list'] }),
...behaviorProperties(),
],
// 每条记录一块:意见折行后的高度加上下各 2 mm;块不拆开,放不下就整块挪到下一页
prepare: (element, context) => {
const list = context.field(String(element.field))
const items = (Array.isArray(list) ? list : []) as Approval[]
return {
repeat: { height: 8, data: ['审批人', '意见'] },
blocks: items.map(item => ({
height: context.measureText(item.comment, style, element.width - 40).height + 4,
data: [item.name, item.comment],
})),
}
},
}
export const approvalsExtension: PrintDesignerExtension = {
id: '@acme/print-approvals',
elements: [approvals],
palette: { approvals: { label: '审批记录' } },
renderers: {
// 每页一段:data 是 { blocks, repeat },blocks 是这一页的那几块
approvals: (box, { document }) => {
const { blocks, repeat } = box.data as { blocks: string[][], repeat: string[] }
const table = document.createElement('table')
table.style.width = '100%'
for (const cells of [repeat, ...blocks]) {
const row = table.insertRow()
for (const text of cells)
row.insertCell().textContent = text
}
return table
},
},
}- 块依次排进正文区,放不下的整块挪到下一页,与长文本、表格一样把下方的元素往下推;只有一段时高度不小于设计高度。
- 每页一段,渲染每段调一次:排版盒的
data是{ blocks, repeat },blocks是这一段的那几块的数据,repeat是重复块的数据(没给时为null);fragment给出段序号与是不是最后一段。 - 一块比整页正文区还高时照样放下,分页诊断报
element.overflow;块高度不是不小于 0 的有限数时,排版抛TypeError。 - 跨页的元素只能放在正文区,放进页眉页脚时校验报
element.flowInBand;设计态排成一段、按内容撑高。
属性编辑器
扩展的 editors 给属性面板换编辑器,按扩展的顺序压在设计器自带的编辑器之上。查找顺序是对象种类加属性路径、属性路径、编辑方式,都没有时用面板内建的编辑器:
| 表 | 键 | 适用 |
|---|---|---|
byTarget | 对象种类 → 属性路径 | 只对这种对象生效 |
byPath | 属性路径,如 style.fontFamily | 所有带这个属性的对象 |
byControl | 编辑方式,如 color、stroke | 所有这种编辑方式的属性 |
对象种类是 page(页面设置)、元素类型(如 text、table)与表格部位(table-column、table-header、table-body、table-footer-row、table-footer-cell);编辑方式见元素的「取值」一栏。给了编辑器的复合属性不再拆开,整个值交给编辑器。
编辑器是一个函数:收到当前值与 setValue、commit,返回一个元素;值或只读状态变化时设计器调用 update,移除前调用 destroy。元素标了 data-xh-part="control" 时放进字段里,标签自动接上。下面的编辑器让字体只能从几种里选:
import type { DesignerPropertyEditor } from '@xihan-ui/designer-shell'
import { definePrintDesigner, PRINT_DESIGNER_PANELS } from '@xihan-print/designer'
import { schema } from '../templates/minimal'
const FONTS = ['宋体', '黑体', '楷体', '仿宋']
const fontEditor: DesignerPropertyEditor = (initial) => {
let context = initial
const select = document.createElement('select')
select.dataset.xhPart = 'control'
select.append(new Option('缺省字体', ''), ...FONTS.map(font => new Option(font, font)))
const sync = (): void => {
select.value = typeof context.value === 'string' ? context.value : ''
select.disabled = context.readonly
}
select.addEventListener('change', () => {
context.setValue(select.value === '' ? undefined : select.value)
context.commit()
})
sync()
return {
element: select,
update: (next) => {
context = next
sync()
},
}
}const designer = document.createElement('xh-print-designer')
designer.defaultLayout = { version: 1, hidden: [PRINT_DESIGNER_PANELS.palette, PRINT_DESIGNER_PANELS.issues, PRINT_DESIGNER_PANELS.pages] }
designer.extensions = [{ id: '@acme/print-fonts', editors: { byPath: { 'style.fontFamily': fontEditor } } }]
designer.schema = schemasetValue(undefined)清除这个属性;连续的setValue合成一步撤销,commit()之后的修改另起一步。context.target是当前全部被编辑的对象,多选时有多个。context.issues是落在这个属性里层的校验问题,路径相对这个属性(如值映射第 2 项的数据值是["entries", 1, "value"]),没有时是空数组。复合编辑器据此把问题标到对应的子项上:子项旁边写出问题文字,子项的控件标无效(aria-invalid="true"),并经aria-describedby接上这段文字;读屏名仍是子项自己的名字。编辑器返回的实例写nestedIssues: true时,属性行下面不再重复这些问题;不写时,行下显示第一条。设计器自带的值格式(值映射由内建选项表标到那一格,未命中文本、常用格式与表达式标到各自下面)与表格列、表尾行的条目都这样标;四边边框按页签分边,里层问题仍显示在属性行下面。
命令
扩展的 commands 登记命令,工具栏按钮、右键菜单项与快捷键共用一条。命令 id 用扩展 id 加点加名字;label、enabled 与 run 收到 { designer, translations, commands, settings, frame }(commands 是四款设计器共用的命令文案,如 commands.undo;settings 是生效的设置;frame 是设计器的外框),label 也可以直接写成文字;给了 checked 的是开关式命令,工具栏按钮显示按下状态:
import type { PrintDesignerExtension, XhPrintDesigner } from '@xihan-print/designer'
import { PRINT_TOOLBAR_LAYOUT } from '@xihan-print/designer'
export const lockExtension: PrintDesignerExtension = {
id: '@acme/print-lock',
commands: [{
id: '@acme/print-lock.lock',
label: '锁定',
placement: ['toolbar', 'menu:element'],
shortcut: ['Mod', 'Shift', 'L'],
enabled: ({ designer }) => !designer.readonly && designer.getSelection().elements.length > 0,
run: ({ designer }) => {
designer.execute(designer.getSelection().elements.map(id => ({ type: 'updateElement', id, changes: [{ path: ['locked'], value: true }] })))
},
}],
}placement是命令缺省出现的位置:工具栏toolbar(工具栏只显示图标时,没有图标的命令写它的shortLabel),右键菜单menu:blank(画布空白处)、menu:element(元素上)、menu:table(表格编辑态里),选中工具条selection:text(选中的全是文字类元素)、selection:element(单个别的元素)、selection:multiple(几个元素里有别的种类),或菜单栏的一张菜单menubar:<菜单 id>(见上文「菜单栏」)。扩展命令按登记顺序排在内置命令之后,自成一组。shortLabel是按钮上看得见的短文字,名字与提示仍取label,所以短文字要出现在label里(不分大小写),比如label写「插入条码」、shortLabel写「条码」;开发期不在时,控制台警告一次。shortcut写成键数组,Mod在 macOS 上是 ⌘、其余平台是 Ctrl。焦点在画布上、命令此刻可用时生效。- 设计器的
commandLayout改写排列:位置 → 若干组命令 id,内置命令与扩展命令可以混排;写了某个位置就只显示列出的命令,写了没登记的命令 id 时抛TypeError。缺省排列是导出的PRINT_TOOLBAR_LAYOUT、printMenuLayout(subject, tableSelected)与printSelectionLayout(subject),菜单栏的是printMenubarLayout(commands, elements)(在@xihan-ui/designer-shell的designerMenubarLayout(commands)上加了打印的几处,elements是元素登记表,插入菜单按它的物料分组排),可以在它们的基础上改:
export function lockFirst(designer: XhPrintDesigner): void {
designer.extensions = [lockExtension]
designer.commandLayout = { toolbar: [['@acme/print-lock.lock'], ...PRINT_TOOLBAR_LAYOUT] }
}内置命令的 id:
| 用途 | 命令 id |
|---|---|
| 编辑 | undo redo clear-history cut copy paste duplicate delete select-all |
| 对齐与层级 | align-left align-center align-right align-top align-middle align-bottom distribute-horizontal distribute-vertical bring-to-front bring-forward send-backward send-to-back:工具栏上缺省每条一颗钮,对齐、分布与层级各成一组 |
| 视图 | pan-tool(designer-shell 内置,拖动工具,按下状态跟外框的 canvasTool) zoom-out zoom-in zoom-to-fit zoom-to-actual grid snap design preview page-setup print:grid snap 也在视图菜单,print 也在文件菜单 |
| 表格 | edit-table exit-table-edit insert-column-left insert-column-right remove-column move-column-left move-column-right insert-footer-row-above insert-footer-row-below remove-footer-row merge-cell-right split-cell |
| 文字格式 | font-size text-bold text-italic text-underline text-line-through text-align-left text-align-center text-align-right:缺省排在 selection:text 的选中工具条上 |
| 保存、面板、设置与帮助 | save panels settings help:designer-shell 内置。「保存」(Ctrl / ⌘ + S)派发 save,在文件菜单;其余三条在菜单栏里(面板在视图菜单,设置在文件菜单,帮助在帮助菜单),「面板」也排在工具栏末组。「面板」点开菜单开关各块面板,「设置」打开设置对话框(Ctrl / ⌘ + ,),「帮助」在新窗口打开本站的设计器一页(PRINT_DESIGNER_HELP_URL)。panels-toggle-all panels-restore layout-reset 排在「面板」菜单末尾。start-tour 开始新手引导,shortcuts、about 打开设置的快捷键页与关于页,都在帮助菜单;edit-json 打开编辑 JSON 的对话框(Ctrl / ⌘ + Shift + J),在编辑菜单与画布空白处右键菜单的末组,只在开发者模式开着时存在;show-toolbar show-status-bar 是显示开关,收在视图菜单的「外观」(appearance)子菜单里 |
| 面板开关 | panel:<面板 id>:每块面板一条,含扩展登记的面板(panel:<区名>),文字是面板标题、勾选的是看得见的,宿主锁定的面板不可用;不排在任何位置,可以经 commandLayout 放上工具栏 |
| 页面模板 | previous-page page-menu next-page add-page duplicate-page remove-page:翻页条上的排列固定(PRINT_PAGE_STRIP_LAYOUT),不经 commandLayout;move-page-up move-page-down 在页码菜单里。除了页码,这几条也在页面菜单里 |
| 页面菜单 | page-paper page-orientation:纸张与方向的子菜单;page-properties:在属性面板里编辑当前页面模板 |
| 插入 | insert:<元素类型>:每种元素一条,扩展登记的元素也有(printInsertCommandId(type)),放在视口中央并选中;header-variants footer-variants:页眉、页脚变体的子菜单。都在插入菜单里,不上工具栏,宿主可以经 commandLayout 放上去;扩展的命令不能用 insert: 开头的这些 id |
面板
扩展的 panels 往外框里加区,放印章库、套打对位这类面板。区是 { name, position, size?, minSize?, label?, title?, icon?, anchor?, defaultHidden?, closable?, movable?, floatable?, minimizable? }:position 取 top、start、end、bottom,或 overlay(画布上的浮动件,anchor 定落在哪个角或右侧中间,只能显示或隐藏),同一位置的区按扩展顺序上下排;label 是分隔条可访问名里的区名,给了 title 就画成带面板头的卡片;defaultHidden 的缺省不显示,closable、movable、floatable、minimizable 写 false 就不能隐藏、换栏、浮出或收起。create(context) 返回放进区里的元素;设计器状态变化时调用 update(context),扩展去掉时先调用 destroy() 再移除:
import type { PrintDesignerExtension } from '@xihan-print/designer'
export const selectionPanel: PrintDesignerExtension = {
id: '@acme/print-selection',
panels: [{
region: { name: 'acme-selection', position: 'end', label: '选中' },
create: ({ designer }) => {
const element = designer.ownerDocument.createElement('section')
const render = (ids: readonly string[]): void => {
element.textContent = ids.length === 0 ? '没有选中' : ids.join('、')
}
render(designer.getSelection().elements)
return { element, update: context => render(context.designer.getSelection().elements) }
},
}],
}- 区名与内置区(
menubar、toolbar、pages、minimap、palette、outline、canvas、properties、history、issues、status)或别的扩展的重名时,赋值时抛DesignerExtensionError,设计器保持赋值前的状态。 - 扩展的区同样记在面板布局里,用户能隐藏、挪动与浮出,也各有一条开关命令
panel:<区名>;扩展自己登记了同名命令时同样抛DesignerExtensionError。预览态只保留菜单栏、工具栏与画布。
导入
扩展的 importers 把粘贴与拖到画布上的外部内容换成元素。来源是 { kind, mime?, name?, text?, file? }:kind 为 clipboard 或 drop,文件带 file,文本按链接(text/uri-list)、HTML、纯文本依次给出。设计器按文件、链接、HTML、纯文本的顺序逐个问各导入器的 accepts,扩展靠前的先问,第一个接下的导入:
import type { PrintDesignerExtension } from '@xihan-print/designer'
import { createElementRegistry } from '@xihan-print/core'
const registry = createElementRegistry()
function readAsDataUrl(file: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => resolve(String(reader.result))
reader.onerror = () => reject(reader.error)
reader.readAsDataURL(file)
})
}
export const imageImporter: PrintDesignerExtension = {
id: '@acme/print-images',
importers: [{
id: '@acme/print-images.file',
accepts: source => source.file !== undefined && source.mime?.startsWith('image/') === true,
import: async (source) => {
const url = await readAsDataUrl(source.file!)
const image = registry.createElement('image', { id: 'image', x: 0, y: 0, overrides: { source: { kind: 'static', url } } })
return { elements: [image] }
},
}],
}import可以是异步的,返回{ elements, region? },返回undefined即什么都不放。元素整体挪到落点(粘贴时是视口中央),左上角吸附网格,再按粘贴的规则换新 id 放进当前页面模板并选中,记一步撤销。region缺省按落点所在的区。- 放下前按当前的元素登记表校验;
import抛错或元素没有通过校验时,设计器派发import-error,带导入器的 id、来源与错误,不放任何元素。 - 设计器自己的剪贴板内容不经导入器。
- 导入器只把外部内容换成元素,放进当前页面模板,例如把拖进来的图片传到宿主的存储、换成图片元素。纸张、页眉页脚这类整份模板的设置不经导入器;整份存量模板的迁移见与宿主集成。
