diff --git a/README.md b/README.md
index c75cd86..16f3113 100644
--- a/README.md
+++ b/README.md
@@ -29,9 +29,10 @@ English | [简体中文](./README.zh_CN.md)
## Features
- As a JSON Formatter.
-- Get item data from JSON.
-- Written in TypeScript with predictable static types.
+- Written in TypeScript, support `d.ts`.
+- Support get item data from JSON.
- Support big data.
+- Support editable.
## Environment Support
@@ -52,10 +53,10 @@ $ npm install vue-json-pretty --save
$ yarn add vue-json-pretty
```
-## Use Vue3
+## Use Vue2
```bash
-$ npm install vue-json-pretty@next --save
+$ npm install vue-json-pretty@v1-latest --save
```
## Usage
@@ -65,7 +66,7 @@ The CSS file is included separately and needs to be imported manually. You can e
```vue
-
+
@@ -105,37 +106,47 @@ plugins: [
## Props
-- If you are using only the normal features (JSON pretty), just focus on the `base` properties.
-- If you are using higher features (Get data), you can use `base` and `higher` attributes.
-
-| Attribute | Level | Description | Type | Default |
-| ------------------------ | ------ | --------------------------------------------------------------------------------------- | ---------------------------------------------- | -------- |
-| data | normal | JSON data | JSON object | - |
-| deep | normal | Data depth, data larger than this depth will not be expanded | number | Infinity |
-| deepCollapseChildren | normal | Whether children collapsed by `deep` prop should also be collapsed | boolean | false |
-| showLength | normal | Whether to show the length when closed | boolean | false |
-| showLine | normal | Whether to show the line | boolean | true |
-| showDoubleQuotes | normal | Whether to show doublequotes on key | boolean | true |
-| virtual | normal | Whether to use virtual scrolling, usually used for big data | boolean | false |
-| itemHeight | normal | The height of each item when using virtual scrolling | number | auto |
-| v-model | higher | Defines value when the tree can be selected | string, array | - |
-| path | higher | Root data path | string | root |
-| pathSelectable | higher | Defines whether a data path supports selection | function(path, content) | - |
-| selectableType | higher | Defines the selected type, this feature is not supported by default | multiple, single | - |
-| showSelectController | higher | Whether to show the select controller at left | boolean | false |
-| selectOnClickNode | higher | Whether to change selected value when click node | boolean | true |
-| highlightSelectedNode | higher | Highlight current node when selected | boolean | true |
-| collapsedOnClickBrackets | higher | Collapsed control | boolean | true |
-| customValueFormatter | higher | A function that can return different html or strings to display for values in the data. | function(data, key, path, defaultFormatResult) | - |
+| Property | Description | Type | Default |
+| ------------------------ | ----------------------------------------------- | --------------------------------- | ------- |
+| data(v-model) | JSON data, support v-model when use editable | JSON object | - |
+| deep | Paths greater than this depth will be collapsed | number | - |
+| showLength | Show the length when collapsed | boolean | false |
+| showLine | Show the line | boolean | true |
+| showLineNumber | Show the line number | boolean | false |
+| showIcon | Show the icon | boolean | false |
+| showDoubleQuotes | Show doublequotes on key | boolean | true |
+| virtual | Use virtual scroll | boolean | false |
+| height | The height of list when using virtual | number | 400 |
+| itemHeight | The height of node when using virtual | number | 20 |
+| selectedValue(v-model) | Selected data path | string, array | - |
+| rootPath | Root data path | string | `root` |
+| nodeSelectable | Defines whether a node supports selection | (node) => boolean | - |
+| selectableType | Support path select, default none | `multiple` \| `single` | - |
+| showSelectController | Show the select controller | boolean | false |
+| selectOnClickNode | Trigger select when click node | boolean | true |
+| highlightSelectedNode | Support highlighting selected nodes | boolean | true |
+| collapsedOnClickBrackets | Support click brackets to collapse | boolean | true |
+| renderNodeValue | render node value, or use slot #renderNodeValue | ({ node, defaultValue }) => vNode | - |
+| editable | Support editable | boolean | false |
+| editableTrigger | Trigger | `click` \| `dblclick` | `click` |
## Events
-| Event Name | Description | Callback Parameters |
-| ---------- | ---------------------------------------------------------------------------- | ------------------- |
-| click | triggered when a data item is clicked | (path, data) |
-| change | triggered when the selected value changed (only the selectableType not null) | (newVal, oldVal) |
+| Event Name | Description | Parameters |
+| -------------- | ---------------------------------------- | -------------------- |
+| nodeClick | triggers when click node | (node: NodeData) |
+| bracketsClick | triggers when click brackets | (collapsed: boolean) |
+| iconClick | triggers when click icon | (collapsed: boolean) |
+| selectedChange | triggers when the selected value changed | (newVal, oldVal) |
-## Major Contributors
+## Slots
-[](https://github.com/rchl)
-[](https://github.com/blackmad)
+| Slot Name | Description | Parameters |
+| --------------- | ----------------- | ---------------------- |
+| renderNodeValue | render node value | { node, defaultValue } |
+
+## Contributors
+
+
+
+
diff --git a/README.zh_CN.md b/README.zh_CN.md
index 7b15a47..cec3008 100644
--- a/README.zh_CN.md
+++ b/README.zh_CN.md
@@ -3,37 +3,48 @@
## 特性
- 一个 JSON 美化工具
-- 提取字段层级数据
-- 使用 Typescript,提供类型描述
+- 使用 Typescript,提供类型描述 `d.ts`
+- 支持字段层级数据提取
- 支持大数据虚拟滚动
+- 支持编辑
## Props
-- 若仅使用基础功能(JSON 美化),只需关注功能级别为 `基础` 的属性。
-- 若使用高级功能(选择数据),你可以同时使用 `基础` 与 `高级` 的属性。
-
-| 属性 | 级别 | 说明 | 类型 | 默认值 |
-| ------------------------ | ---- | ----------------------------------------- | ---------------------------------------------- | ------------- |
-| data | 基础 | 待美化的源数据,注意不是 `JSON` 字符串 | `JSON` 对象 | - |
-| deep | 基础 | 数据深度, 大于该深度的数据将不被展开 | number | Infinity |
-| showLength | 基础 | 是否在数据线闭合的时候展示长度 | boolean | false |
-| showLine | 基础 | 是否显示缩紧标识线 | boolean | true |
-| showDoubleQuotes | 基础 | 是否展示 key 名的双引号 | boolean | true |
-| virtual | 基础 | 是否使用虚拟滚动(大数据量时) | boolean | false |
-| itemHeight | 基础 | 使用虚拟滚动时,定义每一行高度 | number | auto |
-| v-model | 高级 | 双向绑定选中的数据层级 | string, array | string, array |
-| path | 高级 | 定义最顶层数据层级 | string | root |
-| pathSelectable | 高级 | 定义哪些数据层级是可以被选中的 | function(path, content) | - |
-| selectableType | 高级 | 定义组件支持的选中方式,默认无选中功能 | multiple, single | - |
-| showSelectController | 高级 | 是否展示选择控制器 | boolean | false |
-| selectOnClickNode | 高级 | 是否在点击节点的时候触发 v-model 双向绑定 | boolean | true |
-| highlightSelectedNode | 高级 | 是否高亮已选项 | boolean | true |
-| collapsedOnClickBrackets | 高级 | 是否支持折叠 | boolean | true |
-| customValueFormatter | 高级 | 可以进行值的自定义渲染 | function(data, key, path, defaultFormatResult) | - |
+| 属性 | 说明 | 类型 | 默认值 |
+| ------------------------ | ------------------------------------------- | --------------------------------- | ------------- |
+| data(v-model) | 源数据,注意不是 `JSON` 字符串 | `JSON` 数据对象 | - |
+| deep | 深度,大于该深度的节点将被折叠 | number | Infinity |
+| showLength | 在数据折叠的时候展示长度 | boolean | false |
+| showLine | 展示标识线 | boolean | true |
+| showLineNumber | 展示行计数 | boolean | false |
+| showIcon | 展示图标 | boolean | false |
+| showDoubleQuotes | 展示 key 名的双引号 | boolean | true |
+| virtual | 使用虚拟滚动(大数据量) | boolean | false |
+| height | 使用虚拟滚动时,定义总高度 | number | 400 |
+| itemHeight | 使用虚拟滚动时,定义节点高度 | number | 20 |
+| selectedValue(v-model) | 双向绑定选中的数据路径 | string, array | string, array |
+| rootPath | 定义最顶层数据路径 | string | `root` |
+| nodeSelectable | 定义哪些数据节点可以被选择 | function(node) | - |
+| selectableType | 定义选择功能,默认无 | `multiple` \| `single` | - |
+| showSelectController | 展示选择器 | boolean | false |
+| selectOnClickNode | 支持点击节点的时候触发选择 | boolean | true |
+| highlightSelectedNode | 支持高亮已选择节点 | boolean | true |
+| collapsedOnClickBrackets | 支持点击括号折叠 | boolean | true |
+| renderNodeValue | 自定义渲染节点值,也可使用 #renderNodeValue | ({ node, defaultValue }) => vNode | - |
+| editable | 支持可编辑 | boolean | false |
+| editableTrigger | 触发编辑的时机 | `click` \| `dblclick` | `click` |
## Events
-| 事件名 | 说明 | 回调参数 |
-| ------ | -------------------------------------- | ---------------- |
-| click | 点击某一个数据层级时触发的事件 | (path, data) |
-| change | v-model 改变的事件(仅在选择模式下可用) | (newVal, oldVal) |
+| 事件名称 | 说明 | 回调参数 |
+| -------------- | -------------------- | -------------------- |
+| nodeClick | 点击节点时触发 | (node: NodeData) |
+| bracketsClick | 点击括号时触发 | (collapsed: boolean) |
+| iconClick | 点击图标时触发 | (collapsed: boolean) |
+| selectedChange | 选中值发生变化时触发 | (newVal, oldVal) |
+
+## Slots
+
+| 插槽名 | 描述 | 参数 |
+| --------------- | ---------- | ---------------------- |
+| renderNodeValue | 渲染节点值 | { node, defaultValue } |
diff --git a/example/App.tsx b/example/App.tsx
index cc62ba7..730fd7f 100644
--- a/example/App.tsx
+++ b/example/App.tsx
@@ -1,50 +1,91 @@
-import { defineComponent } from 'vue';
+import { defineComponent, reactive } from 'vue';
import Basic from './Basic.vue';
import VirtualList from './VirtualList.vue';
import SelectControl from './SelectControl.vue';
+import Editable from './Editable.vue';
+// import Tsx from './Tsx';
import './styles.less';
+const list = [
+ {
+ title: 'Basic Use',
+ key: 'Basic',
+ component: Basic,
+ },
+ {
+ title: 'Virtual List',
+ key: 'VirtualList',
+ component: VirtualList,
+ },
+ {
+ title: 'Selector',
+ key: 'Selector',
+ component: SelectControl,
+ },
+ {
+ title: 'Editable',
+ key: 'Editable',
+ component: Editable,
+ },
+ // {
+ // title: 'Tsx',
+ // key: 'Tsx',
+ // component: Tsx,
+ // },
+];
+
export default defineComponent({
+ setup() {
+ const state = reactive({
+ activeKey: list[0].key,
+ opened: [list[0].key],
+ });
+
+ const onActiveChange = (key: string) => {
+ state.activeKey = key;
+ if (!state.opened.includes(key)) {
+ state.opened.push(key);
+ }
+ };
+
+ return {
+ state,
+ onActiveChange,
+ };
+ },
+
render() {
- const list = [
- {
- title: 'Basic Use',
- key: 'basic',
- component: ,
- },
- {
- title: 'Virtual List',
- key: 'virtual-list',
- component: ,
- },
- {
- title: 'Selector',
- key: 'select-control',
- component: ,
- },
- ];
+ const { state, onActiveChange } = this;
return (
- {list.map(item => (
-
-
{item.title}
- {item.component}
+
Vue Json Pretty
+
+ Welcome to the demo space of Vue Json Pretty, here we provide the following different
+ usage scenarios, try to click on different tab panel to browse.
+
+
+
+
+
+
+ {list.map(({ component: Component, key }) => (
+
+ {key === state.activeKey || state.opened.includes(key) ? : null}
+
+ ))}
- ))}
-
-
-
-
+
);
},
diff --git a/example/Basic.vue b/example/Basic.vue
index 49b91e7..76a6295 100644
--- a/example/Basic.vue
+++ b/example/Basic.vue
@@ -1,11 +1,15 @@
-
+
JSON:
Options:
+
+
Slots:
+
@@ -49,14 +57,22 @@
+ :show-icon="state.showIcon"
+ style="position: relative"
+ >
+
+
+ {{ node.content }}
+
+ {{ defaultValue }}
+
+
@@ -80,9 +96,6 @@ const defaultData = {
'Traffic paradise: How to design streets for people and unmanned vehicles in the future?',
source: 'Netease smart',
link: 'http://netease.smart/traffic-paradise/1235',
- author: {
- names: ['Daniel', 'Mike', 'John'],
- },
},
{
news_id: 51182,
@@ -105,21 +118,17 @@ export default defineComponent({
data: defaultData,
showLength: false,
showLine: true,
+ showLineNumber: false,
showDoubleQuotes: true,
collapsedOnClickBrackets: true,
- useCustomLinkFormatter: false,
+ useRenderNodeValueSlot: false,
deep: 4,
- deepCollapseChildren: false,
- collapsePath: /members/,
- collapsePathPattern: 'members',
+ setPathCollapsible: false,
+ showIcon: false,
});
- const customLinkFormatter = (data, key, path, defaultFormatted) => {
- if (typeof data === 'string' && data.startsWith('http://')) {
- return `
"${data}" `;
- } else {
- return defaultFormatted;
- }
+ const pathCollapsible = node => {
+ return node.key === 'members';
};
watch(
@@ -133,21 +142,16 @@ export default defineComponent({
},
);
- watch(
- () => state.collapsePath,
- newVal => {
- try {
- state.collapsePath = new RegExp(newVal);
- } catch (err) {
- // console.log('Regexp ERROR');
- }
- },
- );
-
return {
state,
- customLinkFormatter,
+ pathCollapsible,
};
},
});
+
+
diff --git a/example/Editable.vue b/example/Editable.vue
new file mode 100644
index 0000000..84e1865
--- /dev/null
+++ b/example/Editable.vue
@@ -0,0 +1,126 @@
+
+
+
+
JSON:
+
+
+
Options:
+
+
+ showLine
+
+
+
+ showLineNumber
+
+
+
+ editable
+
+
+
+ editableTrigger
+
+ click
+ dblclick
+
+
+
+ deep
+
+ 2
+ 3
+ 4
+
+
+
+
+
+
vue-json-pretty:
+
+
+
+
+
+
diff --git a/example/SelectControl.vue b/example/SelectControl.vue
index 69cb05e..2bb929e 100644
--- a/example/SelectControl.vue
+++ b/example/SelectControl.vue
@@ -1,11 +1,15 @@
-
+
JSON:
Options:
-
v-model:
-
{{ state.value }}
-
Current Click:
-
path: {{ state.itemPath }}
-
- data:
-
{{ state.itemData }}
+
v-model:selectedValue:
+
{{ state.selectedValue }}
+
Current Node Click:
+
{{ state.node }}
vue-json-pretty:
@@ -132,38 +131,27 @@ export default defineComponent({
renderOK: true,
val: JSON.stringify(defaultData),
data: defaultData,
- value: 'res.error',
+ selectedValue: 'res.error',
selectableType: 'single',
showSelectController: true,
showLength: false,
showLine: true,
- showDoubleQuotes: true,
+ showLineNumber: false,
highlightSelectedNode: true,
selectOnClickNode: true,
collapsedOnClickBrackets: true,
- useCustomLinkFormatter: false,
- path: 'res',
+ rootPath: 'res',
deep: 3,
- itemData: {},
- itemPath: '',
+ node: '',
+ showIcon: false,
});
- const handleClick = (path, data) => {
- // console.log('click: ', path, data);
- state.itemPath = path;
- state.itemData = !data ? data + '' : data; // 处理 data = null 的情况
- };
-
- const handleChange = () => {
- // console.log('newVal: ', newVal, ' oldVal: ', oldVal);
+ const handleNodeClick = node => {
+ state.node = node;
};
- const customLinkFormatter = (data, key, path, defaultFormatted) => {
- if (typeof data === 'string' && data.startsWith('http://')) {
- return `
"${data}" `;
- } else {
- return defaultFormatted;
- }
+ const handleAll = (...rest) => {
+ console.log('handleAll: ', rest);
};
watch(
@@ -182,20 +170,11 @@ export default defineComponent({
async newVal => {
state.renderOK = false;
if (newVal === 'single') {
- state.value = 'res.error';
+ state.selectedValue = 'res.error';
} else if (newVal === 'multiple') {
- state.value = ['res.error', 'res.data[0].title'];
+ state.selectedValue = ['res.error', 'res.data[0].title'];
}
- // 重新渲染, 因为2中情况的v-model格式不同
- await nextTick();
- state.renderOK = true;
- },
- );
-
- watch(
- () => state.useCustomLinkFormatter,
- async () => {
- state.renderOK = false;
+ // Re-render because v-model:selectedValue format is different in case 2
await nextTick();
state.renderOK = true;
},
@@ -203,9 +182,8 @@ export default defineComponent({
return {
state,
- customLinkFormatter,
- handleClick,
- handleChange,
+ handleNodeClick,
+ handleAll,
};
},
});
diff --git a/example/Tsx.tsx b/example/Tsx.tsx
new file mode 100644
index 0000000..5465aa5
--- /dev/null
+++ b/example/Tsx.tsx
@@ -0,0 +1,41 @@
+import { defineComponent, reactive } from 'vue';
+import VueJsonPretty from 'src';
+
+const defaultData = {
+ status: 200,
+ data: [
+ {
+ news_id: 51184,
+ link: 'http://netease.smart/traffic-paradise/51184',
+ },
+ {
+ news_id: 51183,
+ link: 'http://netease.smart/traffic-paradise/51183',
+ },
+ ],
+};
+
+export default defineComponent({
+ setup() {
+ const state = reactive({
+ data: defaultData,
+ });
+
+ return () => (
+
+ typeof node.content === 'string' && node.content?.startsWith('http://') ? (
+
+ {node.content}
+
+ ) : (
+ defaultValue
+ )
+ }
+ />
+ );
+ },
+});
diff --git a/example/VirtualList.vue b/example/VirtualList.vue
index 0320097..572bf06 100644
--- a/example/VirtualList.vue
+++ b/example/VirtualList.vue
@@ -1,5 +1,5 @@
-
+
JSON:
@@ -7,8 +7,8 @@
Options:
-
vue-json-pretty(1000+ items):
+
vue-json-pretty(10000+ items):
({
- news_id: index,
+ data: [],
+};
+
+for (let i = 0; i < 10000; i++) {
+ defaultData.data.push({
+ news_id: i,
title: 'iPhone X Review: Innovative future with real black technology',
source: 'Netease phone',
- })),
-};
+ });
+}
export default defineComponent({
name: 'VirtualList',
@@ -69,7 +72,7 @@ export default defineComponent({
showLine: true,
collapsedOnClickBrackets: true,
deep: 3,
- virtualLines: 20,
+ itemHeight: 20,
});
watch(
diff --git a/example/index.html b/example/index.html
index 884b210..a7451db 100644
--- a/example/index.html
+++ b/example/index.html
@@ -1,11 +1,65 @@
-
+
vue-json-pretty
+
+
+
+
+
+
+
+
diff --git a/example/styles.less b/example/styles.less
index 44579b8..6c0d867 100644
--- a/example/styles.less
+++ b/example/styles.less
@@ -1,55 +1,98 @@
+@primary-color: #1890ff;
+
+* {
+ box-sizing: border-box;
+}
+
html,
body {
margin: 0;
+}
+
+body {
+ font-size: 14px;
background-color: #f9f9f9;
}
-.example {
+
+.tabs-header {
+ display: flex;
+ margin-bottom: 20px;
+ border-bottom: 1px solid #ccc;
+}
+
+.tabs-header-item {
position: relative;
- padding: 0 15px;
- margin: 0 auto;
- width: 1200px;
+ margin-right: 20px;
+ padding: 8px 0;
+ cursor: pointer;
+ transition: color 0.3s;
+
+ &:hover,
+ &.is-active {
+ color: @primary-color;
+
+ &:after {
+ border-bottom: 2px solid @primary-color;
+ content: '';
+ width: 100%;
+ position: absolute;
+ left: 0;
+ bottom: -1px;
+ }
+ }
}
-.example-box {
- margin: 0 -15px;
- overflow: hidden;
- h3 {
- display: inline-block;
+
+.example {
+ padding: 0 20px;
+
+ > h1 {
+ text-align: center;
}
+
+ > p {
+ margin: 20px 20px 20px 0;
+ color: #8c8c8c;
+ }
+}
+
+.example-box {
+ display: flex;
+
.title {
text-align: center;
}
+
.block {
- float: left;
- padding: 0 15px;
width: 50%;
- box-sizing: border-box;
+ &:first-child {
+ padding-right: 30px;
+ }
}
+
input,
select,
textarea {
padding: 3px 8px;
- box-sizing: border-box;
border-radius: 5px;
border: 1px solid #bbb;
font-family: inherit;
&:focus {
outline: none;
- border-color: #1d8ce0;
- box-shadow: 0 0 3px #1d8ce0;
+ border-color: @primary-color;
+ box-shadow: 0 0 3px @primary-color;
}
}
+
textarea {
width: 100%;
height: 150px;
resize: vertical;
}
+
pre {
margin: 0;
font-family: Consolas;
overflow: hidden;
text-overflow: ellipsis;
}
- .options {
- font-size: 14px;
- }
}
diff --git a/package-lock.json b/package-lock.json
index 5359c61..5358db1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1792,12 +1792,20 @@
}
},
"@vue/reactivity": {
- "version": "3.2.33",
- "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.33.tgz",
- "integrity": "sha512-62Sq0mp9/0bLmDuxuLD5CIaMG2susFAGARLuZ/5jkU1FCf9EDbwUuF+BO8Ub3Rbodx0ziIecM/NsmyjardBxfQ==",
+ "version": "3.2.37",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.37.tgz",
+ "integrity": "sha512-/7WRafBOshOc6m3F7plwzPeCu/RCVv9uMpOwa/5PiY1Zz+WLVRWiy0MYKwmg19KBdGtFWsmZ4cD+LOdVPcs52A==",
"dev": true,
"requires": {
- "@vue/shared": "3.2.33"
+ "@vue/shared": "3.2.37"
+ },
+ "dependencies": {
+ "@vue/shared": {
+ "version": "3.2.37",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.37.tgz",
+ "integrity": "sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==",
+ "dev": true
+ }
}
},
"@vue/reactivity-transform": {
@@ -1814,34 +1822,90 @@
}
},
"@vue/runtime-core": {
- "version": "3.2.33",
- "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.33.tgz",
- "integrity": "sha512-N2D2vfaXsBPhzCV3JsXQa2NECjxP3eXgZlFqKh4tgakp3iX6LCGv76DLlc+IfFZq+TW10Y8QUfeihXOupJ1dGw==",
+ "version": "3.2.37",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.37.tgz",
+ "integrity": "sha512-JPcd9kFyEdXLl/i0ClS7lwgcs0QpUAWj+SKX2ZC3ANKi1U4DOtiEr6cRqFXsPwY5u1L9fAjkinIdB8Rz3FoYNQ==",
"dev": true,
"requires": {
- "@vue/reactivity": "3.2.33",
- "@vue/shared": "3.2.33"
+ "@vue/reactivity": "3.2.37",
+ "@vue/shared": "3.2.37"
+ },
+ "dependencies": {
+ "@vue/shared": {
+ "version": "3.2.37",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.37.tgz",
+ "integrity": "sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==",
+ "dev": true
+ }
}
},
"@vue/runtime-dom": {
- "version": "3.2.33",
- "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.33.tgz",
- "integrity": "sha512-LSrJ6W7CZTSUygX5s8aFkraDWlO6K4geOwA3quFF2O+hC3QuAMZt/0Xb7JKE3C4JD4pFwCSO7oCrZmZ0BIJUnw==",
+ "version": "3.2.37",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.37.tgz",
+ "integrity": "sha512-HimKdh9BepShW6YozwRKAYjYQWg9mQn63RGEiSswMbW+ssIht1MILYlVGkAGGQbkhSh31PCdoUcfiu4apXJoPw==",
"dev": true,
"requires": {
- "@vue/runtime-core": "3.2.33",
- "@vue/shared": "3.2.33",
+ "@vue/runtime-core": "3.2.37",
+ "@vue/shared": "3.2.37",
"csstype": "^2.6.8"
+ },
+ "dependencies": {
+ "@vue/shared": {
+ "version": "3.2.37",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.37.tgz",
+ "integrity": "sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==",
+ "dev": true
+ }
}
},
"@vue/server-renderer": {
- "version": "3.2.33",
- "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.33.tgz",
- "integrity": "sha512-4jpJHRD4ORv8PlbYi+/MfP8ec1okz6rybe36MdpkDrGIdEItHEUyaHSKvz+ptNEyQpALmmVfRteHkU9F8vxOew==",
+ "version": "3.2.37",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.37.tgz",
+ "integrity": "sha512-kLITEJvaYgZQ2h47hIzPh2K3jG8c1zCVbp/o/bzQOyvzaKiCquKS7AaioPI28GNxIsE/zSx+EwWYsNxDCX95MA==",
"dev": true,
"requires": {
- "@vue/compiler-ssr": "3.2.33",
- "@vue/shared": "3.2.33"
+ "@vue/compiler-ssr": "3.2.37",
+ "@vue/shared": "3.2.37"
+ },
+ "dependencies": {
+ "@vue/compiler-core": {
+ "version": "3.2.37",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.37.tgz",
+ "integrity": "sha512-81KhEjo7YAOh0vQJoSmAD68wLfYqJvoiD4ulyedzF+OEk/bk6/hx3fTNVfuzugIIaTrOx4PGx6pAiBRe5e9Zmg==",
+ "dev": true,
+ "requires": {
+ "@babel/parser": "^7.16.4",
+ "@vue/shared": "3.2.37",
+ "estree-walker": "^2.0.2",
+ "source-map": "^0.6.1"
+ }
+ },
+ "@vue/compiler-dom": {
+ "version": "3.2.37",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.37.tgz",
+ "integrity": "sha512-yxJLH167fucHKxaqXpYk7x8z7mMEnXOw3G2q62FTkmsvNxu4FQSu5+3UMb+L7fjKa26DEzhrmCxAgFLLIzVfqQ==",
+ "dev": true,
+ "requires": {
+ "@vue/compiler-core": "3.2.37",
+ "@vue/shared": "3.2.37"
+ }
+ },
+ "@vue/compiler-ssr": {
+ "version": "3.2.37",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.37.tgz",
+ "integrity": "sha512-7mQJD7HdXxQjktmsWp/J67lThEIcxLemz1Vb5I6rYJHR5vI+lON3nPGOH3ubmbvYGt8xEUaAr1j7/tIFWiEOqw==",
+ "dev": true,
+ "requires": {
+ "@vue/compiler-dom": "3.2.37",
+ "@vue/shared": "3.2.37"
+ }
+ },
+ "@vue/shared": {
+ "version": "3.2.37",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.37.tgz",
+ "integrity": "sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==",
+ "dev": true
+ }
}
},
"@vue/shared": {
@@ -6159,9 +6223,9 @@
"dev": true
},
"moment": {
- "version": "2.29.3",
- "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz",
- "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==",
+ "version": "2.29.4",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
+ "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"dev": true
},
"mrmime": {
@@ -9070,16 +9134,87 @@
}
},
"vue": {
- "version": "3.2.33",
- "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.33.tgz",
- "integrity": "sha512-si1ExAlDUrLSIg/V7D/GgA4twJwfsfgG+t9w10z38HhL/HA07132pUQ2KuwAo8qbCyMJ9e6OqrmWrOCr+jW7ZQ==",
+ "version": "3.2.37",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.37.tgz",
+ "integrity": "sha512-bOKEZxrm8Eh+fveCqS1/NkG/n6aMidsI6hahas7pa0w/l7jkbssJVsRhVDs07IdDq7h9KHswZOgItnwJAgtVtQ==",
"dev": true,
"requires": {
- "@vue/compiler-dom": "3.2.33",
- "@vue/compiler-sfc": "3.2.33",
- "@vue/runtime-dom": "3.2.33",
- "@vue/server-renderer": "3.2.33",
- "@vue/shared": "3.2.33"
+ "@vue/compiler-dom": "3.2.37",
+ "@vue/compiler-sfc": "3.2.37",
+ "@vue/runtime-dom": "3.2.37",
+ "@vue/server-renderer": "3.2.37",
+ "@vue/shared": "3.2.37"
+ },
+ "dependencies": {
+ "@vue/compiler-core": {
+ "version": "3.2.37",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.37.tgz",
+ "integrity": "sha512-81KhEjo7YAOh0vQJoSmAD68wLfYqJvoiD4ulyedzF+OEk/bk6/hx3fTNVfuzugIIaTrOx4PGx6pAiBRe5e9Zmg==",
+ "dev": true,
+ "requires": {
+ "@babel/parser": "^7.16.4",
+ "@vue/shared": "3.2.37",
+ "estree-walker": "^2.0.2",
+ "source-map": "^0.6.1"
+ }
+ },
+ "@vue/compiler-dom": {
+ "version": "3.2.37",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.37.tgz",
+ "integrity": "sha512-yxJLH167fucHKxaqXpYk7x8z7mMEnXOw3G2q62FTkmsvNxu4FQSu5+3UMb+L7fjKa26DEzhrmCxAgFLLIzVfqQ==",
+ "dev": true,
+ "requires": {
+ "@vue/compiler-core": "3.2.37",
+ "@vue/shared": "3.2.37"
+ }
+ },
+ "@vue/compiler-sfc": {
+ "version": "3.2.37",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.37.tgz",
+ "integrity": "sha512-+7i/2+9LYlpqDv+KTtWhOZH+pa8/HnX/905MdVmAcI/mPQOBwkHHIzrsEsucyOIZQYMkXUiTkmZq5am/NyXKkg==",
+ "dev": true,
+ "requires": {
+ "@babel/parser": "^7.16.4",
+ "@vue/compiler-core": "3.2.37",
+ "@vue/compiler-dom": "3.2.37",
+ "@vue/compiler-ssr": "3.2.37",
+ "@vue/reactivity-transform": "3.2.37",
+ "@vue/shared": "3.2.37",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.25.7",
+ "postcss": "^8.1.10",
+ "source-map": "^0.6.1"
+ }
+ },
+ "@vue/compiler-ssr": {
+ "version": "3.2.37",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.37.tgz",
+ "integrity": "sha512-7mQJD7HdXxQjktmsWp/J67lThEIcxLemz1Vb5I6rYJHR5vI+lON3nPGOH3ubmbvYGt8xEUaAr1j7/tIFWiEOqw==",
+ "dev": true,
+ "requires": {
+ "@vue/compiler-dom": "3.2.37",
+ "@vue/shared": "3.2.37"
+ }
+ },
+ "@vue/reactivity-transform": {
+ "version": "3.2.37",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.37.tgz",
+ "integrity": "sha512-IWopkKEb+8qpu/1eMKVeXrK0NLw9HicGviJzhJDEyfxTR9e1WtpnnbYkJWurX6WwoFP0sz10xQg8yL8lgskAZg==",
+ "dev": true,
+ "requires": {
+ "@babel/parser": "^7.16.4",
+ "@vue/compiler-core": "3.2.37",
+ "@vue/shared": "3.2.37",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.25.7"
+ }
+ },
+ "@vue/shared": {
+ "version": "3.2.37",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.37.tgz",
+ "integrity": "sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==",
+ "dev": true
+ }
}
},
"vue-eslint-parser": {
diff --git a/package.json b/package.json
index 2d7ca45..2ae02e5 100644
--- a/package.json
+++ b/package.json
@@ -71,7 +71,7 @@
"tsc-alias": "^1.6.9",
"typescript": "^4.3.4",
"url-loader": "^4.1.0",
- "vue": "^3.1.2",
+ "vue": "^3.2.37",
"vue-loader": "^16.2.0",
"vue-style-loader": "^4.1.3",
"webpack": "^5.40.0",
diff --git a/src/components/Brackets/index.tsx b/src/components/Brackets/index.tsx
index 0c4feff..15fc205 100644
--- a/src/components/Brackets/index.tsx
+++ b/src/components/Brackets/index.tsx
@@ -16,7 +16,7 @@ export default defineComponent({
const { onClick } = this;
return (
-
+
{data}
);
diff --git a/src/components/Brackets/styles.less b/src/components/Brackets/styles.less
index c5cb869..9ff3ab5 100644
--- a/src/components/Brackets/styles.less
+++ b/src/components/Brackets/styles.less
@@ -1,6 +1,6 @@
@import '~src/themes.less';
-.@{css-prefix}-tree__brackets {
+.@{css-prefix}-tree-brackets {
cursor: pointer;
&:hover {
color: @color-primary;
diff --git a/src/components/Carets/index.tsx b/src/components/Carets/index.tsx
new file mode 100644
index 0000000..39e9028
--- /dev/null
+++ b/src/components/Carets/index.tsx
@@ -0,0 +1,40 @@
+import { defineComponent, PropType } from 'vue';
+import './styles.less';
+
+export default defineComponent({
+ props: {
+ nodeType: {
+ required: true,
+ type: String,
+ },
+ onClick: Function as PropType<(e: MouseEvent) => void>,
+ },
+
+ render() {
+ const { nodeType } = this;
+
+ const { onClick } = this;
+
+ const isOpen = nodeType === 'objectStart' || nodeType === 'arrayStart';
+
+ const isClose = nodeType === 'objectCollapsed' || nodeType === 'arrayCollapsed';
+
+ if (!isOpen && !isClose) return null;
+
+ return (
+
+
+
+
+
+ );
+ },
+});
diff --git a/src/components/Carets/styles.less b/src/components/Carets/styles.less
new file mode 100644
index 0000000..884f1fd
--- /dev/null
+++ b/src/components/Carets/styles.less
@@ -0,0 +1,19 @@
+@import '~src/themes.less';
+
+.@{css-prefix}-carets {
+ position: absolute;
+ right: 0;
+ cursor: pointer;
+
+ svg {
+ transition: transform 0.3s;
+ }
+
+ &:hover {
+ color: @color-primary;
+ }
+}
+
+.@{css-prefix}-carets-close {
+ transform: rotate(-90deg);
+}
diff --git a/src/components/CheckController/index.tsx b/src/components/CheckController/index.tsx
index 0ddd13e..ff5dd80 100644
--- a/src/components/CheckController/index.tsx
+++ b/src/components/CheckController/index.tsx
@@ -35,10 +35,10 @@ export default defineComponent({
class={[`vjs-check-controller`, model ? 'is-checked' : '']}
onClick={e => e.stopPropagation()}
>
-
+
$emit('change', model)}
/>
diff --git a/src/components/CheckController/styles.less b/src/components/CheckController/styles.less
index 0591c22..fc8bbd3 100644
--- a/src/components/CheckController/styles.less
+++ b/src/components/CheckController/styles.less
@@ -4,7 +4,7 @@
position: absolute;
left: 0;
- &.is-checked .@{css-prefix}-check-controller__inner {
+ &.is-checked .@{css-prefix}-check-controller-inner {
background-color: @color-primary;
border-color: darken(@color-primary, 10%);
@@ -17,7 +17,7 @@
}
}
- .@{css-prefix}-check-controller__inner {
+ .@{css-prefix}-check-controller-inner {
display: inline-block;
position: relative;
border: 1px solid @border-color;
@@ -61,7 +61,7 @@
}
}
- .@{css-prefix}-check-controller__original {
+ .@{css-prefix}-check-controller-original {
opacity: 0;
outline: none;
position: absolute;
diff --git a/src/components/Tree/index.tsx b/src/components/Tree/index.tsx
index 3c00e31..47f845a 100644
--- a/src/components/Tree/index.tsx
+++ b/src/components/Tree/index.tsx
@@ -1,10 +1,16 @@
-import { defineComponent, reactive, computed, watchEffect, ref, PropType } from 'vue';
+import {
+ defineComponent,
+ reactive,
+ computed,
+ watchEffect,
+ ref,
+ PropType,
+ CSSProperties,
+} from 'vue';
import TreeNode, { treeNodePropsPass, NodeDataType } from 'src/components/TreeNode';
-import { emitError, jsonFlatten, JSONDataType } from 'src/utils';
+import { emitError, jsonFlatten, JSONDataType, cloneDeep } from 'src/utils';
import './styles.less';
-type FlatDataType = NodeDataType[];
-
export default defineComponent({
name: 'Tree',
@@ -20,16 +26,12 @@ export default defineComponent({
type: Number,
default: Infinity,
},
- deepCollapseChildren: {
- type: Boolean,
- default: false,
- },
- collapsePath: {
- type: RegExp,
- default: null,
+ pathCollapsible: {
+ type: Function as PropType<(node: NodeDataType) => boolean>,
+ default: (): boolean => false,
},
// Data root path.
- path: {
+ rootPath: {
type: String,
default: 'root',
},
@@ -38,10 +40,10 @@ export default defineComponent({
type: Boolean,
default: false,
},
- //When using virtual scroll, set the number of items there can be
- virtualLines: {
+ // When using virtual scroll, set the height of tree.
+ height: {
type: Number,
- default: 10,
+ default: 400,
},
// When using virtual scroll, define the height of each row.
itemHeight: {
@@ -50,27 +52,43 @@ export default defineComponent({
},
// When there is a selection function, define the selected path.
// For multiple selections, it is an array ['root.a','root.b'], for single selection, it is a string of 'root.a'.
- modelValue: {
+ selectedValue: {
type: [String, Array] as PropType,
default: () => '',
},
+ // Collapsed control.
+ collapsedOnClickBrackets: {
+ type: Boolean,
+ default: true,
+ },
+ style: Object as PropType,
+ onSelectedChange: {
+ type: Function as PropType<(newVal: string | string[], oldVal: string | string[]) => void>,
+ },
},
- emits: ['click', 'change', 'update:modelValue'],
+ slots: ['renderNodeValue'],
+
+ emits: [
+ 'nodeClick',
+ 'bracketsClick',
+ 'iconClick',
+ 'selectedChange',
+ 'update:selectedValue',
+ 'update:data',
+ ],
- setup(props, { emit }) {
- const tree = ref();
+ setup(props, { emit, slots }) {
+ const treeRef = ref();
+
+ const originFlatData = computed(() => jsonFlatten(props.data, props.rootPath));
const state = reactive({
translateY: 0,
- visibleData: null as FlatDataType | null,
- hiddenPaths: jsonFlatten(props.data, props.path).reduce((acc, item) => {
- const depthComparison = props.deepCollapseChildren
- ? item.level >= props.deep
- : item.level === props.deep;
- const pathComparison =
- depthComparison ||
- (props.collapsePath && props.collapsePath.test(item.path));
+ visibleData: null as NodeDataType[] | null,
+ hiddenPaths: originFlatData.value.reduce((acc, item) => {
+ const depthComparison = item.level >= props.deep;
+ const pathComparison = props.pathCollapsible?.(item as NodeDataType);
if (
(item.type === 'objectStart' || item.type === 'arrayStart') &&
(depthComparison || pathComparison)
@@ -86,34 +104,39 @@ export default defineComponent({
const flatData = computed(() => {
let startHiddenItem: null | NodeDataType = null;
- const data = jsonFlatten(props.data, props.path).reduce((acc, cur, index) => {
+ const data = [];
+ const length = originFlatData.value.length;
+ for (let i = 0; i < length; i++) {
+ const cur = originFlatData.value[i];
const item = {
...cur,
- id: index,
+ id: i,
};
const isHidden = state.hiddenPaths[item.path];
if (startHiddenItem && startHiddenItem.path === item.path) {
const isObject = startHiddenItem.type === 'objectStart';
const mergeItem = {
- ...startHiddenItem,
...item,
+ ...startHiddenItem,
+ showComma: item.showComma,
content: isObject ? '{...}' : '[...]',
type: isObject ? 'objectCollapsed' : 'arrayCollapsed',
} as NodeDataType;
startHiddenItem = null;
- return acc.concat(mergeItem);
+ data.push(mergeItem);
} else if (isHidden && !startHiddenItem) {
startHiddenItem = item;
- return acc;
+ continue;
+ } else {
+ if (startHiddenItem) continue;
+ else data.push(item);
}
-
- return startHiddenItem ? acc : acc.concat(item);
- }, [] as FlatDataType);
+ }
return data;
});
const selectedPaths = computed(() => {
- const value = props.modelValue;
+ const value = props.selectedValue;
if (value && props.selectableType === 'multiple' && Array.isArray(value)) {
return value;
}
@@ -127,11 +150,11 @@ export default defineComponent({
: '';
});
- const updateVisibleData = (flatDataValue: FlatDataType) => {
+ const updateVisibleData = () => {
+ const flatDataValue = flatData.value;
if (props.virtual) {
- const treeRefValue = tree.value;
- const visibleCount = props.virtualLines;
- const scrollTop = (treeRefValue && treeRefValue.scrollTop) || 0;
+ const visibleCount = props.height / props.itemHeight;
+ const scrollTop = treeRef.value?.scrollTop || 0;
const scrollCount = Math.floor(scrollTop / props.itemHeight);
let start =
scrollCount < 0
@@ -150,11 +173,11 @@ export default defineComponent({
}
};
- const onTreeScroll = () => {
- updateVisibleData(flatData.value);
+ const handleTreeScroll = () => {
+ updateVisibleData();
};
- const onSelectedChange = ({ path }: NodeDataType) => {
+ const handleSelectedChange = ({ path }: NodeDataType) => {
const type = props.selectableType;
if (type === 'multiple') {
const index = selectedPaths.value.findIndex(item => item === path);
@@ -164,23 +187,23 @@ export default defineComponent({
} else {
newVal.push(path);
}
- emit('update:modelValue', newVal);
- emit('change', newVal, [...selectedPaths.value]);
+ emit('update:selectedValue', newVal);
+ emit('selectedChange', newVal, [...selectedPaths.value]);
} else if (type === 'single') {
if (selectedPaths.value[0] !== path) {
const [oldVal] = selectedPaths.value;
const newVal = path;
- emit('update:modelValue', newVal);
- emit('change', newVal, oldVal);
+ emit('update:selectedValue', newVal);
+ emit('selectedChange', newVal, oldVal);
}
}
};
- const onTreeNodeClick = ({ content, path }: NodeDataType) => {
- emit('click', path, content);
+ const handleNodeClick = (node: NodeDataType) => {
+ emit('nodeClick', node);
};
- const onBracketsClick = (collapsed: boolean, path: string) => {
+ const updateCollapsedPaths = (collapsed: boolean, path: string) => {
if (collapsed) {
state.hiddenPaths = {
...state.hiddenPaths,
@@ -193,6 +216,25 @@ export default defineComponent({
}
};
+ const handleBracketsClick = (collapsed: boolean, path: string) => {
+ if (props.collapsedOnClickBrackets) {
+ updateCollapsedPaths(collapsed, path);
+ }
+ emit('bracketsClick', collapsed);
+ };
+
+ const handleIconClick = (collapsed: boolean, path: string) => {
+ updateCollapsedPaths(collapsed, path);
+ emit('iconClick', collapsed);
+ };
+
+ const handleValueChange = (value: unknown, path: string) => {
+ const newData = cloneDeep(props.data);
+ const rootPath = props.rootPath;
+ new Function('data', 'val', `data${path.slice(rootPath.length)}=val`)(newData, value);
+ emit('update:data', newData);
+ };
+
watchEffect(() => {
if (propsErrorMessage.value) {
emitError(propsErrorMessage.value);
@@ -201,84 +243,83 @@ export default defineComponent({
watchEffect(() => {
if (flatData.value) {
- updateVisibleData(flatData.value);
+ updateVisibleData();
}
});
- return {
- tree,
- state,
- flatData,
- selectedPaths,
- onTreeScroll,
- onSelectedChange,
- onTreeNodeClick,
- onBracketsClick,
- };
- },
-
- render() {
- const {
- virtual,
- itemHeight,
- customValueFormatter,
- showDoubleQuotes,
- showLength,
- showLine,
- showSelectController,
- selectOnClickNode,
- pathSelectable,
- highlightSelectedNode,
- collapsedOnClickBrackets,
- state,
- flatData,
- selectedPaths,
- selectableType,
- } = this;
-
- const { onTreeNodeClick, onBracketsClick, onSelectedChange, onTreeScroll } = this;
+ return () => {
+ const renderNodeValue = props.renderNodeValue ?? slots.renderNodeValue;
- const nodeContent =
- state.visibleData &&
- state.visibleData.map(item => (
-
- ));
+ const nodeContent =
+ state.visibleData &&
+ state.visibleData.map(item => (
+
+ ));
- return (
-
- {virtual ? (
-
- ) : (
- nodeContent
- )}
-
- );
+ return (
+
+ {props.virtual ? (
+
+ ) : (
+ nodeContent
+ )}
+
+ );
+ };
},
});
diff --git a/src/components/Tree/styles.less b/src/components/Tree/styles.less
index 4a30bc1..a81ded4 100644
--- a/src/components/Tree/styles.less
+++ b/src/components/Tree/styles.less
@@ -8,7 +8,7 @@
&.is-virtual {
overflow: auto;
- .@{css-prefix}-tree__node {
+ .@{css-prefix}-tree-node {
white-space: nowrap;
}
}
diff --git a/src/components/TreeNode/index.tsx b/src/components/TreeNode/index.tsx
index 3c9ee8c..ed1ae69 100644
--- a/src/components/TreeNode/index.tsx
+++ b/src/components/TreeNode/index.tsx
@@ -1,7 +1,8 @@
-import { defineComponent, reactive, computed, PropType } from 'vue';
+import { defineComponent, reactive, computed, PropType, CSSProperties } from 'vue';
import Brackets from 'src/components/Brackets';
import CheckController from 'src/components/CheckController';
-import { getDataType, JSONFlattenReturnType } from 'src/utils';
+import Carets from 'src/components/Carets';
+import { getDataType, JSONFlattenReturnType, stringToAutoType } from 'src/utils';
import './styles.less';
export interface NodeDataType extends JSONFlattenReturnType {
@@ -20,9 +21,9 @@ export const treeNodePropsPass = {
type: Boolean,
default: true,
},
- // Custom formatter for values.
- customValueFormatter: Function as PropType<
- (data: string, key: NodeDataType['key'], path: string, defaultFormatResult: string) => unknown
+ // Custom render for value.
+ renderNodeValue: Function as PropType<
+ (opt: { node: NodeDataType; defaultValue: string | JSX.Element }) => unknown
>,
// Define the selection method supported by the data level, which is not available by default.
selectableType: String as PropType<'multiple' | 'single' | ''>,
@@ -36,19 +37,18 @@ export const treeNodePropsPass = {
type: Boolean,
default: true,
},
- // Whether to trigger selection when clicking on the node.
- selectOnClickNode: {
+ showLineNumber: {
type: Boolean,
- default: true,
+ default: false,
},
- // Collapsed control.
- collapsedOnClickBrackets: {
+ // Whether to trigger selection when clicking on the node.
+ selectOnClickNode: {
type: Boolean,
default: true,
},
// When using the selectableType, define whether current path/content is enabled.
- pathSelectable: {
- type: Function as PropType<(path: string, content: string) => boolean>,
+ nodeSelectable: {
+ type: Function as PropType<(node: NodeDataType) => boolean>,
default: (): boolean => true,
},
// Highlight current node when selected.
@@ -56,6 +56,30 @@ export const treeNodePropsPass = {
type: Boolean,
default: true,
},
+ showIcon: {
+ type: Boolean,
+ default: false,
+ },
+ editable: {
+ type: Boolean,
+ default: false,
+ },
+ editableTrigger: {
+ type: String as PropType<'click' | 'dblclick'>,
+ default: 'click',
+ },
+ onNodeClick: {
+ type: Function as PropType<(node: NodeDataType) => void>,
+ },
+ onBracketsClick: {
+ type: Function as PropType<(collapsed: boolean, path: string) => void>,
+ },
+ onIconClick: {
+ type: Function as PropType<(collapsed: boolean, path: string) => void>,
+ },
+ onValueChange: {
+ type: Function as PropType<(value: boolean, path: string) => void>,
+ },
};
export default defineComponent({
@@ -72,21 +96,18 @@ export default defineComponent({
collapsed: Boolean,
// Whether the current node is checked(When using the selection function).
checked: Boolean,
- onTreeNodeClick: {
- type: Function as PropType<(node: NodeDataType) => void>,
- },
- onBracketsClick: {
- type: Function as PropType<(collapsed: boolean, path: string) => void>,
- },
+ style: Object as PropType,
onSelectedChange: {
type: Function as PropType<(node: NodeDataType) => void>,
},
},
+ emits: ['nodeClick', 'bracketsClick', 'iconClick', 'selectedChange', 'valueChange'],
+
setup(props, { emit }) {
- const dataType = computed(() => getDataType(props.node.content));
+ const dataType = computed(() => getDataType(props.node.content));
- const valueClass = computed(() => `vjs-value vjs-value__${dataType.value}`);
+ const valueClass = computed(() => `vjs-value vjs-value-${dataType.value}`);
const prettyKey = computed(() =>
props.showDoubleQuotes ? `"${props.node.key}"` : props.node.key,
@@ -98,127 +119,156 @@ export default defineComponent({
// Whether the current node supports the selected function.
const selectable = computed(
- () =>
- props.pathSelectable(props.node.path, props.node.content) &&
- (isMultiple.value || isSingle.value),
+ () => props.nodeSelectable(props.node) && (isMultiple.value || isSingle.value),
);
- const defaultFormatter = (data: string) => {
- let text = data + '';
- if (dataType.value === 'string') text = `"${text}"`;
+ const state = reactive({
+ editing: false,
+ });
+
+ const handleInputChange = (e: Event) => {
+ const source = (e.target as HTMLInputElement)?.value;
+ const value = stringToAutoType(source);
+ emit('valueChange', value, props.node.path);
+ };
+
+ const defaultValue = computed(() => {
+ const str = (props.node?.content ?? '') + '';
+ const text = dataType.value === 'string' ? `"${str}"` : str;
return text;
+ });
+
+ const renderValue = () => {
+ const render = props.renderNodeValue;
+
+ return render
+ ? render({ node: props.node, defaultValue: defaultValue.value })
+ : defaultValue.value;
};
- const customFormatter = props.customValueFormatter
- ? (data: string) =>
- props.customValueFormatter?.(
- data,
- props.node.key,
- props.node.path,
- defaultFormatter(data),
- )
- : null;
-
- const onBracketsClickHandler = () => {
- if (props.collapsedOnClickBrackets) {
- emit('brackets-click', !props.collapsed, props.node.path);
- }
+ const handleBracketsClick = () => {
+ emit('bracketsClick', !props.collapsed, props.node.path);
+ };
+
+ const handleIconClick = () => {
+ emit('iconClick', !props.collapsed, props.node.path);
};
- const onCheckedChange = () => {
- emit('selected-change', props.node);
+ const handleSelectedChange = () => {
+ emit('selectedChange', props.node);
};
- const onNodeClick = () => {
- emit('tree-node-click', props.node);
+ const handleNodeClick = () => {
+ emit('nodeClick', props.node);
if (selectable.value && props.selectOnClickNode) {
- emit('selected-change', props.node);
+ emit('selectedChange', props.node);
}
};
- const state = reactive({
- valueClass,
- prettyKey,
- isMultiple,
- selectable,
- });
-
- return {
- state,
- defaultFormatter,
- customFormatter,
- onBracketsClickHandler,
- onCheckedChange,
- onNodeClick,
+ const handleValueEdit = (e: MouseEvent) => {
+ if (!props.editable) return;
+ if (!state.editing) {
+ state.editing = true;
+ const handle = (innerE: MouseEvent) => {
+ if (
+ innerE.target !== e.target &&
+ (innerE.target as Element)?.parentElement !== e.target
+ ) {
+ state.editing = false;
+ document.removeEventListener('click', handle);
+ }
+ };
+ document.removeEventListener('click', handle);
+ document.addEventListener('click', handle);
+ }
};
- },
- render() {
- const {
- state,
- node,
- showSelectController,
- highlightSelectedNode,
- checked,
- showLength,
- collapsed,
- showLine,
- } = this;
-
- const {
- defaultFormatter,
- customFormatter,
- onNodeClick,
- onCheckedChange,
- onBracketsClickHandler,
- } = this;
-
- return (
-
- {showSelectController &&
- state.selectable &&
- node.type !== 'objectEnd' &&
- node.type !== 'arrayEnd' && (
-
- )}
-
- {Array.from(Array(node.level)).map((item, index) => (
-
- ))}
-
- {node.key &&
{`${state.prettyKey}: `} }
-
-
- {node.type !== 'content' ? (
-
- ) : customFormatter ? (
-
- ) : (
- {defaultFormatter(node.content)}
- )}
-
- {node.showComma && {','} }
-
- {showLength && collapsed && }
-
-
- );
+ return () => {
+ const { node } = props;
+
+ return (
+
+ {props.showLineNumber &&
{node.id + 1} }
+
+ {props.showSelectController &&
+ selectable.value &&
+ node.type !== 'objectEnd' &&
+ node.type !== 'arrayEnd' && (
+
+ )}
+
+
+ {Array.from(Array(node.level)).map((item, index) => (
+
+ ))}
+ {props.showIcon &&
}
+
+
+ {node.key &&
{`${prettyKey.value}: `} }
+
+
+ {node.type !== 'content' && node.content ? (
+
+ ) : (
+
+ {props.editable && state.editing ? (
+
+ ) : (
+ renderValue()
+ )}
+
+ )}
+
+ {node.showComma && {','} }
+
+ {props.showLength && props.collapsed && (
+
+ )}
+
+
+ );
+ };
},
});
diff --git a/src/components/TreeNode/styles.less b/src/components/TreeNode/styles.less
index 8360297..bb38f61 100644
--- a/src/components/TreeNode/styles.less
+++ b/src/components/TreeNode/styles.less
@@ -4,12 +4,18 @@
color: @color;
}
-.@{css-prefix}-tree__node {
+.@{css-prefix}-tree-node {
display: flex;
position: relative;
+ line-height: 20px;
- &.has-selector {
- padding-left: @selector-span;
+ &.has-carets {
+ padding-left: 15px;
+ }
+
+ &.has-selector,
+ &.has-carets.has-selector {
+ padding-left: 30px;
}
&.is-highlight,
@@ -17,8 +23,13 @@
background-color: @highlight-bg-color;
}
- .@{css-prefix}-tree__indent {
- flex: 0 0 1em;
+ .@{css-prefix}-indent {
+ display: flex;
+ position: relative;
+ }
+
+ .@{css-prefix}-indent-unit {
+ width: 1em;
&.has-line {
border-left: 1px dashed @border-color;
@@ -26,22 +37,32 @@
}
}
+.@{css-prefix}-node-index {
+ position: absolute;
+ right: 100%;
+ margin-right: 4px;
+}
+
.@{css-prefix}-comment {
color: @comment-color;
}
-.@{css-prefix}-value__null {
+.@{css-prefix}-value {
+ word-break: break-word;
+}
+
+.@{css-prefix}-value-null {
.gen-value-style(@color-null);
}
-.@{css-prefix}-value__number {
+.@{css-prefix}-value-number {
.gen-value-style(@color-number);
}
-.@{css-prefix}-value__boolean {
+.@{css-prefix}-value-boolean {
.gen-value-style(@color-boolean);
}
-.@{css-prefix}-value__string {
+.@{css-prefix}-value-string {
.gen-value-style(@color-string);
}
diff --git a/src/hooks/useError.ts b/src/hooks/useError.ts
new file mode 100644
index 0000000..228f517
--- /dev/null
+++ b/src/hooks/useError.ts
@@ -0,0 +1,21 @@
+import { watchEffect } from 'vue';
+
+type UseErrorOptions = {
+ emitListener: boolean;
+};
+
+export default function useError(message: string, { emitListener }: UseErrorOptions) {
+ const emit = () => {
+ throw new Error(`[VueJsonPretty] ${message}`);
+ };
+
+ watchEffect(() => {
+ if (emitListener) {
+ emit();
+ }
+ });
+
+ return {
+ emit,
+ };
+}
diff --git a/src/themes.less b/src/themes.less
index 1ed2bfa..e187a67 100644
--- a/src/themes.less
+++ b/src/themes.less
@@ -21,6 +21,3 @@
/* common border-color */
@border-color: #bfcbd9;
-
-/* 左侧可选区域占用空间 */
-@selector-span: 30px;
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 422340b..47ac26a 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -16,7 +16,7 @@ interface JSONFlattenOptions {
export type JSONDataType = string | number | boolean | unknown[] | Record | null;
export interface JSONFlattenReturnType extends JSONFlattenOptions {
- content: string;
+ content: string | number | null | boolean;
level: number;
path: string;
}
@@ -45,17 +45,16 @@ export function jsonFlatten(
const dataType = getDataType(data);
if (dataType === 'array') {
- const inner = (data as JSONDataType[])
- .map((item, idx, arr) =>
+ const inner = arrFlat(
+ (data as JSONDataType[]).map((item, idx, arr) =>
jsonFlatten(item, `${path}[${idx}]`, level + 1, {
index: idx,
showComma: idx !== arr.length - 1,
length,
type,
}),
- )
- // No flat, for compatibility.
- .reduce((acc, val) => acc.concat(val), []);
+ ),
+ ) as JSONFlattenReturnType[];
return [
jsonFlatten('[', path, level, {
showComma: false,
@@ -73,8 +72,8 @@ export function jsonFlatten(
);
} else if (dataType === 'object') {
const keys = Object.keys(data as Record);
- const inner = keys
- .map((objKey, idx, arr) =>
+ const inner = arrFlat(
+ keys.map((objKey, idx, arr) =>
jsonFlatten(
(data as Record)[objKey],
objKey.includes('.') ? `${path}["${objKey}"]` : `${path}.${objKey}`,
@@ -86,9 +85,8 @@ export function jsonFlatten(
type,
},
),
- )
- // No flat, for compatibility.
- .reduce((acc, val) => acc.concat(val), []);
+ ),
+ ) as JSONFlattenReturnType[];
return [
jsonFlatten('{', path, level, {
showComma: false,
@@ -103,24 +101,73 @@ export function jsonFlatten(
);
}
- const output = Object.entries({
- content: data,
- level,
- key,
- index,
- path,
- showComma,
- length,
- type,
- }).reduce((acc, [key, value]) => {
- if (value !== undefined) {
- return {
- ...acc,
- [key]: value,
- };
+ return [
+ {
+ content: data as JSONFlattenReturnType['content'],
+ level,
+ key,
+ index,
+ path,
+ showComma,
+ length,
+ type,
+ },
+ ];
+}
+
+export function arrFlat(arr: T): unknown[] {
+ if (typeof Array.prototype.flat === 'function') {
+ return arr.flat();
+ }
+ const stack = [...arr];
+ const result = [];
+ while (stack.length) {
+ const first = stack.shift();
+ if (Array.isArray(first)) {
+ stack.unshift(...first);
+ } else {
+ result.push(first);
}
- return acc;
- }, {}) as JSONFlattenReturnType;
+ }
+ return result;
+}
- return [output];
+export function cloneDeep(source: T, hash = new WeakMap()): T {
+ if (source === null) return source;
+ if (source instanceof Date) return new Date(source) as T;
+ if (source instanceof RegExp) return new RegExp(source) as T;
+ if (typeof source !== 'object') return source;
+ if (hash.get(source as Record))
+ return hash.get(source as Record);
+
+ if (Array.isArray(source)) {
+ const output = source.map(item => cloneDeep(item, hash));
+ hash.set(source, output);
+ return output as T;
+ }
+ const output = {} as T;
+ for (const key in source) {
+ output[key] = cloneDeep(source[key], hash);
+ }
+ hash.set(source as Record, output);
+ return output as T;
+}
+
+export function stringToAutoType(source: string): unknown {
+ let value;
+ if (source === 'null') value = null;
+ else if (source === 'undefined') value = undefined;
+ else if (source === 'true') value = true;
+ else if (source === 'false') value = false;
+ else if (
+ source[0] + source[source.length - 1] === '""' ||
+ source[0] + source[source.length - 1] === "''"
+ ) {
+ value = source.slice(1, -1);
+ } else if ((typeof Number(source) === 'number' && !isNaN(Number(source))) || source === 'NaN') {
+ value = Number(source);
+ } else {
+ value = source;
+ }
+ return value;
}