简体中文
JsonViewer JSON 视图
把一份 JSON 展开为可折叠的树:键名、值与值类型各自成块,对象与数组可以逐层收起。
用法
一份 JSON 展开为可折叠的树:键名与值各自成块,六种类型各自上色,默认只展开根行
name"曦寒视图"
version"1.0.0-alpha.2"
stars128
activetrue
homepagenull
No data
组件结构
加粗的是必需部件。
data-scope="json-viewer":root · tree · item · item-key · item-value · branch · branch-control · branch-trigger · branch-indicator · branch-text · branch-summary · branch-content · preview · text · token · empty · mark · toolbar · expand-trigger · wrap-trigger · copy-trigger · view-trigger · status
示例
默认展开层数
defaultExpandedDepth 决定初次展开到第几层:1 只展开根行,3 连孙层一起铺开
No data
host"127.0.0.1"
port5173
enabledfalse
certnull
target"es2022"
minifytrue
No data
受控展开
传入 expandedValue 后由宿主决定,组件只发 expanded-value-change 不落内部值,写回后才变化
No data
大数据
maxItems 把超长数组折为一行占位,maxStringLength 截断过长的字符串,一份大 JSON 不会拖慢页面
total240
cursor"eyJvZmZzZXQiOjAsImxpbWl0…"
0"第 1 条"
1"第 2 条"
2"第 3 条"
3"第 4 条"
4"第 5 条"
… 235 more
No data
键排序
sortKeys 使对象键按字典序排列,数组顺序不变;接口返回的字段顺序不稳定时使用它
action"deploy"
at"2026-08-20"
by"ci"
retries2
0"build"
1"test"
2"publish"
zone"cn-east-1"
No data
循环引用
值出现在自己的祖先链上即停止并标为 [Circular],不会无限递归;共享引用不算环,照常展开
name"root"
id1
id1
parent[Circular]
No data
尺寸
size 三档只改变字号与层级缩进,行的结构与配色都不变
id7
label"曦寒"
oktrue
No data
id7
label"曦寒"
oktrue
No data
id7
label"曦寒"
oktrue
No data
原文视图
view="text" 直接输出缩进后的 JSON 原文:整块可框选可复制,且不受 maxStringLength / maxItems 折减
{ "orderNo": "SO-2026-0825-0417", "amount": 12.5, "items": [ { "sku": "A-1001", "qty": 2 }, { "sku": "B-2003", "qty": 1 } ], "remark": "跨境订单,需人工复核收件地址与税号" }
No data
空态与形态
一行都无法展开时显示空态格;variant="ghost" 去掉外框与底色
这份接口还没有返回内容
No data
搜索
search 标出键名与值里含有搜索词的行并展开它们的祖先,命中的那一段铺成 mark;工具条里的上一条 / 下一条在命中之间逐个走,停住的那一条换成实心并滚进视野
0 / 4
name"曦寒视图"
version"1.0.0-alpha.2"
No data
内建工具条
tools 打开内建工具条:树档有展开全部 / 收起全部,原文档有自动换行,复制原文与原文开关固定在末端;数组只铺列出的几项
traceId"9ef5a8c3e1b44f0f"
method"POST"
url"/api/workflow/definition/new-version?tenant=hangzhou-branch&operator=owner&reason=quarterly-review"
status200
elapsed35
No data
{ "traceId": "9ef5a8c3e1b44f0f", "request": { "method": "POST", "url": "/api/workflow/definition/new-version?tenant=hangzhou-branch&operator=owner&reason=quarterly-review", "headers": { "content-type": "application/json", "x-request-id": "c0d1e2f3" } }, "response": { "status": 200, "elapsed": 35, "body": { "id": "207795978470952990", "version": 3 } } }
No data
设计指引
何时使用
- 调试面板、接口返回体、配置文件的只读呈现。
- 日志详情中的大段结构化字段,直接铺开会淹没正文。
何时不用
- 数据可编辑时,本组件只读;修改值需要自行接入表单与文本字段。
- 数据只是一段带语法高亮的源码时,使用代码视图。
- 层级数据不是 JSON、键名与类型没有语义时,使用树;它处理通用层级数据与选中,本组件处理 JSON 的类型语义(键名、值形态、按类型着色、循环引用)。
- 只有几个字段需要平铺展示时,使用描述列表。
特性
- 行结构由
value展开,作者不写任何行标记:Vue 与自定义元素两侧铺出同一棵 DOM,根容器内原有内容由组件接管。 - 自定义渲染器可调用
groupJsonViewerNodesByParent(nodes)把可见行按父路径分组;返回值保留父路径首次出现顺序、组内输入顺序与节点身份。 - 未提供
value时是空视图,不展开任何行;对象内确实存在值为undefined的成员时,该行照常展开。 - 展开集合可受控(
expandedValue/defaultExpandedValue),非受控时按defaultExpandedDepth计算:数据晚于组件挂载到达(自定义元素常先升级、再由脚本写.value)也能计算,第一次展开或收起之后固定,不再跟随数据。 maxStringLength截断长字符串,maxItems折叠超长数组,sortKeys让对象键按字典序排列。- 循环引用展开到即停,标记为
[Circular],不会无限递归。 - 每一行带
data-value-type,六种值形态各自着色。原文档同样着色:原文按类别切成token部件(data-kind为key、string、number、boolean、null、punctuation),与树档使用同一组覆盖槽与语法令牌;原文无法识别为 JSON 时整段不着色。 - 尺寸轴与其他组件同源;
variant决定外框形态,默认outline;subtle换成淡底无描边,ghost去掉外框与底色只保留内容。 - 没有任何行可展开时由
empty部件说明,文案使用translations.empty,作者也可以自行写入内容。 - 内建工具条按需打开:
tools不提供即没有工具条;true铺出全部工具;数组只铺列出的几项(expand、wrap、copy、view),数组里出现未知的工具名会抛错。条是根的第一个孩子,钮的排列由组件决定:展开 / 收起全部是一颗切换钮,只在树档出现:还有收着的分支时按下展开全部,已经全部展开时按下收回顶层,名称与字形随之变化;「自动换行」只在原文档出现;复制与树 / 原文切换钮固定在末端,切换形态时这两颗不移动,切换钮的名称与字形取要切去的那一档。 - 树 / 原文切换钮改写
view:view提供即受控,开关只触发onViewChange;defaultView是非受控初值。自动换行同理(wrap/defaultWrap/onWrapChange),开启后原文的长行折到下一行而不是横向滚动。 - 复制钮写入的是原文档那一份文本(不受
maxStringLength/maxItems折减),无论当前处于哪一档;成功后钮换成对钩约 3 秒,写入失败时回到常态并触发onCopyError。剪贴板接口要求安全上下文。 - 手里有值的原文(如后端下发的 JSON 文本)时传给
source:原文档与复制钮改用它,能解析成 JSON 就只按两格缩进重排,数字、字符串等字面量保持不变,解析不了就原样显示。value是解析过的值,超出安全整数的数字(如 64 位 ID)已经失真,只有原文与来源一致;树档仍展示value。headless 另导出同一个重排函数formatJsonSource。 - 只支持 JSON 能表达的形状,传入活对象时呈现有损:
Date/Map/Set一律按自有可枚举键展开,因此显示为{};undefined归入null一档、显示为undefined;bigint归入number;函数与 symbol 归入string,按各自的字符串形式呈现。需要如实展示这些值时先转换为 JSON 能表达的形状。 - 自定义元素侧:
value属性接受一段 JSON 文本(无法解析时按字符串值展示),对象与数组直接赋 property(el.value = { … });expandedValue/defaultExpandedValue/translations没有对应属性,只能通过 property 设置,写成expanded-value='["$"]'不会生效。
组合
搜索:
search标出键名与值里含有搜索词的行(不区分大小写),命中行的祖先分支自动展开(写进展开集合,之后照常能收起),命中的那一段铺成mark部件。搜索框与上一条 / 下一条由作者摆在树之前:Vue 写进toolbar插槽,React 经toolbar传入,载荷是searchMatches、activeMatch、nextMatch、prevMatch;Web Components 在元素上取searchMatches/activeMatch并调用nextMatch()/prevMatch()。停住的那一条投影data-current、命中片段换成实心,并被滚进视野。只需要复制原文时打开
tools的copy一项;复制钮要放在别处或换成带文字的样式时,改用剪贴板并把jsonText(value, sortKeys)的结果交给它。搜索框(
toolbar插槽)与内建工具条可以同时使用:插槽内容排在根之前,内建工具条排在根内的最前面。
最佳实践
- 大数据必须提供
maxItems与maxStringLength:一次展开几万行会让页面停滞。 - 默认展开层数不宜过大:
defaultExpandedDepth超过 2 会把整份数据铺满屏幕。 - 值的类型只靠颜色区分不够,字符串的引号、
null的字面量都要保留。 - 一行被收起时,其内部持有焦点的行会离开 DOM,焦点回到
<body>。应在收起前把焦点交回分支行本身,键盘用户才不会每收一层就丢失位置。
反模式
- 将它用作日志流:日志是时间序的条目,使用日志。
- 把几 MB 的响应体原样传入,让用户自行查找。
API 参考
产物
| 层 | 值 |
|---|---|
| 自定义元素 | <xh-json-viewer> |
| Vue 组件 | XhJsonViewerRoot |
| 组合式函数 | useJsonViewer |
| 状态机 | jsonViewerMachine |
| 皮肤 | @xihan-ui/styles/json-viewer.css |
Props
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
value | unknown | 要展示的值,任意形状。未提供时为空视图(不展开任何行)。 | |
view | JsonViewerView | 展示形态。提供即受控:cell 直读 prop,原文开关只发 onViewChange 不落内部值。 text 档直接输出 JSON 原文:整块可框选可复制,且不受 maxStringLength / maxItems 折减: 目的是与后端下发的内容完全一致。展开集合与键盘导航在该档上不生效。 | |
defaultView | JsonViewerView | 非受控初值,默认 tree。 | |
source | string | 值的原文(如后端下发的那段 JSON 文本)。提供时原文档与复制钮一律取它:能解析成 JSON 就只按两格缩进重排, 数字、字符串等字面量一字不改;解析不了就原样。value 是解析过的值,超出安全整数的数字已经失真, 原文才是与后端一致的那一份。不提供时由 value 序列化。树档照旧展示 value。 | |
wrap | boolean | 原文档是否自动换行。提供即受控,写入只发 onWrapChange。缺省不换行:长行横向滚动,缩进不被折乱。 | |
defaultWrap | boolean | 非受控初值,默认 false。 | |
tools | boolean | readonly JsonViewerTool[] | 内建工具条:不提供即没有工具条;true 铺出全部工具;数组只铺列出的几项。 钮的排列由组件定(展开 / 收起、换行、复制、原文开关依次排在条的末端),与数组顺序无关; 数组里出现未知的工具名即抛错。 | |
variant | ControlVariant | 外框形态:outline 带描边与底色(默认),subtle 淡底无描边,ghost 去掉描边与底色只保留内容。 | |
expandedValue | string[] | 展开集合(元素是行路径)。提供即受控:cell 直读 prop,写入只发 onExpandedValueChange 不落内部值。 | |
defaultExpandedValue | string[] | 非受控初值;未提供时按 defaultExpandedDepth 计算。 | |
defaultExpandedDepth | number | 初始展开到第几层(层级号不超过它的分支全部展开),默认 1,即只展开根行。 | |
maxStringLength | number | 字符串值超过该字符数即截断并补省略号;未提供时不截断。 | |
maxItems | number | 同一层最多展开该数量的成员,其余收为一行占位;未提供时全部展开。 | |
sortKeys | boolean | 对象键按字典序排列;数组顺序不受影响。 | |
loop | boolean | 上下键到达首尾是否回绕,默认 false。 | |
dir | Direction | 文字方向,只对调左右方向键的展开 / 收起语义;未提供时从 DOM 读取。 | |
size | Size | 尺寸:sm / md / lg。 | |
search | string | 搜索词:键名与值文本里含有它(不区分大小写)的行即为命中。 命中行的祖先分支自动展开(写进展开集合),命中片段由作者按 api.segments 铺成 mark 部件。 空串与只含空白视为没有搜索。 | |
translations | Partial<JsonViewerTranslations> | ||
onExpandedValueChange | (details: JsonViewerExpandedValueChangeDetails) => void | ||
onViewChange | (details: JsonViewerViewChangeDetails) => void | 原文开关切换了展示形态;受控时由宿主据此回写 view。 | |
onWrapChange | (details: JsonViewerWrapChangeDetails) => void | 换行开关切换了原文档的换行;受控时由宿主据此回写 wrap。 | |
onCopyError | (details: JsonViewerCopyErrorDetails) => void | 复制钮写入剪贴板失败时通知;此时复制钮已回到常态,不留下「已复制」的假象。 |
事件
自定义元素将载荷放在 detail;Vue 使用同名 emit。
| 事件 | 载荷 | 说明 |
|---|---|---|
expanded-value-change | JsonViewerExpandedValueChangeDetails | 展开集合变化;detail 为 { value: string[] } |
view-change | JsonViewerViewChangeDetails | 原文开关切换了展示形态;detail 为 { view: 'tree' | 'text' } |
wrap-change | JsonViewerWrapChangeDetails | 换行开关切换了原文档的换行;detail 为 { wrap: boolean } |
copy-error | JsonViewerCopyErrorDetails | 复制钮写入剪贴板失败;detail 为 { error: unknown, value: string } |
插槽
仅列出带载荷的插槽。
| Vue 组件 | 插槽 | 载荷 | 说明 |
|---|---|---|---|
XhJsonViewerRoot | empty | — | |
XhJsonViewerRoot | toolbar | JsonViewerToolbarSlotProps | 树之前的一条:放搜索框与上一条 / 下一条,渲染为根的前一个兄弟。 |
React 适配器 props
只列各组件自己声明的那些:继承自 ComponentPropsWithRef 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。
| React 组件 | 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
XhJsonViewerRoot | empty | ReactNode | 空态格子的内容;未写时铺设 translations 中的兜底文案。 | |
XhJsonViewerRoot | toolbar | SlotChildren<JsonViewerToolbarSlotProps> | 树之前的一条:放搜索框与上一条 / 下一条,渲染为根的前一个兄弟。 |
状态
公开状态写入 data-state。
| 部件 | 取值 |
|---|---|
branch | 'open' | 'closed' |
branch-control | 'open' | 'closed' |
branch-trigger | 'open' | 'closed' |
branch-indicator | 'open' | 'closed' |
branch-text | 'open' | 'closed' |
branch-summary | 'open' | 'closed' |
branch-content | 'open' | 'closed' |
preview | 'open' | 'closed' |
expand-trigger | 'open' | 'closed' |
wrap-trigger | 'on' | 'off' |
copy-trigger | 'idle' | 'copying' | 'copied' |
status | 'idle' | 'copying' | 'copied' |
以下名称仅用于内部状态机。
状态:idle · copying · copied
事件:EXPANDED.SET · BRANCH.EXPAND · BRANCH.COLLAPSE · BRANCH.TOGGLE · NODE.FOCUS · VIEWER.BLUR · PRESS.START · PRESS.END · MATCH.SET · SEARCH.SYNC · VIEW.SET · WRAP.SET · COPY.TRIGGER · COPY.SUCCESS · COPY.ERROR · after.timeout · TOOL.PRESS.START · TOOL.PRESS.END
connect API
getXxxProps() 返回对应部件的宿主属性。
| 成员 | 类型 | 说明 |
|---|---|---|
visibleNodes | readonly JsonViewerNode[] | 当前可见行序列(收起分支的子行不在其中)。 方向键、Home/End 都在它上面移动,适配器也按它铺设 DOM。 |
expandedValue | string[] | |
focusedValue | string | null | roving tabindex 的锚点行:焦点在树内时即当前行,焦点离开后仍保留(Tab 回来时落回它); 它已随分支收起而不再可见时为 null。 |
isFocusWithin | boolean | 焦点当前是否在树内。行的高亮标记随它变化,锚点不随之变化。 |
isExpanded | (value: string) => boolean | |
previewText | (node: JsonViewerNode) => string | 分支收起摘要的显示文字(如 {…} 3),只用于视觉;叶子行返回空串。 |
branchSummary | (node: JsonViewerNode) => string | 分支的可及名字(键名,根行取 translations.root;收起时带上成员数),铺进 branch-summary。 |
valueText | (node: JsonViewerNode) => string | 值的显示文字;截断占位行返回其余 N 项的文案。 |
setExpandedValue | (next: string[]) => void | |
expand | (value: string) => void | |
collapse | (value: string) => void | |
toggle | (value: string) => void | |
view | JsonViewerView | 当前生效的展示形态。 |
wrap | boolean | 原文档当前是否自动换行。 |
tools | readonly JsonViewerTool[] | 工具条上铺的工具,按条上的排列顺序;没有工具条时为空。 |
copyStatus | JsonViewerCopyStatus | 复制钮的写入相位。 |
copied | boolean | 已复制成功且仍在停留窗口内:复制钮换成对钩、播报区念出成功回执的唯一判据。 |
announcement | string | 播报区该念的语句;未到已复制档时为空串。 |
setView | (view: JsonViewerView) => void | |
setWrap | (wrap: boolean) => void | |
expandAll | () => void | 展开全部分支(maxItems 折掉的成员与循环引用照旧不展开)。 |
collapseAll | () => void | 收起到只剩顶层:根行留着展开,其下的分支全部收起。 |
allExpanded | boolean | 每一层的分支都已展开(maxItems 折掉的成员与循环引用不算分支)。展开 / 收起钮据此决定这一下是展开还是收起; 只有根一层分支、没有可展开的子层时为 false。原文档不算,恒为 false。 |
copy | () => void | 把 JSON 原文写进剪贴板,与点复制钮走同一路径。 |
isEmpty | boolean | 无法展开任何一行:value 未提供或为 undefined。空态部件随它显隐。 |
emptyText | string | 空态的兜底文案,作者未向空态部件写入内容时铺设它。 |
text | string | 原文档展示、复制钮写入的那份文本:提供了 source 时是它重排缩进后的样子,否则由 value 序列化(键序与环路记号与树档一致)。 |
textSegments | readonly JsonViewerTextSegment[] | 原文按类别切成的段,依次拼接恒等于 text:键名、标量与标点各成一段,铺成 token 部件着色,空白按纯文本铺。 原文解析不了时整段一个不着色的段。只在原文档切分,树档为空:树档用不上,不白算一遍。 |
searchMatches | readonly string[] | 命中搜索词的行路径,按树序排列;没有搜索时为空。 |
activeMatch | string | null | 在命中之间逐个走时停在哪一行;没有时为 null。 |
nextMatch | () => void | 停到下一条命中(走到末尾回到第一条),并把它滚进视野。 |
prevMatch | () => void | 停到上一条命中(走到开头回到最后一条),并把它滚进视野。 |
setActiveMatch | (value: string | null) => void | |
keySegments | (node: JsonViewerNode) => readonly HighlightSegment[] | 键名按搜索词切成的片段,依次拼接恒等于键名;命中的片段铺成 mark 部件。 没有搜索或没有键名时整段是一个不命中的片段。 |
valueSegments | (node: JsonViewerNode) => readonly HighlightSegment[] | 值文本(即 valueText)按搜索词切成的片段;截断占位行的文案不参与命中。 |
getRootProps | () => T['element'] | |
getTreeProps | () => T['element'] | |
getTextProps | () => T['element'] | |
getTokenProps | (props: JsonViewerTokenProps) => T['element'] | 原文档里有类别的一段(键名、标量、标点),带 data-kind 供皮肤按树档同一套颜色着色。 |
getItemProps | (props: JsonViewerNodeProps) => T['element'] | |
getItemKeyProps | (props: JsonViewerNodeProps) => T['element'] | |
getItemValueProps | (props: JsonViewerNodeProps) => T['element'] | |
getBranchProps | (props: JsonViewerNodeProps) => T['element'] | |
getBranchControlProps | (props: JsonViewerNodeProps) => T['element'] | |
getBranchTriggerProps | (props: JsonViewerNodeProps) => T['element'] | |
getBranchIndicatorProps | (props: JsonViewerNodeProps) => T['element'] | |
getBranchTextProps | (props: JsonViewerNodeProps) => T['element'] | |
getBranchSummaryProps | (props: JsonViewerNodeProps) => T['element'] | 视觉隐藏的分支名字,分支行的可及名字即取自它。 |
getBranchContentProps | (props: JsonViewerNodeProps) => T['element'] | |
getPreviewProps | (props: JsonViewerNodeProps) => T['element'] | |
getEmptyProps | () => T['element'] | |
getMarkProps | () => T['element'] | 键名与值文本里命中搜索词的那一段。 |
getToolbarProps | () => T['element'] | 内建工具条:根的第一个孩子,各颗钮按 tools 铺在其中。 报告 role=group:一组有名字的控件,每颗钮各占一个 Tab 位; 不报告 role=toolbar,那个角色承诺条内靠方向键移动。 |
getExpandTriggerProps | () => T['button'] | 展开 / 收起全部的切换钮:data-state 为 open 时已全部展开(再按收起),closed 时还有收着的(再按展开)。 |
getWrapTriggerProps | () => T['button'] | |
getCopyTriggerProps | () => T['button'] | |
getViewTriggerProps | () => T['button'] | |
getStatusProps | () => T['element'] | 复制成功的播报区:视觉隐藏的 polite 实时区,只要有复制钮就常驻,内容取 announcement。 |
无障碍
键盘
规格出处:W3C APG
| 按键 | 生效条件 | 行为 |
|---|---|---|
Tab / Shift+Tab | focus outside the tree | 整棵树只占一个 Tab 位:第一次进来落首行,之后回到上次停留的那一行 |
ArrowDown | focus in tree | 焦点移到下一个可见行(loop 默认关,末行不回绕) |
ArrowUp | focus in tree | 焦点移到上一个可见行(loop 默认关,首行不回绕) |
Home | focus in tree | 焦点移到首个可见行 |
End | focus in tree | 焦点移到末个可见行(展开着的子层也算行) |
ArrowRight | focus on branch(dir=rtl 时改由 ArrowLeft 承担) | 收起的对象/数组就地展开;已展开则把焦点移到首个子行;标量行什么都不做且不吞键 |
ArrowLeft | focus in tree(dir=rtl 时改由 ArrowRight 承担) | 展开的对象/数组就地收起;收起的分支与标量行则把焦点移到父行;根行什么都不做 |
Enter / Space | focus on branch | 切换该分支的展开态;焦点在标量行上时不吞这两个键 |
Enter / Space | held on branch | 按住期间该分支行(branch-control)投影 data-pressed,与指针 :active 同一副按压面(行只换面不缩放);抬起或失焦撤下。展开态的切换照旧由这一次按键承担 |
* | focus in tree | 展开与焦点行同一父级的全部分支(已展开的不动);同级没有可展开的分支时不吞这个键 |
Enter / Space | focus on a toolbar trigger(开启 tools 时) | 触发该钮:展开 / 收起全部、切换自动换行或原文、复制原文;按住期间投影 data-pressed,抬起或失焦撤下。工具钮各占一个 Tab 位,条内不靠方向键移动 |
ARIA
以下属性由 connect 生成。
| 部件 | 属性 | 值 |
|---|---|---|
tree | aria-label | label.tree |
tree | role | 'tree' |
item | aria-level | node?.level |
item | aria-posinset | node?.posInSet |
item | aria-setsize | node?.setSize |
item | role | 'treeitem' |
branch | aria-expanded | 'true' | 'false' |
branch | aria-level | node?.level |
branch | aria-posinset | node?.posInSet |
branch | aria-setsize | node?.setSize |
branch | role | 'treeitem' |
branch-trigger | aria-hidden | 'true' |
branch-indicator | aria-hidden | 'true' |
branch-text | aria-hidden | 'true' |
branch-content | role | 'group' |
preview | aria-hidden | 'true' |
text | aria-label | label.text |
text | role | 'region' |
toolbar | aria-label | label.toolbar |
toolbar | role | 'group' |
wrap-trigger | aria-pressed | 'true' | 'false' |
status | aria-atomic | 'true' |
status | aria-live | 'polite' |
status | role | 'status' |
- 树是
role=tree,每一行是role=treeitem,层级三项(aria-level/aria-posinset/aria-setsize)取自展开结果。 - 整棵树只占一个 Tab 位:首次进入落在首行,之后 Tab 离开再返回时落回上次停留的行;组内靠上下键移动。
- 展开箭头对读屏隐藏,它重复的是分支自身已报出的
aria-expanded与左右方向键。 - 分支的名称显式提供(
aria-label):它包裹整棵子层,从内容计算名称会把所有子孙的文字一并读出。 - 收起摘要(
{…} 3)是排版记号,对读屏隐藏;其中的成员数并入分支的可访问名称(默认读为tags, 3 items,整句可用translations.collapsedBranchLabel替换)。 - 工具条是
role=group并带可访问名称(translations.toolbar),每颗钮各占一个 Tab 位;不报告role=toolbar,条内不靠方向键移动。钮里只有字形,名称取自translations;自动换行是开关钮,开关状态由aria-pressed报告、名称不变;展开 / 收起与树 / 原文两颗切换钮的名称随动作变化(如「展开全部」「收起全部」),不再另报aria-pressed,免得名称与按下态各说各的。 - 复制成功的回执由视觉隐藏的
status部件(aria-live="polite")读出,钮上的对钩只是视觉标记。
样式参考
皮肤
@xihan-ui/styles/json-viewer.css 按 [data-scope="json-viewer"][data-part="root"] 部件选择器书写,发布产物以挂载类 .xh-scope-json-viewer 代替其中的 data-scope(特异性相同),位于 xihan.components 层。覆盖样式使用 xihan.overrides,部件选择器照常可用。
forced-colors: active 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。
数据属性
由 connect 生成;条件不成立时不输出无值属性。
| 部件 | 属性 | 值 |
|---|---|---|
root | data-size | props.size |
root | data-variant | props.variant |
root | data-view | context.get('view') |
item | data-circular | ''(条件成立时才出现) |
item | data-current | ''(条件成立时才出现) |
item | data-highlighted | ''(条件成立时才出现) |
item | data-match | ''(条件成立时才出现) |
item | data-truncated | ''(条件成立时才出现) |
item | data-value-type | node?.type |
item-key | data-circular | ''(条件成立时才出现) |
item-key | data-current | ''(条件成立时才出现) |
item-key | data-highlighted | ''(条件成立时才出现) |
item-key | data-match | ''(条件成立时才出现) |
item-key | data-truncated | ''(条件成立时才出现) |
item-key | data-value-type | node?.type |
item-value | data-circular | ''(条件成立时才出现) |
item-value | data-current | ''(条件成立时才出现) |
item-value | data-highlighted | ''(条件成立时才出现) |
item-value | data-match | ''(条件成立时才出现) |
item-value | data-truncated | ''(条件成立时才出现) |
item-value | data-value-type | node?.type |
branch | data-circular | ''(条件成立时才出现) |
branch | data-current | ''(条件成立时才出现) |
branch | data-highlighted | ''(条件成立时才出现) |
branch | data-match | ''(条件成立时才出现) |
branch | data-state | 'open' | 'closed' |
branch | data-truncated | ''(条件成立时才出现) |
branch | data-value-type | node?.type |
branch-control | data-circular | ''(条件成立时才出现) |
branch-control | data-current | ''(条件成立时才出现) |
branch-control | data-highlighted | ''(条件成立时才出现) |
branch-control | data-match | ''(条件成立时才出现) |
branch-control | data-pressed | ''(条件成立时才出现) |
branch-control | data-state | 'open' | 'closed' |
branch-control | data-truncated | ''(条件成立时才出现) |
branch-control | data-value-type | node?.type |
branch-trigger | data-circular | ''(条件成立时才出现) |
branch-trigger | data-current | ''(条件成立时才出现) |
branch-trigger | data-highlighted | ''(条件成立时才出现) |
branch-trigger | data-match | ''(条件成立时才出现) |
branch-trigger | data-state | 'open' | 'closed' |
branch-trigger | data-truncated | ''(条件成立时才出现) |
branch-trigger | data-value-type | node?.type |
branch-indicator | data-circular | ''(条件成立时才出现) |
branch-indicator | data-current | ''(条件成立时才出现) |
branch-indicator | data-highlighted | ''(条件成立时才出现) |
branch-indicator | data-match | ''(条件成立时才出现) |
branch-indicator | data-state | 'open' | 'closed' |
branch-indicator | data-truncated | ''(条件成立时才出现) |
branch-indicator | data-value-type | node?.type |
branch-text | data-circular | ''(条件成立时才出现) |
branch-text | data-current | ''(条件成立时才出现) |
branch-text | data-highlighted | ''(条件成立时才出现) |
branch-text | data-match | ''(条件成立时才出现) |
branch-text | data-state | 'open' | 'closed' |
branch-text | data-truncated | ''(条件成立时才出现) |
branch-text | data-value-type | node?.type |
branch-summary | data-circular | ''(条件成立时才出现) |
branch-summary | data-current | ''(条件成立时才出现) |
branch-summary | data-highlighted | ''(条件成立时才出现) |
branch-summary | data-match | ''(条件成立时才出现) |
branch-summary | data-state | 'open' | 'closed' |
branch-summary | data-truncated | ''(条件成立时才出现) |
branch-summary | data-value-type | node?.type |
branch-content | data-circular | ''(条件成立时才出现) |
branch-content | data-current | ''(条件成立时才出现) |
branch-content | data-highlighted | ''(条件成立时才出现) |
branch-content | data-match | ''(条件成立时才出现) |
branch-content | data-state | 'open' | 'closed' |
branch-content | data-truncated | ''(条件成立时才出现) |
branch-content | data-value-type | node?.type |
preview | data-circular | ''(条件成立时才出现) |
preview | data-current | ''(条件成立时才出现) |
preview | data-highlighted | ''(条件成立时才出现) |
preview | data-match | ''(条件成立时才出现) |
preview | data-state | 'open' | 'closed' |
preview | data-truncated | ''(条件成立时才出现) |
preview | data-value-type | node?.type |
text | data-wrap | ''(条件成立时才出现) |
token | data-kind | props.kind |
toolbar | data-view | context.get('view') |
expand-trigger | data-state | 'open' | 'closed' |
expand-trigger | data-xh-action-control | '' |
expand-trigger | data-xh-action-display | 'always' |
expand-trigger | data-xh-action-profile | 'icon' |
expand-trigger | data-xh-action-size | 'md' | 'sm' |
expand-trigger | data-xh-action-variant | 'ghost' |
wrap-trigger | data-state | 'on' | 'off' |
wrap-trigger | data-xh-action-control | '' |
wrap-trigger | data-xh-action-display | 'always' |
wrap-trigger | data-xh-action-profile | 'icon' |
wrap-trigger | data-xh-action-size | 'md' | 'sm' |
wrap-trigger | data-xh-action-variant | 'ghost' |
copy-trigger | data-copied | ''(条件成立时才出现) |
copy-trigger | data-state | 'idle' | 'copying' | 'copied' |
copy-trigger | data-xh-action-control | '' |
copy-trigger | data-xh-action-display | 'always' |
copy-trigger | data-xh-action-profile | 'icon' |
copy-trigger | data-xh-action-size | 'md' | 'sm' |
copy-trigger | data-xh-action-variant | 'ghost' |
view-trigger | data-view | context.get('view') |
view-trigger | data-xh-action-control | '' |
view-trigger | data-xh-action-display | 'always' |
view-trigger | data-xh-action-profile | 'icon' |
view-trigger | data-xh-action-size | 'md' | 'sm' |
view-trigger | data-xh-action-variant | 'ghost' |
status | data-state | 'idle' | 'copying' | 'copied' |
CSS 变量
本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。
| 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 |
|---|---|---|---|---|---|
--xh-json-viewer-bg | emptyroottexttree | background | defaultis([data-scope='json-viewer'][data-part='tree'], [data-scope='json-viewer'][data-part='text'], [data-scope='json-viewer'][data-part='empty'])variant=subtle | --xh-bg-subtle--xh-bg-surface | json-viewer 的 empty、root、text、tree 部件 background 覆盖槽。 |
--xh-json-viewer-boolean-fg | item-valuetoken | color | kind=booleanvalue-type=boolean | --xh-syntax-keyword | json-viewer 的 item-value、token 部件 color 覆盖槽。 |
--xh-json-viewer-border | emptytexttree | border | default | --xh-border-default | json-viewer 的 empty、text、tree 部件 border 覆盖槽。 |
--xh-json-viewer-empty-fg | empty | color | default | --xh-fg-muted | json-viewer 的 empty 部件 color 覆盖槽。 |
--xh-json-viewer-empty-gap | empty | gap | default | --xh-space-2 | json-viewer 的 empty 部件 gap 覆盖槽。 |
--xh-json-viewer-empty-px | empty | padding-inline | default | --xh-space-4 | json-viewer 的 empty 部件 padding-inline 覆盖槽。 |
--xh-json-viewer-empty-py | empty | padding-block | default | --xh-space-6 | json-viewer 的 empty 部件 padding-block 覆盖槽。 |
--xh-json-viewer-fg | root | color | default | --xh-fg-default | json-viewer 的 root 部件 color 覆盖槽。 |
--xh-json-viewer-font | roottext | font-family | default | --xh-font-family-mono | json-viewer 的 root、text 部件 font-family 覆盖槽。 |
--xh-json-viewer-font-size | root | font-size | default | --xh-_json-viewer-font-size | json-viewer 的 root 部件 font-size 覆盖槽。 |
--xh-json-viewer-icon-size | root | --xh-icon-size | defaultsize=lgsize=sm | --xh-glyph-size-lg--xh-glyph-size-md--xh-glyph-size-sm | json-viewer 的 root 部件 --xh-icon-size 覆盖槽。 |
--xh-json-viewer-indent | branch-content | padding-inline-start | default | --xh-_json-viewer-indent | json-viewer 的 branch-content 部件 padding-inline-start 覆盖槽。 |
--xh-json-viewer-indicator-fg | branch-trigger | color | default | --xh-fg-subtle | json-viewer 的 branch-trigger 部件 color 覆盖槽。 |
--xh-json-viewer-indicator-size | branch-trigger | --xh-icon-sizeinline-size | default | --xh-control-indicator-size | json-viewer 的 branch-trigger 部件 --xh-icon-size、inline-size 覆盖槽。 |
--xh-json-viewer-key-fg | branch-textitem-keytoken | color | defaultkind=key | --xh-fg-brand-strong | json-viewer 的 branch-text、item-key、token 部件 color 覆盖槽。 |
--xh-json-viewer-key-font-weight | branch-textitem-keytoken | font-weight | defaultkind=key | --xh-font-weight-medium | json-viewer 的 branch-text、item-key、token 部件 font-weight 覆盖槽。 |
--xh-json-viewer-mark-bg | mark | background | default | --xh-bg-brand-subtle | json-viewer 的 mark 部件 background 覆盖槽。 |
--xh-json-viewer-mark-bg-current | branchbranch-controlbranch-textitemitem-keyitem-valuemark | background | current | --xh-bg-brand | json-viewer 的 branch、branch-control、branch-text、item、item-key、item-value、mark 部件 background 覆盖槽。 |
--xh-json-viewer-mark-fg | mark | color | default | --xh-fg-brand-strong | json-viewer 的 mark 部件 color 覆盖槽。 |
--xh-json-viewer-mark-fg-current | branchbranch-controlbranch-textitemitem-keyitem-valuemark | color | current | --xh-fg-on-brand | json-viewer 的 branch、branch-control、branch-text、item、item-key、item-value、mark 部件 color 覆盖槽。 |
--xh-json-viewer-mark-radius | mark | border-radius | default | --xh-shape-inset | json-viewer 的 mark 部件 border-radius 覆盖槽。 |
--xh-json-viewer-max-h | texttree | max-block-size | default | --xh-viewport-max-h | json-viewer 的 text、tree 部件 max-block-size 覆盖槽。 |
--xh-json-viewer-null-fg | item-valuetoken | color | kind=nullvalue-type=null | --xh-fg-subtle | json-viewer 的 item-value、token 部件 color 覆盖槽。 |
--xh-json-viewer-number-fg | item-valuetoken | color | kind=numbervalue-type=number | --xh-syntax-number | json-viewer 的 item-value、token 部件 color 覆盖槽。 |
--xh-json-viewer-preview-fg | preview | color | default | --xh-fg-muted | json-viewer 的 preview 部件 color 覆盖槽。 |
--xh-json-viewer-preview-font-size | preview | font-size | default | --xh-text-caption-size | json-viewer 的 preview 部件 font-size 覆盖槽。 |
--xh-json-viewer-punctuation-fg | branch-textitem-keyitem-valuetoken | color | defaultkind=punctuationvalue-type=arrayvalue-type=object | --xh-fg-subtle | json-viewer 的 branch-text、item-key、item-value、token 部件 color 覆盖槽。 |
--xh-json-viewer-px | texttree | padding-inline | default | --xh-space-2 | json-viewer 的 text、tree 部件 padding-inline 覆盖槽。 |
--xh-json-viewer-py | texttree | padding-block | default | --xh-space-2 | json-viewer 的 text、tree 部件 padding-block 覆盖槽。 |
--xh-json-viewer-radius | emptytexttree | border-radius | default | --xh-shape-surface | json-viewer 的 empty、text、tree 部件 border-radius 覆盖槽。 |
--xh-json-viewer-row-bg-active | branch-control | background | disabledis(:active, [data-pressed])not([data-disabled])pressed | --xh-bg-subtle-hover | json-viewer 的 branch-control 部件 background 覆盖槽。 |
--xh-json-viewer-row-bg-hover | branch-controlitem | background | highlightedis(:hover, [data-highlighted]) | --xh-bg-subtle | json-viewer 的 branch-control、item 部件 background 覆盖槽。 |
--xh-json-viewer-row-gap | branch-controlitem | gap | default | --xh-space-1 | json-viewer 的 branch-control、item 部件 gap 覆盖槽。 |
--xh-json-viewer-row-px | branch-controlitem | padding-inline | default | --xh-space-1 | json-viewer 的 branch-control、item 部件 padding-inline 覆盖槽。 |
--xh-json-viewer-row-py | branch-controlitem | padding-block | default | --xh-_json-viewer-row-py | json-viewer 的 branch-control、item 部件 padding-block 覆盖槽。 |
--xh-json-viewer-row-radius | branch-controlitem | border-radius | default | --xh-shape-inset | json-viewer 的 branch-control、item 部件 border-radius 覆盖槽。 |
--xh-json-viewer-shadow | emptytexttree | box-shadow | default | none | json-viewer 的 empty、text、tree 部件 box-shadow 覆盖槽。 |
--xh-json-viewer-string-fg | item-valuetoken | color | kind=stringvalue-type=string | --xh-syntax-string | json-viewer 的 item-value、token 部件 color 覆盖槽。 |
--xh-json-viewer-text-fg | text | color | default | --xh-fg-default | json-viewer 的 text 部件 color 覆盖槽。 |
--xh-json-viewer-tool-fg | statustoolbar | color | where(:not([data-part='status']) | --xh-fg-muted | json-viewer 的 status、toolbar 部件 color 覆盖槽。 |
--xh-json-viewer-tool-fg-copied | copy-trigger | color | copieddisabledfocus-visiblehoveris(:active, [data-pressed])loadingnot([data-disabled])not([data-loading])pressed | --xh-fg-success | json-viewer 的 copy-trigger 部件 color 覆盖槽。 |
--xh-json-viewer-tool-icon-size | statustoolbar | --xh-icon-size | where(:not([data-part='status']) | --xh-_action-profile-glyph-size | json-viewer 的 status、toolbar 部件 --xh-icon-size 覆盖槽。 |
--xh-json-viewer-toolbar-gap | toolbar | gap | default | --xh-space-0_5 | json-viewer 的 toolbar 部件 gap 覆盖槽。 |
--xh-json-viewer-toolbar-mb | toolbar | margin-block-end | default | --xh-space-1 | json-viewer 的 toolbar 部件 margin-block-end 覆盖槽。 |
动效
动效角色:状态 · 切换(见动效规范)。
background-color · rotate 走 transition 过渡。时长与缓动读动效令牌,改令牌即改全局节奏。
系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。
RTL
皮肤用逻辑属性排布(inline-start 一族),dir="rtl" 下自动镜像;另有按 dir 分支的规则。
- 左右方向键的展开 / 收起语义跟随书写方向:未传
dir时从 DOM 读取,整页dir="rtl"也能识别。
