fix(signing-editor-marks): 统一占位符样式并修复删除后 DOM 残留
自定义渲染器渲染钩子改走 data.attrs 挂类名与 title:data.class 会被 wangEditor 的规范化函数搬进 props 且到不了 class 模块(胶囊不渲染), data.props.className 则因 props 模块只单向写入而摘不掉旧值,导致勾选框 退格删除后 DOM 仍残留 ms-sign-box 空壳。 同时移除必填标记相关的红角标、签名位专属橙色、插入按钮与「占位符检查」 面板里的 `*` / 「必填」文案,所有占位符共用一个样式。 verify-editor-marks.cjs 增加断言台账(PASS/FAIL 并反映退出码)、叶子结构 探针与退格删除回归,README 补充「踩过的坑 · 5」说明渲染通道的选择。
This commit is contained in:
@@ -57,7 +57,6 @@ interface SigningLeafMark {
|
||||
signKind?: string
|
||||
/** 控件类型的中文名,渲染在胶囊右侧 */
|
||||
signTag?: string
|
||||
signRequired?: boolean
|
||||
/** 是否目录外的自定义文本项(现场手填),决定悬浮提示里说"自动填充"还是"现场填写" */
|
||||
signCustom?: boolean
|
||||
/** 原文,如 {患者姓名},用于悬浮提示 */
|
||||
@@ -99,7 +98,6 @@ export function decorateSigningMarkers([node, path]: DecoratedEntry): DecoratedR
|
||||
signMarker: 'field',
|
||||
signKind: field.kind,
|
||||
signTag: SIGNING_FIELD_KIND_LABELS[field.kind] ?? '文本',
|
||||
signRequired: field.required,
|
||||
signCustom: isCustomFieldId(field.id),
|
||||
signRaw: text.slice(start, end),
|
||||
}
|
||||
@@ -150,12 +148,44 @@ export function decorateSigningMarkers([node, path]: DecoratedEntry): DecoratedR
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染钩子:给带标记的叶子挂上类名与 data-*,样式见 EditorWangPane。
|
||||
* 渲染钩子:给带标记的叶子挂上类名与属性,样式见 EditorWangPane。
|
||||
*
|
||||
* 直接改传入的 vnode.data 而不新建 vnode:新建需要 snabbdom 的 h,
|
||||
* 而 snabbdom 是 wangEditor 的内部依赖,不该由业务代码直接引用。
|
||||
* data.props / data.dataset 是 vnode 的规范字段(见 core 的 normalizeVnodeData),
|
||||
* 改它们不会被后续的归一化覆盖。
|
||||
*
|
||||
* **类名与 title 必须走 data.attrs。** 这一条是踩出来的,两个坑叠在一起:
|
||||
*
|
||||
* ① `data.class` 根本到不了 snabbdom 的 class 模块。
|
||||
* wangEditor 在渲染前会用一个规范化函数(`RT`)递归重写 vnode.data:
|
||||
*
|
||||
* var LT = ["props","attrs","style","dataset","on","hook"] // 白名单
|
||||
* Object.keys(data).forEach(key => {
|
||||
* if (LT.includes(key)) return
|
||||
* if (key.startsWith("data-")) { 移到 data.dataset; delete data[key] }
|
||||
* else { 合并进 data.props; delete data[key] }
|
||||
* })
|
||||
*
|
||||
* `class` 不在白名单里,于是 `data.class` 被搬成 `data.props.class` 并删掉原键,
|
||||
* class 模块永远读不到它 —— 类名压根不会出现在 DOM 上(胶囊整个不渲染)。
|
||||
* 注意 `data.dataset` 与 `data.attrs` 都在白名单里,能原样透传。
|
||||
*
|
||||
* ② `data.props` 只单向写入,摘不掉旧值。
|
||||
*
|
||||
* function propsModule(oldVnode, vnode) {
|
||||
* for (key in newProps) { old === cur || (elm[key] = cur) }
|
||||
* }
|
||||
*
|
||||
* 它只遍历**新** props,从不清理已经消失的旧 prop;而 className / title 都是
|
||||
* 原型上的访问器,`delete elm.className` 是个空操作。于是 props 里的类名一旦
|
||||
* 挂上就再也摘不掉:勾选框被退格删掉后,那个叶子已经空了、decorate 也早已不再
|
||||
* 返回它,DOM 里却还留着 ms-sign-box —— 编辑器里表现为一个擦不掉的浅蓝小方块
|
||||
* (签署侧按文本解析,文本确实空了,所以那边一直是对的)。
|
||||
*
|
||||
* `attrs` 模块两头都正常,所以统一走它:
|
||||
*
|
||||
* create/update 都执行 setAttribute,并且有 `for (key in oldAttrs) key in newAttrs || removeAttribute(key)`
|
||||
*
|
||||
* 于是叶子从"有标记"变成"无标记"时,类名会被真正摘掉。
|
||||
*/
|
||||
function renderSigningMarker(node: StyledNode, vnode: StyledVNode): StyledVNode {
|
||||
const mark = node as unknown as SigningLeafMark
|
||||
@@ -165,14 +195,16 @@ function renderSigningMarker(node: StyledNode, vnode: StyledVNode): StyledVNode
|
||||
}
|
||||
|
||||
const data = (vnode.data ??= {})
|
||||
const props = (data.props ??= {})
|
||||
const dataset = (data.dataset ??= {})
|
||||
|
||||
dataset.signMarker = mark.signMarker
|
||||
|
||||
if (mark.signMarker === 'box') {
|
||||
props.className = 'ms-sign-box'
|
||||
props.title = '勾选项:签署时转为可勾选控件'
|
||||
setAttrs(data, {
|
||||
class: 'ms-sign-box',
|
||||
title: '勾选项:签署时转为可勾选控件',
|
||||
})
|
||||
|
||||
return vnode
|
||||
}
|
||||
|
||||
@@ -184,27 +216,35 @@ function renderSigningMarker(node: StyledNode, vnode: StyledVNode): StyledVNode
|
||||
// 提示必须分开说:目录里的字段签署时自动填充,自定义文本项是留给现场手填的空框。
|
||||
// 一律写成"自动填充",科室会以为写了占位符就有人替他填。
|
||||
const source = mark.signCustom ? '签署时由患者/家属现场填写' : '签署时按患者与就诊信息自动填充'
|
||||
props.className = 'ms-sign-chip'
|
||||
props.title = `待填写项:${mark.signRaw ?? ''}(${kind}${mark.signRequired ? ',必填' : ''}),${source}`
|
||||
|
||||
setAttrs(data, {
|
||||
class: 'ms-sign-chip',
|
||||
title: `待填写项:${mark.signRaw ?? ''}(${kind}),${source}`,
|
||||
})
|
||||
|
||||
if (mark.signTag) {
|
||||
dataset.signTag = mark.signTag
|
||||
}
|
||||
|
||||
if (mark.signRequired) {
|
||||
dataset.signRequired = 'true'
|
||||
}
|
||||
|
||||
return vnode
|
||||
}
|
||||
|
||||
// 大括号只缩小调淡,不移除:见文件头的"硬约束"。
|
||||
// 左右分开给类名,样式里才能各收各的边距(全角括号两侧留白很宽)
|
||||
props.className = `ms-sign-brace ms-sign-brace--${mark.signPart ?? 'open'}`
|
||||
setAttrs(data, { class: `ms-sign-brace ms-sign-brace--${mark.signPart ?? 'open'}` })
|
||||
|
||||
return vnode
|
||||
}
|
||||
|
||||
/**
|
||||
* 写 data.attrs(attrs 模块)。
|
||||
* 不要写 data.class —— 会被规范化函数搬进 props 且到不了 class 模块;
|
||||
* 也不要写 data.props.className —— props 模块只增不删,类名摘不掉。理由见上。
|
||||
*/
|
||||
function setAttrs(data: NonNullable<StyledVNode['data']>, attrs: Record<string, string>) {
|
||||
Object.assign((data.attrs ??= {}), attrs)
|
||||
}
|
||||
|
||||
let registered = false
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user