├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── bump-version-v1.yml │ ├── bump-version.yml │ ├── gh-pages.yml │ └── lint.yml ├── .gitignore ├── .postcssrc.js ├── .prettierrc ├── LICENSE ├── README.md ├── README.zh_CN.md ├── build ├── build.js ├── check-versions.js ├── dev-client.js ├── dev-server.js ├── utils.js ├── webpack.base.conf.js ├── webpack.dev.conf.js └── webpack.prod.conf.js ├── config ├── dev.env.js ├── index.js ├── prod.env.js └── test.env.js ├── cypress.json ├── cypress ├── fixtures │ └── example.json ├── integration │ ├── examples │ │ ├── actions.spec.js │ │ ├── aliasing.spec.js │ │ ├── assertions.spec.js │ │ ├── connectors.spec.js │ │ ├── cookies.spec.js │ │ ├── cypress_api.spec.js │ │ ├── files.spec.js │ │ ├── local_storage.spec.js │ │ ├── location.spec.js │ │ ├── misc.spec.js │ │ ├── navigation.spec.js │ │ ├── network_requests.spec.js │ │ ├── querying.spec.js │ │ ├── spies_stubs_clocks.spec.js │ │ ├── traversal.spec.js │ │ ├── utilities.spec.js │ │ ├── viewport.spec.js │ │ ├── waiting.spec.js │ │ └── window.spec.js │ └── test.spec.js ├── plugins │ └── index.js └── support │ ├── commands.js │ └── index.js ├── example ├── App.tsx ├── Basic.vue ├── Editable.vue ├── Icons.tsx ├── SelectControl.vue ├── Tsx.tsx ├── VirtualList.vue ├── index.html ├── main.js ├── styles.less └── useDarkMode.ts ├── package-lock.json ├── package.json ├── shims-vue.d.ts ├── src ├── components │ ├── Brackets │ │ ├── index.tsx │ │ └── styles.less │ ├── Carets │ │ ├── index.tsx │ │ └── styles.less │ ├── CheckController │ │ ├── index.tsx │ │ └── styles.less │ ├── Tree │ │ ├── index.tsx │ │ └── styles.less │ └── TreeNode │ │ ├── index.tsx │ │ └── styles.less ├── hooks │ ├── useClipboard.ts │ └── useError.ts ├── index.ts ├── themes.less └── utils │ └── index.ts ├── static ├── .gitkeep ├── logo.sketch ├── logo.svg └── screenshot.png ├── tsconfig.dts.json └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 10 versions", "not ie <= 11"] 7 | } 8 | }], 9 | ["@babel/preset-typescript", { 10 | "allExtensions": true, 11 | "isTSX": true 12 | }] 13 | ], 14 | "plugins": ["@babel/plugin-transform-runtime", "@vue/babel-plugin-jsx"], 15 | "env": { 16 | "test": { 17 | "presets": ["@babel/preset-env"], 18 | "plugins": ["istanbul"] 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/typescript/recommended', 10 | // '@vue/prettier', 11 | // '@vue/prettier/@typescript-eslint', 12 | ], 13 | parserOptions: { 14 | ecmaVersion: 2020, 15 | }, 16 | rules: { 17 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 18 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 19 | // 'prettier/prettier': [1, { arrowParens: 'avoid' }], 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /.github/workflows/bump-version-v1.yml: -------------------------------------------------------------------------------- 1 | name: Create Release for v1 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | bump-version: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout Code 11 | uses: actions/checkout@v3 12 | 13 | - name: Automated Version Bump 14 | uses: phips28/gh-action-bump-version@v9.1.0 15 | env: 16 | GITHUB_USER: ${{ secrets.CI_NAME }} 17 | GITHUB_EMAIL: ${{ secrets.CI_EMAIL }} 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | with: 20 | tag-prefix: 'v' 21 | 22 | - name: Set up Node.js 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: 16 26 | registry-url: https://registry.npmjs.org/ 27 | 28 | - name: Publish NPM 29 | run: | 30 | npm ci 31 | npm run build 32 | npm publish --tag v1-latest 33 | env: 34 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 35 | -------------------------------------------------------------------------------- /.github/workflows/bump-version.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | 8 | jobs: 9 | bump-version: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout Code 13 | uses: actions/checkout@v3 14 | 15 | - name: Automated Version Bump 16 | uses: phips28/gh-action-bump-version@v9.1.0 17 | env: 18 | GITHUB_USER: ${{ secrets.CI_NAME }} 19 | GITHUB_EMAIL: ${{ secrets.CI_EMAIL }} 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | with: 22 | tag-prefix: 'v' 23 | 24 | - name: Set up Node.js 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: 16 28 | registry-url: https://registry.npmjs.org/ 29 | 30 | - name: Publish NPM 31 | run: | 32 | npm ci 33 | npm run build 34 | npm publish 35 | env: 36 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 37 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Gh Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | 8 | jobs: 9 | build-and-deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout Code 13 | uses: actions/checkout@v3 14 | 15 | - name: Install 16 | run: npm install 17 | 18 | - name: Build 19 | run: npm run build:example 20 | 21 | - name: Deploy 22 | uses: peaceiris/actions-gh-pages@v3 23 | with: 24 | github_token: ${{ secrets.GITHUB_TOKEN }} 25 | publish_dir: ./example-dist 26 | user_name: ${{ secrets.CI_NAME }} 27 | user_email: ${{ secrets.CI_EMAIL }} 28 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | run-linters: 7 | name: Run Linters 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v3 13 | 14 | - name: Set up Node.js 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: 16 18 | 19 | - name: Lint 20 | run: | 21 | npm install 22 | npm run lint 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | example-dist/ 4 | dist/ 5 | lib/ 6 | esm/ 7 | types/ 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | test/e2e/reports 12 | selenium-debug.log 13 | 14 | # Editor directories and files 15 | .idea 16 | *.suo 17 | *.ntvs* 18 | *.njsproj 19 | *.sln 20 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | plugins: { 5 | // to edit target browsers: use "browserslist" field in package.json 6 | autoprefixer: {}, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "indent": 2, 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "printWidth": 100, 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "arrowParens": "avoid", 11 | "endOfLine": "lf", 12 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 |

8 | Vue Json Pretty 9 |

10 | 11 |
12 | 13 |

A Vue component for rendering JSON data as a tree structure.

14 |

Now it supports Vue3 at least. If you still use Vue2, see 1.x.

15 | 16 | [![Build Status](https://travis-ci.org/leezng/vue-json-pretty.svg?branch=master)](https://travis-ci.org/leezng/vue-json-pretty) 17 | [![npm package](https://img.shields.io/npm/v/vue-json-pretty.svg)](https://www.npmjs.org/package/vue-json-pretty) 18 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/leezng/vue-json-pretty/blob/master/LICENSE) 19 | [![Sizes](https://img.shields.io/bundlephobia/min/vue-json-pretty)](https://bundlephobia.com/result?p=vue-json-pretty) 20 | [![NPM downloads](http://img.shields.io/npm/dm/vue-json-pretty.svg?style=flat-square)](https://www.npmtrends.com/vue-json-pretty) 21 | [![Issues](https://img.shields.io/github/issues-raw/leezng/vue-json-pretty)](https://github.com/leezng/vue-json-pretty/issues) 22 | 23 |
24 | 25 | [![](./static/screenshot.png)](https://github.com/leezng/vue-json-pretty) 26 | 27 | English | [简体中文](./README.zh_CN.md) 28 | 29 | ## Features 30 | 31 | - As a JSON Formatter. 32 | - Written in TypeScript, support `d.ts`. 33 | - Support get item data from JSON. 34 | - Support big data. 35 | - Support editable. 36 | 37 | ## Environment Support 38 | 39 | - Modern browsers, Electron and Internet Explorer 11 (with polyfills) 40 | - Server-side Rendering 41 | 42 | | [IE / Edge](http://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | [Electron](http://godban.github.io/browsers-support-badges/)
Electron | 43 | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 44 | | IE11, Edge | last 10 versions | last 10 versions | last 10 versions | last 2 versions | 45 | 46 | ## Using NPM or Yarn 47 | 48 | ```bash 49 | $ npm install vue-json-pretty --save 50 | ``` 51 | 52 | ```bash 53 | $ yarn add vue-json-pretty 54 | ``` 55 | 56 | ## Use Vue2 57 | 58 | ```bash 59 | $ npm install vue-json-pretty@v1-latest --save 60 | ``` 61 | 62 | ## Usage 63 | 64 | The CSS file is included separately and needs to be imported manually. You can either import CSS globally in your app (if supported by your framework) or directly from the component. 65 | 66 | ```vue 67 | 72 | 73 | 83 | ``` 84 | 85 | ## Use Nuxt.js 86 | 87 | 1. In `plugins/vue-json-pretty.js` 88 | 89 | ``` 90 | import Vue from 'vue' 91 | import VueJsonPretty from 'vue-json-pretty' 92 | 93 | Vue.component("vue-json-pretty", VueJsonPretty) 94 | ``` 95 | 96 | 2. In `nuxt.config.js` 97 | 98 | ```js 99 | css: [ 100 | 'vue-json-pretty/lib/styles.css' 101 | ], 102 | plugins: [ 103 | '@/plugins/vue-json-pretty' 104 | ], 105 | ``` 106 | 107 | ## Props 108 | 109 | | Property | Description | Type | Default | 110 | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------- | ------- | 111 | | data(v-model) | JSON data, support v-model when use editable | JSON object | - | 112 | | collapsedNodeLength | Objects or arrays which length is greater than this threshold will be collapsed | number | - | 113 | | deep | Paths greater than this depth will be collapsed | number | - | 114 | | showLength | Show the length when collapsed | boolean | false | 115 | | showLine | Show the line | boolean | true | 116 | | showLineNumber | Show the line number | boolean | false | 117 | | showIcon | Show the icon | boolean | false | 118 | | showDoubleQuotes | Show doublequotes on key | boolean | true | 119 | | virtual | Use virtual scroll | boolean | false | 120 | | height | The height of list when using virtual | number | 400 | 121 | | itemHeight | The height of node when using virtual | number | 20 | 122 | | selectedValue(v-model) | Selected data path | string, array | - | 123 | | rootPath | Root data path | string | `root` | 124 | | nodeSelectable | Defines whether a node supports selection | (node) => boolean | - | 125 | | selectableType | Support path select, default none | `multiple` \| `single` | - | 126 | | showSelectController | Show the select controller | boolean | false | 127 | | selectOnClickNode | Trigger select when click node | boolean | true | 128 | | highlightSelectedNode | Support highlighting selected nodes | boolean | true | 129 | | collapsedOnClickBrackets | Support click brackets to collapse | boolean | true | 130 | | renderNodeKey | render node key, or use slot #renderNodeKey | ({ node, defaultKey }) => vNode | - | 131 | | renderNodeValue | render node value, or use slot #renderNodeValue | ({ node, defaultValue }) => vNode | - | 132 | | editable | Support editable | boolean | false | 133 | | editableTrigger | Trigger | `click` \| `dblclick` | `click` | 134 | | theme | Sets the theme of the component. Options are 'light' or 'dark', with dark mode enhancing visibility on dark backgrounds | `'light' \| 'dark'` | `light` | 135 | 136 | ## Events 137 | 138 | | Event Name | Description | Parameters | 139 | | -------------- | ---------------------------------------- | ------------------------------------ | 140 | | nodeClick | triggers when click node | (node: NodeData) | 141 | | bracketsClick | triggers when click brackets | (collapsed: boolean, node: NodeData) | 142 | | iconClick | triggers when click icon | (collapsed: boolean, node: NodeData) | 143 | | selectedChange | triggers when the selected value changed | (newVal, oldVal) | 144 | 145 | ## Slots 146 | 147 | | Slot Name | Description | Parameters | 148 | | --------------- | ----------------- | ---------------------- | 149 | | renderNodeKey | render node key | { node, defaultKey } | 150 | | renderNodeValue | render node value | { node, defaultValue } | 151 | 152 | ## Contributors 153 | 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /README.zh_CN.md: -------------------------------------------------------------------------------- 1 | 简体中文 | [English](./README.md) 2 | 3 | ## 特性 4 | 5 | - 一个 JSON 美化工具 6 | - 使用 Typescript,提供类型描述 `d.ts` 7 | - 支持字段层级数据提取 8 | - 支持大数据虚拟滚动 9 | - 支持编辑 10 | 11 | ## Props 12 | 13 | | 属性 | 说明 | 类型 | 默认值 | 14 | | ------------------------ | ------------------------------------------- | --------------------------------- | ------------- | 15 | | data(v-model) | 源数据,注意不是 `JSON` 字符串 | `JSON` 数据对象 | - | 16 | | collapsedNodeLength | 长度大于此阈值的对象或数组将被折叠 | number | Infinity | 17 | | deep | 深度,大于该深度的节点将被折叠 | number | Infinity | 18 | | showLength | 在数据折叠的时候展示长度 | boolean | false | 19 | | showLine | 展示标识线 | boolean | true | 20 | | showLineNumber | 展示行计数 | boolean | false | 21 | | showIcon | 展示图标 | boolean | false | 22 | | showDoubleQuotes | 展示 key 名的双引号 | boolean | true | 23 | | virtual | 使用虚拟滚动(大数据量) | boolean | false | 24 | | height | 使用虚拟滚动时,定义总高度 | number | 400 | 25 | | itemHeight | 使用虚拟滚动时,定义节点高度 | number | 20 | 26 | | selectedValue(v-model) | 双向绑定选中的数据路径 | string, array | string, array | 27 | | rootPath | 定义最顶层数据路径 | string | `root` | 28 | | nodeSelectable | 定义哪些数据节点可以被选择 | function(node) | - | 29 | | selectableType | 定义选择功能,默认无 | `multiple` \| `single` | - | 30 | | showSelectController | 展示选择器 | boolean | false | 31 | | selectOnClickNode | 支持点击节点的时候触发选择 | boolean | true | 32 | | highlightSelectedNode | 支持高亮已选择节点 | boolean | true | 33 | | collapsedOnClickBrackets | 支持点击括号折叠 | boolean | true | 34 | | renderNodeKey | 渲染节点键,也可使用 #renderNodeKey | ({ node, defaultKey }) => vNode | - | 35 | | renderNodeValue | 自定义渲染节点值,也可使用 #renderNodeValue | ({ node, defaultValue }) => vNode | - | 36 | | editable | 支持可编辑 | boolean | false | 37 | | editableTrigger | 触发编辑的时机 | `click` \| `dblclick` | `click` | 38 | | theme | 主题色 | `'light' \| 'dark'` | `light` | 39 | 40 | ## Events 41 | 42 | | 事件名称 | 说明 | 回调参数 | 43 | | -------------- | -------------------- | ------------------------------------ | 44 | | nodeClick | 点击节点时触发 | (node: NodeData) | 45 | | bracketsClick | 点击括号时触发 | (collapsed: boolean, node: NodeData) | 46 | | iconClick | 点击图标时触发 | (collapsed: boolean, node: NodeData) | 47 | | selectedChange | 选中值发生变化时触发 | (newVal, oldVal) | 48 | 49 | ## Slots 50 | 51 | | 插槽名 | 描述 | 参数 | 52 | | --------------- | ---------- | ---------------------- | 53 | | renderNodeKey | 渲染节点键 | { node, defaultKey } | 54 | | renderNodeValue | 渲染节点值 | { node, defaultValue } | 55 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')(); 2 | 3 | process.env.NODE_ENV = 'production'; 4 | 5 | const chalk = require('chalk'); 6 | const webpack = require('webpack'); 7 | const webpackConfig = require('./webpack.prod.conf'); 8 | 9 | const isEsm = process.env.ESM; 10 | const isExampleEnv = process.env.EXAMPLE_ENV; 11 | 12 | const successText = { 13 | main: 'Build main sources complete.', 14 | esm: 'Build esm sources complete.', 15 | example: 'Build example page complete.', 16 | }; 17 | 18 | webpack(webpackConfig, (err, stats) => { 19 | if (err) throw err; 20 | 21 | process.stdout.write( 22 | stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, 26 | chunks: false, 27 | chunkModules: false, 28 | }) + '\n\n', 29 | ); 30 | 31 | if (stats.hasErrors()) { 32 | console.log(chalk.red('Build failed with errors.\n')); 33 | process.exit(1); 34 | } 35 | 36 | const text = isExampleEnv ? successText.example : isEsm ? successText.esm : successText.main; 37 | console.log(chalk.cyan(`${text}\n`)); 38 | 39 | if (isExampleEnv) { 40 | console.log( 41 | chalk.yellow( 42 | 'Tip: built files are meant to be served over an HTTP server.\n' + 43 | "Opening index.html over file:// won't work.\n", 44 | ), 45 | ); 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /build/check-versions.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const semver = require('semver'); 3 | const packageConfig = require('../package.json'); 4 | const shell = require('shelljs'); 5 | function exec(cmd) { 6 | return require('child_process') 7 | .execSync(cmd) 8 | .toString() 9 | .trim(); 10 | } 11 | 12 | const versionRequirements = [ 13 | { 14 | name: 'node', 15 | currentVersion: semver.clean(process.version), 16 | versionRequirement: packageConfig.engines.node, 17 | }, 18 | ]; 19 | 20 | if (shell.which('npm')) { 21 | versionRequirements.push({ 22 | name: 'npm', 23 | currentVersion: exec('npm --version'), 24 | versionRequirement: packageConfig.engines.npm, 25 | }); 26 | } 27 | 28 | module.exports = function() { 29 | const warnings = []; 30 | for (let i = 0; i < versionRequirements.length; i++) { 31 | const mod = versionRequirements[i]; 32 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 33 | warnings.push( 34 | mod.name + 35 | ': ' + 36 | chalk.red(mod.currentVersion) + 37 | ' should be ' + 38 | chalk.green(mod.versionRequirement), 39 | ); 40 | } 41 | } 42 | 43 | if (warnings.length) { 44 | console.log(''); 45 | console.log(chalk.yellow('To use this template, you must update following to modules:')); 46 | console.log(); 47 | for (let i = 0; i < warnings.length; i++) { 48 | const warning = warnings[i]; 49 | console.log(' ' + warning); 50 | } 51 | console.log(); 52 | process.exit(1); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('eventsource-polyfill'); 3 | const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true'); 4 | 5 | hotClient.subscribe(function(event) { 6 | if (event.action === 'reload') { 7 | window.location.reload(); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /build/dev-server.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')(); 2 | 3 | const config = require('../config'); 4 | 5 | if (!process.env.NODE_ENV) { 6 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV); 7 | } 8 | 9 | const open = require('open'); 10 | const path = require('path'); 11 | const chalk = require('chalk'); 12 | const express = require('express'); 13 | const webpack = require('webpack'); 14 | const proxyMiddleware = require('http-proxy-middleware'); 15 | const webpackDevMiddleware = require('webpack-dev-middleware'); 16 | const webpackHotMiddleware = require('webpack-hot-middleware'); 17 | const webpackConfig = 18 | process.env.NODE_ENV === 'testing' || process.env.NODE_ENV === 'production' 19 | ? require('./webpack.prod.conf') 20 | : require('./webpack.dev.conf'); 21 | 22 | // default port where dev server listens for incoming traffic 23 | const port = process.env.PORT || config.dev.port; 24 | // automatically open browser, if not set will be false 25 | const autoOpenBrowser = !!config.dev.autoOpenBrowser; 26 | // Define HTTP proxies to your custom API backend 27 | // https://github.com/chimurai/http-proxy-middleware 28 | const proxyTable = config.dev.proxyTable; 29 | 30 | const app = express(); 31 | 32 | const compiler = webpack(webpackConfig); 33 | 34 | const wdmInstance = webpackDevMiddleware(compiler, { 35 | publicPath: webpackConfig.output.publicPath, 36 | quiet: true, 37 | }); 38 | 39 | const hotMiddleware = webpackHotMiddleware(compiler, { 40 | log: false, 41 | heartbeat: 2000, 42 | }); 43 | 44 | // force page reload when html-webpack-plugin template changes 45 | compiler.hooks.compilation.tap('html-webpack-plugin-after-emit', () => { 46 | hotMiddleware.publish({ 47 | action: 'reload', 48 | }); 49 | }); 50 | 51 | // proxy api requests 52 | Object.keys(proxyTable).forEach(context => { 53 | const options = proxyTable[context]; 54 | if (typeof options === 'string') { 55 | options = { target: options }; 56 | } 57 | app.use(proxyMiddleware(options.filter || context, options)); 58 | }); 59 | 60 | // handle fallback for HTML5 history API 61 | app.use(require('connect-history-api-fallback')()); 62 | 63 | // serve webpack bundle output 64 | app.use(wdmInstance); 65 | 66 | // enable hot-reload and state-preserving 67 | // compilation error display 68 | app.use(hotMiddleware); 69 | 70 | // serve pure static assets 71 | const staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory); 72 | app.use(staticPath, express.static('./static')); 73 | 74 | module.exports = new Promise(resolve => { 75 | console.log('> Starting dev server...'); 76 | const server = app.listen(port, err => { 77 | if (err) { 78 | console.error(err); 79 | } 80 | 81 | wdmInstance.waitUntilValid(() => { 82 | const uri = 'http://localhost:' + port; 83 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { 84 | open(uri); 85 | console.log('> Listening at ' + uri + '\n'); 86 | } 87 | }); 88 | }); 89 | 90 | resolve({ 91 | port, 92 | close: () => { 93 | server.close(); 94 | }, 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const config = require('../config'); 3 | 4 | exports.assetsPath = function(_path) { 5 | const assetsSubDirectory = 6 | process.env.NODE_ENV === 'production' 7 | ? config.build.assetsSubDirectory 8 | : config.dev.assetsSubDirectory; 9 | return path.posix.join(assetsSubDirectory, _path); 10 | }; 11 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { VueLoaderPlugin } = require('vue-loader'); 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | const ESLintPlugin = require('eslint-webpack-plugin'); 5 | const utils = require('./utils'); 6 | const config = require('../config'); 7 | 8 | function resolve(dir) { 9 | return path.join(__dirname, '..', dir); 10 | } 11 | 12 | const isProd = process.env.NODE_ENV === 'production'; 13 | 14 | const styleLoader = isProd ? MiniCssExtractPlugin.loader : 'vue-style-loader'; 15 | 16 | const cssLoader = { 17 | loader: 'css-loader', 18 | options: { 19 | sourceMap: !isProd, 20 | esModule: false, 21 | }, 22 | }; 23 | 24 | module.exports = { 25 | entry: { 26 | app: './example/main.js', 27 | }, 28 | output: { 29 | path: config.build.assetsRoot, 30 | filename: '[name].js', 31 | publicPath: isProd ? config.build.assetsPublicPath : config.dev.assetsPublicPath, 32 | }, 33 | resolve: { 34 | extensions: ['.js', '.jsx', '.ts', '.tsx', '.vue', '.json'], 35 | alias: { 36 | src: resolve('src'), 37 | }, 38 | }, 39 | module: { 40 | rules: [ 41 | { 42 | test: /\.vue$/, 43 | loader: 'vue-loader', 44 | }, 45 | { 46 | test: /\.[t|j]s[x]?$/, 47 | loader: 'babel-loader', 48 | include: [resolve('src'), resolve('example'), resolve('test')], 49 | }, 50 | { 51 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 52 | loader: 'url-loader', 53 | options: { 54 | limit: 10000, 55 | name: utils.assetsPath('img/[name].[hash:7].[ext]'), 56 | }, 57 | }, 58 | { 59 | test: /\.css$/, 60 | use: [styleLoader, cssLoader], 61 | }, 62 | { 63 | test: /\.less$/, 64 | use: [ 65 | styleLoader, 66 | cssLoader, 67 | { 68 | loader: 'less-loader', 69 | options: { 70 | lessOptions: { 71 | javascriptEnabled: true, 72 | }, 73 | }, 74 | }, 75 | ], 76 | }, 77 | ], 78 | }, 79 | plugins: [ 80 | new ESLintPlugin({ 81 | extensions: ['js', 'jsx', 'ts', 'tsx', 'vue'], 82 | emitError: true, 83 | emitWarning: true, 84 | }), 85 | new VueLoaderPlugin(), 86 | ], 87 | }; 88 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | const utils = require('./utils'); 2 | const webpack = require('webpack'); 3 | const config = require('../config'); 4 | const { merge } = require('webpack-merge'); 5 | const baseWebpackConfig = require('./webpack.base.conf'); 6 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin'); 8 | 9 | // add hot-reload related code to entry chunks 10 | Object.keys(baseWebpackConfig.entry).forEach(function(name) { 11 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]); 12 | }); 13 | 14 | module.exports = merge(baseWebpackConfig, { 15 | mode: 'development', 16 | devtool: 'eval-cheap-module-source-map', 17 | plugins: [ 18 | new webpack.DefinePlugin({ 19 | 'process.env': config.dev.env, 20 | }), 21 | new webpack.HotModuleReplacementPlugin(), 22 | new HtmlWebpackPlugin({ 23 | filename: 'index.html', 24 | template: 'example/index.html', 25 | inject: true, 26 | }), 27 | new FriendlyErrorsPlugin(), 28 | ], 29 | }); 30 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const utils = require('./utils'); 3 | const webpack = require('webpack'); 4 | const config = require('../config'); 5 | const { merge } = require('webpack-merge'); 6 | const baseWebpackConfig = require('./webpack.base.conf'); 7 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 8 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 9 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 10 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin'); 12 | 13 | const isEsm = process.env.ESM; 14 | const isExampleEnv = process.env.EXAMPLE_ENV; 15 | const distPath = isEsm ? '../esm' : '../lib'; 16 | 17 | const env = process.env.NODE_ENV === 'testing' ? require('../config/test.env') : config.build.env; 18 | 19 | const webpackConfig = merge(baseWebpackConfig, { 20 | mode: 'production', 21 | devtool: config.build.productionSourceMap ? 'source-map' : false, 22 | output: { 23 | path: config.build.assetsRoot, 24 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 25 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js'), 26 | }, 27 | plugins: [ 28 | new CleanWebpackPlugin(), 29 | new webpack.DefinePlugin({ 30 | 'process.env': env, 31 | }), 32 | ], 33 | }); 34 | 35 | if (!isExampleEnv) { 36 | webpackConfig.output = { 37 | path: path.resolve(__dirname, distPath), 38 | filename: `${distPath}/[name].js`, 39 | }; 40 | if (isEsm) { 41 | webpackConfig.entry = { 42 | 'vue-json-pretty': './src/index.ts', 43 | }; 44 | webpackConfig.experiments = { 45 | outputModule: true, 46 | }; 47 | webpackConfig.output.library = { type: 'module' }; 48 | webpackConfig.output.chunkFormat = 'module'; 49 | webpackConfig.target = 'es2019'; 50 | } else { 51 | webpackConfig.entry = { 52 | 'vue-json-pretty': './src/index.ts', 53 | }; 54 | webpackConfig.output.globalObject = 'this'; 55 | webpackConfig.output.library = 'VueJsonPretty'; 56 | webpackConfig.output.libraryTarget = 'umd'; 57 | } 58 | webpackConfig.externals = { 59 | vue: { 60 | root: 'Vue', 61 | commonjs2: 'vue', 62 | commonjs: 'vue', 63 | amd: 'vue', 64 | module: 'vue', 65 | }, 66 | }; 67 | webpackConfig.plugins.push( 68 | // extract css into its own file 69 | new MiniCssExtractPlugin({ 70 | filename: 'styles.css', 71 | }), 72 | // Compress extracted CSS. We are using this plugin so that possible 73 | // duplicated CSS from different components can be deduped. 74 | new OptimizeCSSPlugin({ 75 | cssProcessorOptions: { 76 | safe: true, 77 | }, 78 | }), 79 | ); 80 | } else { 81 | webpackConfig.optimization = { 82 | splitChunks: { 83 | cacheGroups: { 84 | vendors: { 85 | test: /[\\/]node_modules[\\/]/, 86 | name: 'vendors', 87 | chunks: 'all', 88 | enforce: true, 89 | priority: 0, 90 | }, 91 | commons: { 92 | chunks: 'initial', 93 | minChunks: 3, 94 | name: 'syncs', 95 | maxInitialRequests: 5, // The default limit is too small to showcase the effect 96 | minSize: 1024, // This is example is too small to create commons chunks 97 | priority: -3, 98 | reuseExistingChunk: true, 99 | }, 100 | }, 101 | }, 102 | }; 103 | webpackConfig.plugins.push( 104 | // extract css into its own file 105 | new MiniCssExtractPlugin({ 106 | filename: utils.assetsPath('css/[name].[hash].css'), 107 | }), 108 | // Compress extracted CSS. We are using this plugin so that possible 109 | // duplicated CSS from different components can be deduped. 110 | new OptimizeCSSPlugin({ 111 | cssProcessorOptions: { 112 | safe: true, 113 | }, 114 | }), 115 | // generate dist index.html with correct asset hash for caching. 116 | // you can customize output by editing /index.html 117 | // see https://github.com/ampedandwired/html-webpack-plugin 118 | new HtmlWebpackPlugin({ 119 | filename: process.env.NODE_ENV === 'testing' ? 'index.html' : config.build.index, 120 | template: 'example/index.html', 121 | inject: true, 122 | minify: { 123 | removeComments: true, 124 | collapseWhitespace: true, 125 | removeAttributeQuotes: true, 126 | }, 127 | }), 128 | // copy custom static assets 129 | new CopyWebpackPlugin({ 130 | patterns: [ 131 | { 132 | from: path.resolve(__dirname, '../static'), 133 | to: config.build.assetsSubDirectory, 134 | }, 135 | ], 136 | }), 137 | ); 138 | } 139 | 140 | if (config.build.productionGzip) { 141 | const CompressionWebpackPlugin = require('compression-webpack-plugin'); 142 | 143 | webpackConfig.plugins.push( 144 | new CompressionWebpackPlugin({ 145 | asset: '[path].gz[query]', 146 | algorithm: 'gzip', 147 | test: new RegExp('\\.(' + config.build.productionGzipExtensions.join('|') + ')$'), 148 | threshold: 10240, 149 | minRatio: 0.8, 150 | }), 151 | ); 152 | } 153 | 154 | if (config.build.bundleAnalyzerReport) { 155 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); 156 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()); 157 | } 158 | 159 | module.exports = webpackConfig; 160 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const prodEnv = require('./prod.env'); 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"', 6 | }); 7 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | build: { 6 | env: require('./prod.env'), 7 | index: path.resolve(__dirname, '../example-dist/index.html'), 8 | assetsRoot: path.resolve(__dirname, '../example-dist'), 9 | assetsSubDirectory: 'static', 10 | assetsPublicPath: './', 11 | productionSourceMap: false, 12 | // Gzip off by default as many popular static hosts such as 13 | // Surge or Netlify already gzip all static assets for you. 14 | // Before setting to `true`, make sure to: 15 | // npm install --save-dev compression-webpack-plugin 16 | productionGzip: false, 17 | productionGzipExtensions: ['js', 'css'], 18 | // Run the build command with an extra argument to 19 | // View the bundle analyzer report after build finishes: 20 | // `npm run build --report` 21 | // Set to `true` or `false` to always turn it on or off 22 | bundleAnalyzerReport: process.env.npm_config_report, 23 | }, 24 | dev: { 25 | env: require('./dev.env'), 26 | port: 8088, 27 | autoOpenBrowser: true, 28 | assetsSubDirectory: 'static', 29 | assetsPublicPath: '/', 30 | proxyTable: {}, 31 | // CSS Sourcemaps off by default because relative paths are "buggy" 32 | // with this option, according to the CSS-Loader README 33 | // (https://github.com/webpack/css-loader#sourcemaps) 34 | // In our experience, they generally work as expected, 35 | // just be aware of this issue when enabling this option. 36 | cssSourceMap: false, 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"', 3 | }; 4 | -------------------------------------------------------------------------------- /config/test.env.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const devEnv = require('./dev.env'); 3 | 4 | module.exports = merge(devEnv, { 5 | NODE_ENV: '"testing"', 6 | }); 7 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:8088", 3 | "parseSpecialCharSequences": false 4 | } 5 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /cypress/integration/examples/actions.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Actions', () => { 4 | beforeEach(() => { 5 | cy.visit('https://example.cypress.io/commands/actions') 6 | }) 7 | 8 | // https://on.cypress.io/interacting-with-elements 9 | 10 | it('.type() - type into a DOM element', () => { 11 | // https://on.cypress.io/type 12 | cy.get('.action-email') 13 | .type('fake@email.com').should('have.value', 'fake@email.com') 14 | 15 | // .type() with special character sequences 16 | .type('{leftarrow}{rightarrow}{uparrow}{downarrow}') 17 | .type('{del}{selectall}{backspace}') 18 | 19 | // .type() with key modifiers 20 | .type('{alt}{option}') //these are equivalent 21 | .type('{ctrl}{control}') //these are equivalent 22 | .type('{meta}{command}{cmd}') //these are equivalent 23 | .type('{shift}') 24 | 25 | // Delay each keypress by 0.1 sec 26 | .type('slow.typing@email.com', { delay: 100 }) 27 | .should('have.value', 'slow.typing@email.com') 28 | 29 | cy.get('.action-disabled') 30 | // Ignore error checking prior to type 31 | // like whether the input is visible or disabled 32 | .type('disabled error checking', { force: true }) 33 | .should('have.value', 'disabled error checking') 34 | }) 35 | 36 | it('.focus() - focus on a DOM element', () => { 37 | // https://on.cypress.io/focus 38 | cy.get('.action-focus').focus() 39 | .should('have.class', 'focus') 40 | .prev().should('have.attr', 'style', 'color: orange;') 41 | }) 42 | 43 | it('.blur() - blur off a DOM element', () => { 44 | // https://on.cypress.io/blur 45 | cy.get('.action-blur').type('About to blur').blur() 46 | .should('have.class', 'error') 47 | .prev().should('have.attr', 'style', 'color: red;') 48 | }) 49 | 50 | it('.clear() - clears an input or textarea element', () => { 51 | // https://on.cypress.io/clear 52 | cy.get('.action-clear').type('Clear this text') 53 | .should('have.value', 'Clear this text') 54 | .clear() 55 | .should('have.value', '') 56 | }) 57 | 58 | it('.submit() - submit a form', () => { 59 | // https://on.cypress.io/submit 60 | cy.get('.action-form') 61 | .find('[type="text"]').type('HALFOFF') 62 | 63 | cy.get('.action-form').submit() 64 | .next().should('contain', 'Your form has been submitted!') 65 | }) 66 | 67 | it('.click() - click on a DOM element', () => { 68 | // https://on.cypress.io/click 69 | cy.get('.action-btn').click() 70 | 71 | // You can click on 9 specific positions of an element: 72 | // ----------------------------------- 73 | // | topLeft top topRight | 74 | // | | 75 | // | | 76 | // | | 77 | // | left center right | 78 | // | | 79 | // | | 80 | // | | 81 | // | bottomLeft bottom bottomRight | 82 | // ----------------------------------- 83 | 84 | // clicking in the center of the element is the default 85 | cy.get('#action-canvas').click() 86 | 87 | cy.get('#action-canvas').click('topLeft') 88 | cy.get('#action-canvas').click('top') 89 | cy.get('#action-canvas').click('topRight') 90 | cy.get('#action-canvas').click('left') 91 | cy.get('#action-canvas').click('right') 92 | cy.get('#action-canvas').click('bottomLeft') 93 | cy.get('#action-canvas').click('bottom') 94 | cy.get('#action-canvas').click('bottomRight') 95 | 96 | // .click() accepts an x and y coordinate 97 | // that controls where the click occurs :) 98 | 99 | cy.get('#action-canvas') 100 | .click(80, 75) // click 80px on x coord and 75px on y coord 101 | .click(170, 75) 102 | .click(80, 165) 103 | .click(100, 185) 104 | .click(125, 190) 105 | .click(150, 185) 106 | .click(170, 165) 107 | 108 | // click multiple elements by passing multiple: true 109 | cy.get('.action-labels>.label').click({ multiple: true }) 110 | 111 | // Ignore error checking prior to clicking 112 | cy.get('.action-opacity>.btn').click({ force: true }) 113 | }) 114 | 115 | it('.dblclick() - double click on a DOM element', () => { 116 | // https://on.cypress.io/dblclick 117 | 118 | // Our app has a listener on 'dblclick' event in our 'scripts.js' 119 | // that hides the div and shows an input on double click 120 | cy.get('.action-div').dblclick().should('not.be.visible') 121 | cy.get('.action-input-hidden').should('be.visible') 122 | }) 123 | 124 | it('.rightclick() - right click on a DOM element', () => { 125 | // https://on.cypress.io/rightclick 126 | 127 | // Our app has a listener on 'contextmenu' event in our 'scripts.js' 128 | // that hides the div and shows an input on right click 129 | cy.get('.rightclick-action-div').rightclick().should('not.be.visible') 130 | cy.get('.rightclick-action-input-hidden').should('be.visible') 131 | }) 132 | 133 | it('.check() - check a checkbox or radio element', () => { 134 | // https://on.cypress.io/check 135 | 136 | // By default, .check() will check all 137 | // matching checkbox or radio elements in succession, one after another 138 | cy.get('.action-checkboxes [type="checkbox"]').not('[disabled]') 139 | .check().should('be.checked') 140 | 141 | cy.get('.action-radios [type="radio"]').not('[disabled]') 142 | .check().should('be.checked') 143 | 144 | // .check() accepts a value argument 145 | cy.get('.action-radios [type="radio"]') 146 | .check('radio1').should('be.checked') 147 | 148 | // .check() accepts an array of values 149 | cy.get('.action-multiple-checkboxes [type="checkbox"]') 150 | .check(['checkbox1', 'checkbox2']).should('be.checked') 151 | 152 | // Ignore error checking prior to checking 153 | cy.get('.action-checkboxes [disabled]') 154 | .check({ force: true }).should('be.checked') 155 | 156 | cy.get('.action-radios [type="radio"]') 157 | .check('radio3', { force: true }).should('be.checked') 158 | }) 159 | 160 | it('.uncheck() - uncheck a checkbox element', () => { 161 | // https://on.cypress.io/uncheck 162 | 163 | // By default, .uncheck() will uncheck all matching 164 | // checkbox elements in succession, one after another 165 | cy.get('.action-check [type="checkbox"]') 166 | .not('[disabled]') 167 | .uncheck().should('not.be.checked') 168 | 169 | // .uncheck() accepts a value argument 170 | cy.get('.action-check [type="checkbox"]') 171 | .check('checkbox1') 172 | .uncheck('checkbox1').should('not.be.checked') 173 | 174 | // .uncheck() accepts an array of values 175 | cy.get('.action-check [type="checkbox"]') 176 | .check(['checkbox1', 'checkbox3']) 177 | .uncheck(['checkbox1', 'checkbox3']).should('not.be.checked') 178 | 179 | // Ignore error checking prior to unchecking 180 | cy.get('.action-check [disabled]') 181 | .uncheck({ force: true }).should('not.be.checked') 182 | }) 183 | 184 | it('.select() - select an option in a $emit('change', model)} 44 | /> 45 | 46 | ); 47 | }, 48 | }); 49 | -------------------------------------------------------------------------------- /src/components/CheckController/styles.less: -------------------------------------------------------------------------------- 1 | @import '~src/themes.less'; 2 | 3 | .@{css-prefix}-check-controller { 4 | position: absolute; 5 | left: 0; 6 | 7 | &.is-checked .@{css-prefix}-check-controller-inner { 8 | background-color: @color-primary; 9 | border-color: darken(@color-primary, 10%); 10 | 11 | &.is-checkbox:after { 12 | transform: rotate(45deg) scaleY(1); 13 | } 14 | 15 | &.is-radio:after { 16 | transform: translate(-50%, -50%) scale(1); 17 | } 18 | } 19 | 20 | .@{css-prefix}-check-controller-inner { 21 | display: inline-block; 22 | position: relative; 23 | border: 1px solid @border-color; 24 | border-radius: 2px; 25 | vertical-align: middle; 26 | box-sizing: border-box; 27 | width: 16px; 28 | height: 16px; 29 | background-color: #fff; 30 | z-index: 1; 31 | cursor: pointer; 32 | transition: border-color 0.25s cubic-bezier(0.71, -0.46, 0.29, 1.46), 33 | background-color 0.25s cubic-bezier(0.71, -0.46, 0.29, 1.46); 34 | 35 | &:after { 36 | box-sizing: content-box; 37 | content: ''; 38 | border: 2px solid #fff; 39 | border-left: 0; 40 | border-top: 0; 41 | height: 8px; 42 | left: 4px; 43 | position: absolute; 44 | top: 1px; 45 | transform: rotate(45deg) scaleY(0); 46 | width: 4px; 47 | transition: transform 0.15s cubic-bezier(0.71, -0.46, 0.88, 0.6) 0.05s; 48 | transform-origin: center; 49 | } 50 | 51 | &.is-radio { 52 | border-radius: 100%; 53 | 54 | &:after { 55 | border-radius: 100%; 56 | height: 4px; 57 | background-color: #fff; 58 | left: 50%; 59 | top: 50%; 60 | } 61 | } 62 | } 63 | 64 | .@{css-prefix}-check-controller-original { 65 | opacity: 0; 66 | outline: none; 67 | position: absolute; 68 | z-index: -1; 69 | top: 0; 70 | left: 0; 71 | right: 0; 72 | bottom: 0; 73 | margin: 0; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/components/Tree/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | defineComponent, 3 | reactive, 4 | computed, 5 | watchEffect, 6 | watch, 7 | ref, 8 | PropType, 9 | CSSProperties, 10 | } from 'vue'; 11 | import TreeNode, { treeNodePropsPass, NodeDataType } from 'src/components/TreeNode'; 12 | import { emitError, jsonFlatten, JSONDataType, cloneDeep } from 'src/utils'; 13 | import './styles.less'; 14 | 15 | export default defineComponent({ 16 | name: 'Tree', 17 | 18 | props: { 19 | ...treeNodePropsPass, 20 | // JSONLike data. 21 | data: { 22 | type: [String, Number, Boolean, Array, Object] as PropType, 23 | default: null, 24 | }, 25 | collapsedNodeLength: { 26 | type: Number, 27 | default: Infinity, 28 | }, 29 | // Define the depth of the tree, nodes greater than this depth will not be expanded. 30 | deep: { 31 | type: Number, 32 | default: Infinity, 33 | }, 34 | pathCollapsible: { 35 | type: Function as PropType<(node: NodeDataType) => boolean>, 36 | default: (): boolean => false, 37 | }, 38 | // Data root path. 39 | rootPath: { 40 | type: String, 41 | default: 'root', 42 | }, 43 | // Whether to use virtual scroll, usually applied to big data. 44 | virtual: { 45 | type: Boolean, 46 | default: false, 47 | }, 48 | // When using virtual scroll, set the height of tree. 49 | height: { 50 | type: Number, 51 | default: 400, 52 | }, 53 | // When using virtual scroll, define the height of each row. 54 | itemHeight: { 55 | type: Number, 56 | default: 20, 57 | }, 58 | // When there is a selection function, define the selected path. 59 | // For multiple selections, it is an array ['root.a','root.b'], for single selection, it is a string of 'root.a'. 60 | selectedValue: { 61 | type: [String, Array] as PropType, 62 | default: () => '', 63 | }, 64 | // Collapsed control. 65 | collapsedOnClickBrackets: { 66 | type: Boolean, 67 | default: true, 68 | }, 69 | style: Object as PropType, 70 | onSelectedChange: { 71 | type: Function as PropType<(newVal: string | string[], oldVal: string | string[]) => void>, 72 | }, 73 | theme: { 74 | type: String as PropType<'light' | 'dark'>, 75 | default: 'light', 76 | }, 77 | }, 78 | 79 | slots: ['renderNodeKey', 'renderNodeValue'], 80 | 81 | emits: [ 82 | 'nodeClick', 83 | 'nodeMouseover', 84 | 'bracketsClick', 85 | 'iconClick', 86 | 'selectedChange', 87 | 'update:selectedValue', 88 | 'update:data', 89 | ], 90 | 91 | setup(props, { emit, slots }) { 92 | const treeRef = ref(); 93 | 94 | const originFlatData = computed(() => jsonFlatten(props.data, props.rootPath)); 95 | 96 | const initHiddenPaths = (deep: number, collapsedNodeLength: number) => { 97 | return originFlatData.value.reduce((acc, item) => { 98 | const doCollapse = item.level >= deep || item.length >= collapsedNodeLength; 99 | const pathComparison = props.pathCollapsible?.(item as NodeDataType); 100 | if ( 101 | (item.type === 'objectStart' || item.type === 'arrayStart') && 102 | (doCollapse || pathComparison) 103 | ) { 104 | return { 105 | ...acc, 106 | [item.path]: 1, 107 | }; 108 | } 109 | return acc; 110 | }, {}) as Record; 111 | }; 112 | 113 | const state = reactive({ 114 | translateY: 0, 115 | visibleData: null as NodeDataType[] | null, 116 | hiddenPaths: initHiddenPaths(props.deep, props.collapsedNodeLength), 117 | }); 118 | 119 | const flatData = computed(() => { 120 | let startHiddenItem: null | NodeDataType = null; 121 | const data = []; 122 | const length = originFlatData.value.length; 123 | for (let i = 0; i < length; i++) { 124 | const cur = originFlatData.value[i]; 125 | const item = { 126 | ...cur, 127 | id: i, 128 | }; 129 | const isHidden = state.hiddenPaths[item.path]; 130 | if (startHiddenItem && startHiddenItem.path === item.path) { 131 | const isObject = startHiddenItem.type === 'objectStart'; 132 | const mergeItem = { 133 | ...item, 134 | ...startHiddenItem, 135 | showComma: item.showComma, 136 | content: isObject ? '{...}' : '[...]', 137 | type: isObject ? 'objectCollapsed' : 'arrayCollapsed', 138 | } as NodeDataType; 139 | startHiddenItem = null; 140 | data.push(mergeItem); 141 | } else if (isHidden && !startHiddenItem) { 142 | startHiddenItem = item; 143 | continue; 144 | } else { 145 | if (startHiddenItem) continue; 146 | else data.push(item); 147 | } 148 | } 149 | return data; 150 | }); 151 | 152 | const selectedPaths = computed(() => { 153 | const value = props.selectedValue; 154 | if (value && props.selectableType === 'multiple' && Array.isArray(value)) { 155 | return value; 156 | } 157 | return [value]; 158 | }); 159 | 160 | const propsErrorMessage = computed(() => { 161 | const error = props.selectableType && !props.selectOnClickNode && !props.showSelectController; 162 | return error 163 | ? 'When selectableType is not null, selectOnClickNode and showSelectController cannot be false at the same time, because this will cause the selection to fail.' 164 | : ''; 165 | }); 166 | 167 | const updateVisibleData = () => { 168 | const flatDataValue = flatData.value; 169 | if (props.virtual) { 170 | const visibleCount = props.height / props.itemHeight; 171 | const scrollTop = treeRef.value?.scrollTop || 0; 172 | const scrollCount = Math.floor(scrollTop / props.itemHeight); 173 | let start = 174 | scrollCount < 0 175 | ? 0 176 | : scrollCount + visibleCount > flatDataValue.length 177 | ? flatDataValue.length - visibleCount 178 | : scrollCount; 179 | if (start < 0) { 180 | start = 0; 181 | } 182 | const end = start + visibleCount; 183 | state.translateY = start * props.itemHeight; 184 | state.visibleData = flatDataValue.filter((item, index) => index >= start && index < end); 185 | } else { 186 | state.visibleData = flatDataValue; 187 | } 188 | }; 189 | 190 | const handleTreeScroll = () => { 191 | updateVisibleData(); 192 | }; 193 | 194 | const handleSelectedChange = ({ path }: NodeDataType) => { 195 | const type = props.selectableType; 196 | if (type === 'multiple') { 197 | const index = selectedPaths.value.findIndex(item => item === path); 198 | const newVal = [...selectedPaths.value]; 199 | if (index !== -1) { 200 | newVal.splice(index, 1); 201 | } else { 202 | newVal.push(path); 203 | } 204 | emit('update:selectedValue', newVal); 205 | emit('selectedChange', newVal, [...selectedPaths.value]); 206 | } else if (type === 'single') { 207 | if (selectedPaths.value[0] !== path) { 208 | const [oldVal] = selectedPaths.value; 209 | const newVal = path; 210 | emit('update:selectedValue', newVal); 211 | emit('selectedChange', newVal, oldVal); 212 | } 213 | } 214 | }; 215 | 216 | const handleNodeClick = (node: NodeDataType) => { 217 | emit('nodeClick', node); 218 | }; 219 | 220 | const handleNodeMouseover = (node: NodeDataType) => { 221 | emit('nodeMouseover', node); 222 | }; 223 | 224 | const updateCollapsedPaths = (collapsed: boolean, path: string) => { 225 | if (collapsed) { 226 | state.hiddenPaths = { 227 | ...state.hiddenPaths, 228 | [path]: 1, 229 | }; 230 | } else { 231 | const newPaths = { ...state.hiddenPaths }; 232 | delete newPaths[path]; 233 | state.hiddenPaths = newPaths; 234 | } 235 | }; 236 | 237 | const handleBracketsClick = (collapsed: boolean, node: NodeDataType) => { 238 | if (props.collapsedOnClickBrackets) { 239 | updateCollapsedPaths(collapsed, node.path); 240 | } 241 | emit('bracketsClick', collapsed, node); 242 | }; 243 | 244 | const handleIconClick = (collapsed: boolean, node: NodeDataType) => { 245 | updateCollapsedPaths(collapsed, node.path); 246 | emit('iconClick', collapsed, node); 247 | }; 248 | 249 | const handleValueChange = (value: unknown, path: string) => { 250 | const newData = cloneDeep(props.data); 251 | const rootPath = props.rootPath; 252 | new Function('data', 'val', `data${path.slice(rootPath.length)}=val`)(newData, value); 253 | emit('update:data', newData); 254 | }; 255 | 256 | watchEffect(() => { 257 | if (propsErrorMessage.value) { 258 | emitError(propsErrorMessage.value); 259 | } 260 | }); 261 | 262 | watchEffect(() => { 263 | if (flatData.value) { 264 | updateVisibleData(); 265 | } 266 | }); 267 | 268 | watch( 269 | () => props.deep, 270 | val => { 271 | if (val) state.hiddenPaths = initHiddenPaths(val, props.collapsedNodeLength); 272 | }, 273 | ); 274 | 275 | watch( 276 | () => props.collapsedNodeLength, 277 | val => { 278 | if (val) state.hiddenPaths = initHiddenPaths(props.deep, val); 279 | }, 280 | ); 281 | 282 | return () => { 283 | const renderNodeKey = props.renderNodeKey ?? slots.renderNodeKey; 284 | const renderNodeValue = props.renderNodeValue ?? slots.renderNodeValue; 285 | 286 | const nodeContent = 287 | state.visibleData && 288 | state.visibleData.map(item => ( 289 | 322 | )); 323 | 324 | return ( 325 |
342 | {props.virtual ? ( 343 |
344 |
348 |
352 | {nodeContent} 353 |
354 |
355 |
356 | ) : ( 357 | nodeContent 358 | )} 359 |
360 | ); 361 | }; 362 | }, 363 | }); 364 | -------------------------------------------------------------------------------- /src/components/Tree/styles.less: -------------------------------------------------------------------------------- 1 | @import '~src/themes.less'; 2 | 3 | .@{css-prefix}-tree { 4 | font-family: 'Monaco', 'Menlo', 'Consolas', 'Bitstream Vera Sans Mono', monospace; 5 | font-size: 14px; 6 | text-align: left; 7 | 8 | &.is-virtual { 9 | overflow: auto; 10 | 11 | .@{css-prefix}-tree-node { 12 | white-space: nowrap; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/TreeNode/index.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, reactive, computed, PropType, CSSProperties } from 'vue'; 2 | import Brackets from 'src/components/Brackets'; 3 | import CheckController from 'src/components/CheckController'; 4 | import Carets from 'src/components/Carets'; 5 | import { getDataType, JSONFlattenReturnType, stringToAutoType } from 'src/utils'; 6 | import './styles.less'; 7 | 8 | export interface NodeDataType extends JSONFlattenReturnType { 9 | id: number; 10 | } 11 | 12 | // The props here will be exposed to the user through the topmost component. 13 | export const treeNodePropsPass = { 14 | // Whether to display the length of (array|object). 15 | showLength: { 16 | type: Boolean, 17 | default: false, 18 | }, 19 | // Whether the key name uses double quotes. 20 | showDoubleQuotes: { 21 | type: Boolean, 22 | default: true, 23 | }, 24 | // Custom render for key. 25 | renderNodeKey: Function as PropType< 26 | (opt: { node: NodeDataType; defaultKey: string | JSX.Element }) => unknown 27 | >, 28 | // Custom render for value. 29 | renderNodeValue: Function as PropType< 30 | (opt: { node: NodeDataType; defaultValue: string | JSX.Element }) => unknown 31 | >, 32 | // Define the selection method supported by the data level, which is not available by default. 33 | selectableType: String as PropType<'multiple' | 'single' | ''>, 34 | // Whether to display the selection control. 35 | showSelectController: { 36 | type: Boolean, 37 | default: false, 38 | }, 39 | // Whether to display the data level connection. 40 | showLine: { 41 | type: Boolean, 42 | default: true, 43 | }, 44 | showLineNumber: { 45 | type: Boolean, 46 | default: false, 47 | }, 48 | // Whether to trigger selection when clicking on the node. 49 | selectOnClickNode: { 50 | type: Boolean, 51 | default: true, 52 | }, 53 | // When using the selectableType, define whether current path/content is enabled. 54 | nodeSelectable: { 55 | type: Function as PropType<(node: NodeDataType) => boolean>, 56 | default: (): boolean => true, 57 | }, 58 | // Highlight current node when selected. 59 | highlightSelectedNode: { 60 | type: Boolean, 61 | default: true, 62 | }, 63 | showIcon: { 64 | type: Boolean, 65 | default: false, 66 | }, 67 | theme: { 68 | type: String as PropType<'light' | 'dark'>, 69 | default: 'light', 70 | }, 71 | showKeyValueSpace: { 72 | type: Boolean, 73 | default: true, 74 | }, 75 | editable: { 76 | type: Boolean, 77 | default: false, 78 | }, 79 | editableTrigger: { 80 | type: String as PropType<'click' | 'dblclick'>, 81 | default: 'click', 82 | }, 83 | onNodeClick: { 84 | type: Function as PropType<(node: NodeDataType) => void>, 85 | }, 86 | onNodeMouseover: { 87 | type: Function as PropType<(node: NodeDataType) => void>, 88 | }, 89 | onBracketsClick: { 90 | type: Function as PropType<(collapsed: boolean, node: NodeDataType) => void>, 91 | }, 92 | onIconClick: { 93 | type: Function as PropType<(collapsed: boolean, node: NodeDataType) => void>, 94 | }, 95 | onValueChange: { 96 | type: Function as PropType<(value: boolean, path: string) => void>, 97 | }, 98 | }; 99 | 100 | export default defineComponent({ 101 | name: 'TreeNode', 102 | 103 | props: { 104 | ...treeNodePropsPass, 105 | // Current node data. 106 | node: { 107 | type: Object as PropType, 108 | required: true, 109 | }, 110 | // Whether the current node is collapsed. 111 | collapsed: Boolean, 112 | // Whether the current node is checked(When using the selection function). 113 | checked: Boolean, 114 | style: Object as PropType, 115 | onSelectedChange: { 116 | type: Function as PropType<(node: NodeDataType) => void>, 117 | }, 118 | }, 119 | 120 | emits: ['nodeClick', 'nodeMouseover', 'bracketsClick', 'iconClick', 'selectedChange', 'valueChange'], 121 | 122 | setup(props, { emit }) { 123 | const dataType = computed(() => getDataType(props.node.content)); 124 | 125 | const valueClass = computed(() => `vjs-value vjs-value-${dataType.value}`); 126 | 127 | const prettyKey = computed(() => 128 | props.showDoubleQuotes ? `"${props.node.key}"` : props.node.key, 129 | ); 130 | 131 | const renderKey = () => { 132 | const render = props.renderNodeKey; 133 | 134 | return render 135 | ? render({ node: props.node, defaultKey: prettyKey.value || '' }) 136 | : prettyKey.value; 137 | }; 138 | 139 | const isMultiple = computed(() => props.selectableType === 'multiple'); 140 | 141 | const isSingle = computed(() => props.selectableType === 'single'); 142 | 143 | // Whether the current node supports the selected function. 144 | const selectable = computed( 145 | () => props.nodeSelectable(props.node) && (isMultiple.value || isSingle.value), 146 | ); 147 | 148 | const state = reactive({ 149 | editing: false, 150 | }); 151 | 152 | const handleInputChange = (e: Event) => { 153 | const source = (e.target as HTMLInputElement)?.value; 154 | const value = stringToAutoType(source); 155 | emit('valueChange', value, props.node.path); 156 | }; 157 | 158 | const defaultValue = computed(() => { 159 | let value = props.node?.content; 160 | if (value === null) { 161 | value = 'null'; 162 | } else if (value === undefined) { 163 | value = 'undefined'; 164 | } 165 | return dataType.value === 'string' ? `"${value}"` : value + ''; 166 | }); 167 | 168 | const renderValue = () => { 169 | const render = props.renderNodeValue; 170 | 171 | return render 172 | ? render({ node: props.node, defaultValue: defaultValue.value }) 173 | : defaultValue.value; 174 | }; 175 | 176 | const handleBracketsClick = () => { 177 | emit('bracketsClick', !props.collapsed, props.node); 178 | }; 179 | 180 | const handleIconClick = () => { 181 | emit('iconClick', !props.collapsed, props.node); 182 | }; 183 | 184 | const handleSelectedChange = () => { 185 | emit('selectedChange', props.node); 186 | }; 187 | 188 | const handleNodeClick = () => { 189 | emit('nodeClick', props.node); 190 | if (selectable.value && props.selectOnClickNode) { 191 | emit('selectedChange', props.node); 192 | } 193 | }; 194 | 195 | const handleNodeMouseover = () => { 196 | emit('nodeMouseover', props.node); 197 | }; 198 | 199 | const handleValueEdit = (e: MouseEvent) => { 200 | if (!props.editable) return; 201 | if (!state.editing) { 202 | state.editing = true; 203 | const handle = (innerE: MouseEvent) => { 204 | if ( 205 | innerE.target !== e.target && 206 | (innerE.target as Element)?.parentElement !== e.target 207 | ) { 208 | state.editing = false; 209 | document.removeEventListener('click', handle); 210 | } 211 | }; 212 | document.removeEventListener('click', handle); 213 | document.addEventListener('click', handle); 214 | } 215 | }; 216 | 217 | return () => { 218 | const { node } = props; 219 | 220 | return ( 221 |
233 | {props.showLineNumber && {node.id + 1}} 234 | 235 | {props.showSelectController && 236 | selectable.value && 237 | node.type !== 'objectEnd' && 238 | node.type !== 'arrayEnd' && ( 239 | 244 | )} 245 | 246 |
247 | {Array.from(Array(node.level)).map((item, index) => ( 248 |
255 | ))} 256 | {props.showIcon && } 257 |
258 | 259 | {node.key && ( 260 | 261 | {renderKey()} 262 | {`:${props.showKeyValueSpace ? ' ' : ''}`} 263 | 264 | )} 265 | 266 | 267 | {node.type !== 'content' && node.content ? ( 268 | 269 | ) : ( 270 | 283 | {props.editable && state.editing ? ( 284 | 296 | ) : ( 297 | renderValue() 298 | )} 299 | 300 | )} 301 | 302 | {node.showComma && {','}} 303 | 304 | {props.showLength && props.collapsed && ( 305 | // {node.length} items 306 | )} 307 | 308 |
309 | ); 310 | }; 311 | }, 312 | }); 313 | -------------------------------------------------------------------------------- /src/components/TreeNode/styles.less: -------------------------------------------------------------------------------- 1 | @import '~src/themes.less'; 2 | 3 | .gen-value-style(@color) { 4 | color: @color; 5 | } 6 | 7 | .@{css-prefix}-tree-node { 8 | display: flex; 9 | position: relative; 10 | line-height: 20px; 11 | 12 | &.has-carets { 13 | padding-left: 15px; 14 | } 15 | 16 | &.has-selector, 17 | &.has-carets.has-selector { 18 | padding-left: 30px; 19 | } 20 | 21 | &.is-highlight, 22 | &:hover { 23 | background-color: @highlight-bg-color; 24 | } 25 | 26 | .@{css-prefix}-indent { 27 | display: flex; 28 | position: relative; 29 | } 30 | 31 | .@{css-prefix}-indent-unit { 32 | width: 1em; 33 | 34 | &.has-line { 35 | border-left: 1px dashed @border-color; 36 | } 37 | } 38 | 39 | &.dark { 40 | &.is-highlight, 41 | &:hover { 42 | background-color: @highlight-bg-color-dark; 43 | } 44 | } 45 | } 46 | 47 | .@{css-prefix}-node-index { 48 | position: absolute; 49 | right: 100%; 50 | margin-right: 4px; 51 | user-select: none; 52 | } 53 | 54 | .@{css-prefix}-colon { 55 | white-space: pre; 56 | } 57 | 58 | .@{css-prefix}-comment { 59 | color: @comment-color; 60 | } 61 | 62 | .@{css-prefix}-value { 63 | word-break: break-word; 64 | } 65 | 66 | .@{css-prefix}-value-null { 67 | .gen-value-style(@color-null); 68 | } 69 | 70 | .@{css-prefix}-value-undefined { 71 | .gen-value-style(@color-undefined); 72 | } 73 | 74 | .@{css-prefix}-value-number { 75 | .gen-value-style(@color-number); 76 | } 77 | 78 | .@{css-prefix}-value-boolean { 79 | .gen-value-style(@color-boolean); 80 | } 81 | 82 | .@{css-prefix}-value-string { 83 | .gen-value-style(@color-string); 84 | } 85 | -------------------------------------------------------------------------------- /src/hooks/useClipboard.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue'; 2 | 3 | export type UseClipboardOptions = { 4 | source: string; 5 | }; 6 | 7 | export function useClipboard({ source }: UseClipboardOptions) { 8 | const copied = ref(false); 9 | 10 | const copy = async () => { 11 | try { 12 | await navigator.clipboard.writeText(source); 13 | copied.value = true; 14 | setTimeout(() => { 15 | copied.value = false; 16 | }, 300); 17 | } catch (err) { 18 | console.error('[vue-json-pretty] Copy failed: ', err); 19 | } 20 | }; 21 | 22 | return { 23 | copy, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /src/hooks/useError.ts: -------------------------------------------------------------------------------- 1 | import { watchEffect } from 'vue'; 2 | 3 | type UseErrorOptions = { 4 | emitListener: boolean; 5 | }; 6 | 7 | export function useError(message: string, { emitListener }: UseErrorOptions) { 8 | const emit = () => { 9 | throw new Error(`[VueJsonPretty] ${message}`); 10 | }; 11 | 12 | watchEffect(() => { 13 | if (emitListener) { 14 | emit(); 15 | } 16 | }); 17 | 18 | return { 19 | emit, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from 'vue'; 2 | import Tree from './components/Tree'; 3 | 4 | export default Tree as typeof Tree & Plugin; 5 | -------------------------------------------------------------------------------- /src/themes.less: -------------------------------------------------------------------------------- 1 | /* css scope */ 2 | @css-prefix: vjs; 3 | 4 | /* theme color */ 5 | @color-primary: #1890ff; 6 | @color-info: #1d8ce0; 7 | @color-error: #ff4949; 8 | @color-success: #13ce66; 9 | @color-nil: #d55fde; 10 | 11 | /* field values color */ 12 | @color-string: @color-success; 13 | @color-number: @color-info; 14 | @color-boolean: @color-info; 15 | @color-null: @color-nil; 16 | @color-undefined: @color-nil; 17 | 18 | /* highlight */ 19 | @highlight-bg-color: #e6f7ff; 20 | @highlight-bg-color-dark: #2e4558; 21 | 22 | /* comment */ 23 | @comment-color: #bfcbd9; 24 | 25 | /* common border-color */ 26 | @border-color: #bfcbd9; 27 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | interface JSONFlattenOptions { 2 | key?: string; 3 | index?: number; 4 | showComma: boolean; 5 | length: number; 6 | type: 7 | | 'content' 8 | | 'objectStart' 9 | | 'objectEnd' 10 | | 'objectCollapsed' 11 | | 'arrayStart' 12 | | 'arrayEnd' 13 | | 'arrayCollapsed'; 14 | } 15 | 16 | export type JSONDataType = string | number | boolean | unknown[] | Record | null; 17 | 18 | export interface JSONFlattenReturnType extends JSONFlattenOptions { 19 | content: string | number | null | boolean; 20 | level: number; 21 | path: string; 22 | } 23 | 24 | export function emitError(message: string): void { 25 | throw new Error(`[VueJSONPretty] ${message}`); 26 | } 27 | 28 | export function getDataType(value: unknown): string { 29 | return Object.prototype.toString.call(value).slice(8, -1).toLowerCase(); 30 | } 31 | 32 | export function jsonFlatten( 33 | data: JSONDataType, 34 | path = 'root', 35 | level = 0, 36 | options?: JSONFlattenOptions, 37 | ): JSONFlattenReturnType[] { 38 | const { 39 | key, 40 | index, 41 | type = 'content', 42 | showComma = false, 43 | length = 1, 44 | } = options || ({} as JSONFlattenOptions); 45 | const dataType = getDataType(data); 46 | 47 | if (dataType === 'array') { 48 | const inner = arrFlat( 49 | (data as JSONDataType[]).map((item, idx, arr) => 50 | jsonFlatten(item, `${path}[${idx}]`, level + 1, { 51 | index: idx, 52 | showComma: idx !== arr.length - 1, 53 | length, 54 | type, 55 | }), 56 | ), 57 | ) as JSONFlattenReturnType[]; 58 | return [ 59 | jsonFlatten('[', path, level, { 60 | showComma: false, 61 | key, 62 | length: (data as unknown[]).length, 63 | type: 'arrayStart', 64 | })[0], 65 | ].concat( 66 | inner, 67 | jsonFlatten(']', path, level, { 68 | showComma, 69 | length: (data as unknown[]).length, 70 | type: 'arrayEnd', 71 | })[0], 72 | ); 73 | } else if (dataType === 'object') { 74 | const keys = Object.keys(data as Record); 75 | const inner = arrFlat( 76 | keys.map((objKey, idx, arr) => 77 | jsonFlatten( 78 | (data as Record)[objKey], 79 | /^[a-zA-Z_]\w*$/.test(objKey) ? `${path}.${objKey}` : `${path}["${objKey}"]`, 80 | level + 1, 81 | { 82 | key: objKey, 83 | showComma: idx !== arr.length - 1, 84 | length, 85 | type, 86 | }, 87 | ), 88 | ), 89 | ) as JSONFlattenReturnType[]; 90 | return [ 91 | jsonFlatten('{', path, level, { 92 | showComma: false, 93 | key, 94 | index, 95 | length: keys.length, 96 | type: 'objectStart', 97 | })[0], 98 | ].concat( 99 | inner, 100 | jsonFlatten('}', path, level, { showComma, length: keys.length, type: 'objectEnd' })[0], 101 | ); 102 | } 103 | 104 | return [ 105 | { 106 | content: data as JSONFlattenReturnType['content'], 107 | level, 108 | key, 109 | index, 110 | path, 111 | showComma, 112 | length, 113 | type, 114 | }, 115 | ]; 116 | } 117 | 118 | export function arrFlat(arr: T): unknown[] { 119 | if (typeof Array.prototype.flat === 'function') { 120 | return arr.flat(); 121 | } 122 | const stack = [...arr]; 123 | const result = []; 124 | while (stack.length) { 125 | const first = stack.shift(); 126 | if (Array.isArray(first)) { 127 | stack.unshift(...first); 128 | } else { 129 | result.push(first); 130 | } 131 | } 132 | return result; 133 | } 134 | 135 | export function cloneDeep(source: T, hash = new WeakMap()): T { 136 | if (source === null || source === undefined) return source; 137 | if (source instanceof Date) return new Date(source) as T; 138 | if (source instanceof RegExp) return new RegExp(source) as T; 139 | if (typeof source !== 'object') return source; 140 | if (hash.get(source as Record)) 141 | return hash.get(source as Record); 142 | 143 | if (Array.isArray(source)) { 144 | const output = source.map(item => cloneDeep(item, hash)); 145 | hash.set(source, output); 146 | return output as T; 147 | } 148 | const output = {} as T; 149 | for (const key in source) { 150 | output[key] = cloneDeep(source[key], hash); 151 | } 152 | hash.set(source as Record, output); 153 | return output as T; 154 | } 155 | 156 | export function stringToAutoType(source: string): unknown { 157 | let value; 158 | if (source === 'null') value = null; 159 | else if (source === 'undefined') value = undefined; 160 | else if (source === 'true') value = true; 161 | else if (source === 'false') value = false; 162 | else if ( 163 | source[0] + source[source.length - 1] === '""' || 164 | source[0] + source[source.length - 1] === "''" 165 | ) { 166 | value = source.slice(1, -1); 167 | } else if ((typeof Number(source) === 'number' && !isNaN(Number(source))) || source === 'NaN') { 168 | value = Number(source); 169 | } else { 170 | value = source; 171 | } 172 | return value; 173 | } 174 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leezng/vue-json-pretty/01d7c27150961aea151f212d9b893de1da3ea3c3/static/.gitkeep -------------------------------------------------------------------------------- /static/logo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leezng/vue-json-pretty/01d7c27150961aea151f212d9b893de1da3ea3c3/static/logo.sketch -------------------------------------------------------------------------------- /static/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | vue-json-pretty 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /static/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leezng/vue-json-pretty/01d7c27150961aea151f212d9b893de1da3ea3c3/static/screenshot.png -------------------------------------------------------------------------------- /tsconfig.dts.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "emitDeclarationOnly": true, 6 | "outDir": "lib", 7 | "declaration": true, 8 | "sourceMap": false, 9 | "declarationDir": "types" 10 | }, 11 | "include": [ 12 | "src/**/*.ts", 13 | "src/**/*.tsx", 14 | "src/**/*.vue" 15 | ], 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "target": "esnext", 5 | "module": "esnext", 6 | "strict": true, 7 | "jsx": "preserve", 8 | "importHelpers": true, 9 | "moduleResolution": "node", 10 | "experimentalDecorators": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "sourceMap": true, 15 | "resolveJsonModule": true, 16 | "baseUrl": "./", 17 | "paths": { 18 | "src/*": ["src/*"] 19 | }, 20 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"] 21 | }, 22 | "include": [ 23 | "src/**/*.ts", 24 | "src/**/*.tsx", 25 | "src/**/*.vue", 26 | "example/**/*.ts", 27 | "example/**/*.tsx", 28 | "example/**/*.vue" 29 | ], 30 | "exclude": ["node_modules"], 31 | "files": ["shims-vue.d.ts"] 32 | } 33 | --------------------------------------------------------------------------------