Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/jsonviwer customrender #2676

Open
wants to merge 8 commits into
base: release
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 86 additions & 6 deletions content/plus/jsonviewer/index-en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,78 @@ function FormatJsonComponent() {
render(FormatJsonComponent);
```

### Custom Render Rules

By configuring the `options.customRenderRule` parameter, you can customize how JSON content is rendered (Note: only works in read-only mode).

`customRenderRule` is an array of rules, where each rule contains two properties:
- `match`: Matching condition, can be one of three types:
- String: Exact match
- Regular expression: Match by regex
- Function: Custom matching logic, with signature `(value: string, pathChain: string) => boolean`
- `value`: Value to match (key or value from JSON key-value pairs, as strings since internal processing only filters quotes)
- `path`: Current matching path, format is `root.key1.key2.key3[0].key4`
- `render`: Custom render function, with signature `(content: string) => React.ReactNode`
- `content`: Matched content. For string values, includes double quotes (e.g., `"name"`, `"Semi"`)

```jsx live=true dir="column" noInline=true
import React, { useRef } from 'react';
import { JsonViewer, Button, Rating, Popover, Tag, Image } from '@douyinfe/semi-ui';
const data = `{
"name": "Semi",
"version": "2.7.4",
"rating": 5,
"tags": ["design", "react", "ui"],
"image": "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg"
}`;
function CustomRenderJsonComponent() {
const jsonviewerRef = useRef();
const customRenderRule = [
{
match: 'Semi',
render: (content) => {
return <Popover showArrow content={'I am a custom render'} trigger='hover'><span>{content}</span></Popover>;
}
},
{
match: (value)=> value == 5,
render: (content) => {
return <Rating defaultValue={content} size={10} disabled/>;
}
},
{
match: (value, path)=> path === 'root.tags[0]' || path === 'root.tags[1]' || path === 'root.tags[2]',
render: (content) => {
return <Tag size='small' shape='circle'>{content}</Tag>;
}
},
{
match: new RegExp('^http'),
render: (content) => {
// content is original string with quotes, need to remove quotes for valid URL
return <Popover showArrow content={<Image width={100} height={100} src={content.replace(/^"|"$/g, '')} />} trigger='hover'><span>{content}</span></Popover>;
}
}
];
return (
<div>
<div style={{ marginBottom: 16, marginTop: 16 }}>
<JsonViewer
ref={jsonviewerRef}
height={200}
width={600}
value={data}
showSearch={false}
options={{ formatOptions: { tabSize: 4, insertSpaces: true, eol: '\n' }, customRenderRule, readOnly: true, autoWrap: true }}
/>
</div>
</div>
);
}

render(CustomRenderJsonComponent);
```

## API Reference

### JsonViewer
Expand All @@ -167,12 +239,13 @@ render(FormatJsonComponent);

### JsonViewerOptions

| Attribute | Description | Type | Default |
| ------------- | --------------------------------------- | ----------------- | ------- |
| lineHeight | Height of each line of content, unit:px | number | 20 |
| autoWrap | Whether to wrap lines automatically. | boolean | true |
| readOnly | Whether to be read-only. | boolean | false |
| formatOptions | Content format setting | FormattingOptions | - |
| Attribute | Description | Type | Default | Version |
| ------------- | --------------------------------------- | ----------------- | ------- | ------- |
| lineHeight | Height of each line of content, unit:px | number | 20 | - |
| autoWrap | Whether to wrap lines automatically. | boolean | true | - |
| readOnly | Whether to be read-only. | boolean | false | - |
| customRenderRule | Custom render rules | CustomRenderRule[] | - | 2.74.0 |
| formatOptions | Content format setting | FormattingOptions | - | - |

### FormattingOptions

Expand All @@ -182,6 +255,13 @@ render(FormatJsonComponent);
| insertSpaces | Whether to use spaces for indentation | boolean | true |
| eol | Line break character | string | '\n' |

### CustomRenderRule

| Attribute | Description | Type | Default |
| --- | --- | --- | --- |
| match | Matching rule | string \| RegExp \| (value: string, path: string) => boolean | - |
| render | Render function | (content: string) => React.ReactNode | - |

## Methods

Methods bound to the component instance can be called via `ref` to achieve certain special interactions.
Expand Down
89 changes: 84 additions & 5 deletions content/plus/jsonviewer/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,78 @@ function FormatJsonComponent() {
render(FormatJsonComponent);
```

### 自定义渲染规则

通过配置 `options.customRenderRule` 参数,你可以自定义 JSON 内容的渲染方式(注意:仅在只读模式下生效)。

`customRenderRule` 是一个规则数组,每条规则包含两个属性:
- `match`: 匹配条件,可以是以下三种类型之一:
- 字符串:精确匹配
- 正则表达式:按正则匹配
- 函数:自定义匹配逻辑,函数签名为 `(value: string, path: string) => boolean`
- `value`: 待匹配的值(为Json字符串的键值对的键或者值,由于内部处理注入时仅过滤引号,因此类型全部为string)
- `path`: 当前匹配到的路径,格式为 `root.key1.key2.key3[0].key4`
- `render`: 自定义渲染函数,函数签名为 `(content: string) => React.ReactNode`
- `content`: 匹配到的内容。如果是字符串类型的值,将包含双引号(如 `"name"`,`"Semi"`)

```jsx live=true dir="column" noInline=true
import React, { useRef } from 'react';
import { JsonViewer, Button, Rating, Popover, Tag, Image } from '@douyinfe/semi-ui';
const data = `{
"name": "Semi",
"version": "2.7.4",
"rating": 5,
"tags": ["design", "react", "ui"],
"image": "https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/abstract.jpg"
}`;
function CustomRenderJsonComponent() {
const jsonviewerRef = useRef();
const customRenderRule = [
{
match: 'Semi',
render: (content) => {
return <Popover showArrow content={'我是用户自定义的渲染'} trigger='hover'><span>{content}</span></Popover>;
}
},
{
match: (value)=> value == 5,
render: (content) => {
return <Rating defaultValue={content} size={10} disabled/>;
}
},
{
match: (value, path)=> path === 'root.tags[0]' || path === 'root.tags[1]' || path === 'root.tags[2]',
render: (content) => {
return <Tag size='small' shape='circle'>{content}</Tag>;
}
},
{
match: new RegExp('^http'),
render: (content) => {
// content 为原始字符串,包含引号,因此需要去除引号才可以作为合法的url
return <Popover showArrow content={<Image width={100} height={100} src={content.replace(/^"|"$/g, '')} />} trigger='hover'><span>{content}</span></Popover>;
}
}
];
return (
<div>
<div style={{ marginBottom: 16, marginTop: 16 }}>
<JsonViewer
ref={jsonviewerRef}
height={200}
width={600}
value={data}
showSearch={false}
options={{ formatOptions: { tabSize: 4, insertSpaces: true, eol: '\n' }, customRenderRule, readOnly: true, autoWrap: true }}
/>
</div>
</div>
);
}

render(CustomRenderJsonComponent);
```


## API 参考

Expand All @@ -159,17 +231,24 @@ render(FormatJsonComponent);
| className | 类名 | string | - |
| style | 内联样式 | object | - |
| showSearch | 是否显示搜索Icon | boolean | true |
| options | 格式化配置 | JsonViewerOptions | - |
| options | 编辑器配置 | JsonViewerOptions | - |
| onChange | 内容变化回调 | (value: string) => void | - |

### JsonViewerOptions

| 属性 | 说明 | 类型 | 默认值 | 版本
|-------------------|------------------------------------------------|---------------------------------|-----------|---------|
| lineHeight | 行高 | number | 20 | - |
| autoWrap | 是否自动换行 | boolean | true | - |
| readOnly | 是否只读 | boolean | false | - |
| customRenderRule | 自定义渲染规则 | CustomRenderRule[] | - | 2.74.0 |
| formatOptions | 格式化配置 | FormattingOptions | - | - |

### CustomRenderRule
| 属性 | 说明 | 类型 | 默认值 |
|-------------------|------------------------------------------------|---------------------------------|-----------|
| lineHeight | 行高 | number | 20 |
| autoWrap | 是否自动换行 | boolean | true |
| readOnly | 是否只读 | boolean | false |
| formatOptions | 格式化配置 | FormattingOptions | - |
| match | 匹配规则 | string \| RegExp \| (value: string, path: string) => boolean | - |
| render | 渲染函数 | (content: string) => React.ReactNode | - |

### FormattingOptions

Expand Down
22 changes: 11 additions & 11 deletions cypress/e2e/jsonViewer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,23 +77,23 @@ describe('jsonViewer', () => {
typeTextAtPosition(2, 7, `:`);
typeTextAtPosition(2, 8, `1`);
typeTextAtPosition(2, 9, `,`);
cy.get('.lines-content').children().eq(1).children().should('have.length', 5);
cy.get('.lines-content').children().eq(1).children().children().should('have.length', 5);


// undo redo
undo(1);
cy.get('.lines-content').children().eq(1).children().should('have.length', 4);
cy.get('.lines-content').children().eq(1).children().children().should('have.length', 4);
redo(1);
cy.get('.lines-content').children().eq(1).children().should('have.length', 5);
cy.get('.lines-content').children().eq(1).children().children().should('have.length', 5);
undo(8);
cy.get('.lines-content').children().eq(1).children().should('have.length', 6);
cy.get('.lines-content').children().eq(1).children().children().should('have.length', 6);

//del
typeTextAtPosition(2, 1, `{backspace}`);
cy.get('.lines-content').children().eq(0).children().should('have.length', 7);
cy.get('.lines-content').children().eq(0).children().children().should('have.length', 7);
undo(1);
cy.get('.lines-content').children().eq(0).children().should('have.length', 1);
cy.get('.lines-content').children().eq(1).children().should('have.length', 6);
cy.get('.lines-content').children().eq(0).children().children().should('have.length', 1);
cy.get('.lines-content').children().eq(1).children().children().should('have.length', 6);

// cut
// typeTextAtPosition(2, 1, `{meta+x}`);
Expand All @@ -103,19 +103,19 @@ describe('jsonViewer', () => {

//complete
typeTextAtPosition(14, 4, '{enter}');
cy.get('.lines-content').children().eq(14).children().should('have.length', 1);
cy.get('.lines-content').children().eq(14).children().children().should('have.length', 1);
typeTextAtPosition(15, 4, `c`);
cy.get('.semi-json-viewer-complete-suggestions-container').children().should('have.length', 2);
cy.get('.lines-content').type('{enter}');
cy.get('.semi-json-viewer-complete-container').should('have.css', 'display', 'none');
cy.get('.lines-content').children().eq(14).children().should('have.length', 2);
cy.get('.lines-content').children().eq(14).children().children().should('have.length', 2);
typeTextAtPosition(15, 11, `:`);
cy.get('.semi-json-viewer-complete-container').should('have.css', 'display', 'block');
cy.get('.semi-json-viewer-complete-suggestions-container').children().should('have.length', 2);
cy.get('.lines-content').type('{enter}');
cy.get('.semi-json-viewer-complete-container').should('have.css', 'display', 'none');
typeTextAtPosition(15, 19, `,{enter}`);
cy.get('.lines-content').children().eq(14).children().should('have.length', 5);
cy.get('.lines-content').children().eq(14).children().children().should('have.length', 5);
typeTextAtPosition(16, 4, `a`);
cy.get('.semi-json-viewer-complete-suggestions-container').children().should('have.length', 2);
typeTextAtPosition(16, 5, `{rightArrow}`);
Expand All @@ -127,7 +127,7 @@ describe('jsonViewer', () => {
typeTextAtPosition(16, 9, `:`);
cy.get('.lines-content').type('{enter}');
cy.get('.semi-json-viewer-complete-container').should('have.css', 'display', 'none');
cy.get('.lines-content').children().eq(15).children().should('have.length', 4);
cy.get('.lines-content').children().eq(15).children().children().should('have.length', 4);

//search
cy.get('.semi-json-viewer-search-bar-trigger').click();
Expand Down
19 changes: 9 additions & 10 deletions packages/semi-foundation/jsonViewer/foundation.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@

import { JsonViewer, JsonViewerOptions } from '@douyinfe/semi-json-viewer-core';
import BaseFoundation, { DefaultAdapter, noopFunction } from '../base/foundation';
import { JsonViewer, JsonViewerOptions, CustomRenderRule } from '@douyinfe/semi-json-viewer-core';
import BaseFoundation, { DefaultAdapter } from '../base/foundation';

export type { JsonViewerOptions };
export type { JsonViewerOptions, CustomRenderRule };
export interface JsonViewerAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
getEditorRef: () => HTMLElement;
getSearchRef: () => HTMLInputElement;
notifyChange: (value: string) => void;
notifyHover: (value: string, el: HTMLElement) => HTMLElement | undefined;
setSearchOptions: (key: string) => void;
showSearchBar: () => void
showSearchBar: () => void;
notifyCustomRender: (customRenderMap: Map<HTMLElement, any>) => void
}

class JsonViewerFoundation extends BaseFoundation<JsonViewerAdapter> {
Expand All @@ -23,19 +24,17 @@ class JsonViewerFoundation extends BaseFoundation<JsonViewerAdapter> {
const props = this.getProps();
const editorRef = this._adapter.getEditorRef();
this.jsonViewer = new JsonViewer(editorRef, props.value, props.options);
this.jsonViewer.emitter.on('customRender', (e) => {
this._adapter.notifyCustomRender(e.customRenderMap);
});
this.jsonViewer.layout();
this.jsonViewer.emitter.on('contentChanged', (e) => {
this._adapter.notifyChange(this.jsonViewer?.getModel().getValue());
if (this.getState('showSearchBar')) {
this.search(this._adapter.getSearchRef().value);
}
});
this.jsonViewer.emitter.on('hoverNode', (e) => {
const el = this._adapter.notifyHover(e.value, e.target);
if (el) {
this.jsonViewer.emitter.emit('renderHoverNode', { el });
}
});

}

search(searchText: string) {
Expand Down
9 changes: 8 additions & 1 deletion packages/semi-json-viewer-core/src/common/emitterEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ export interface GlobalEvents {
problemsChanged: IProblemsChangedEvent;
hoverNode: IHoverNodeEvent;
renderHoverNode: IRenderHoverNodeEvent;
forceRender: undefined
forceRender: undefined;
customRender: ICustomRenderEvent
}

interface IRange {
Expand Down Expand Up @@ -50,3 +51,9 @@ export interface IHoverNodeEvent {
value: string;
target: HTMLElement
}

export interface ICustomRenderEvent {
customRenderMap: ICustomRenderMap
}

export type ICustomRenderMap = Map<HTMLElement, any>;
8 changes: 7 additions & 1 deletion packages/semi-json-viewer-core/src/json-viewer/jsonViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export interface JsonViewerOptions {
autoWrap?: boolean;
readOnly?: boolean;
formatOptions?: FormattingOptions;
completionOptions?: CompletionOptions
completionOptions?: CompletionOptions;
customRenderRule?: CustomRenderRule[]
}

export interface CompletionOptions {
Expand All @@ -27,6 +28,11 @@ export interface FormattingOptions {
eol?: string
}

export interface CustomRenderRule {
match: string | RegExp | ((value: string, pathChain: string) => boolean);
render: (value: string) => HTMLElement
}

export class JsonViewer {
private _container: HTMLElement;
private _jsonModel: JSONModel;
Expand Down
Loading
Loading