├── options.html
├── eslint.config.js
├── jsconfig.json
├── src
├── constants
│ ├── common.js
│ └── options.js
├── vendor
│ ├── d3-state-visualizer
│ │ ├── lib
│ │ │ ├── index.js
│ │ │ ├── charts
│ │ │ │ ├── index.js
│ │ │ │ ├── tree
│ │ │ │ │ ├── sortAndSerialize.d.ts
│ │ │ │ │ ├── sortAndSerialize.js
│ │ │ │ │ ├── utils.d.ts
│ │ │ │ │ ├── tree.d.ts
│ │ │ │ │ └── utils.js
│ │ │ │ └── index.d.ts
│ │ │ └── index.d.ts
│ │ ├── src
│ │ │ ├── index.ts
│ │ │ └── charts
│ │ │ │ ├── index.ts
│ │ │ │ └── tree
│ │ │ │ ├── sortAndSerialize.ts
│ │ │ │ └── utils.ts
│ │ ├── tsconfig.json
│ │ ├── eslint.config.js
│ │ ├── vite.config.js
│ │ ├── LICENSE.md
│ │ ├── package.json
│ │ └── CHANGELOG.md
│ ├── d3tooltip
│ │ ├── tsconfig.json
│ │ ├── eslint.config.js
│ │ ├── lib
│ │ │ ├── index.d.ts
│ │ │ └── index.js
│ │ ├── LICENSE.md
│ │ ├── package.json
│ │ ├── CHANGELOG.md
│ │ └── src
│ │ │ └── index.ts
│ ├── map2tree
│ │ ├── tsconfig.json
│ │ ├── tsconfig.test.json
│ │ ├── jest.config.cjs
│ │ ├── lib
│ │ │ ├── index.d.ts
│ │ │ └── index.js
│ │ ├── eslint.config.js
│ │ ├── pnpm-lock.yaml
│ │ ├── package.json
│ │ ├── LICENSE.md
│ │ ├── CHANGELOG.md
│ │ ├── src
│ │ │ └── index.ts
│ │ └── test
│ │ │ └── map2tree.spec.ts
│ ├── tooltip
│ │ ├── style.css
│ │ └── index.js
│ └── switch
│ │ ├── switch.css
│ │ ├── switch.js
│ │ └── example.html
├── utils
│ ├── datetime.js
│ ├── dowloadFile.js
│ ├── json-viewer
│ │ ├── json-viewer.css
│ │ └── json-viewer.js
│ ├── common.js
│ ├── chart.ts
│ ├── convertContent.js
│ └── common.test.js
├── hooks
│ └── usePrevious.jsx
├── components
│ ├── Logo
│ │ ├── index.jsx
│ │ └── style.css
│ ├── Editor
│ │ ├── Toolbar
│ │ │ ├── style.css
│ │ │ └── index.jsx
│ │ ├── style.css
│ │ └── index.jsx
│ ├── Icons
│ │ ├── Tree.jsx
│ │ ├── Copy.jsx
│ │ ├── Branch.jsx
│ │ ├── Brackets.jsx
│ │ └── Gear.jsx
│ ├── Searchbar
│ │ ├── style.scss
│ │ └── index.jsx
│ ├── Select
│ │ └── index.jsx
│ └── Dropdown
│ │ ├── style.scss
│ │ └── index.jsx
├── testserver
│ ├── dummy.json
│ ├── index.html
│ └── index.js
├── css
│ ├── color-themes
│ │ ├── mdn-light.css
│ │ └── dark-pro.css
│ └── style.scss
├── scripts
│ ├── background.js
│ └── contentScript.js
├── main.jsx
├── Menus.jsx
├── App.jsx
└── TreeView.jsx
├── chrome-web-store.png
├── postcss.config.js
├── awesome-json-slideshow.gif
├── images
└── icons
│ ├── extensions.png
│ ├── interface.png
│ ├── json-file.png
│ ├── organizer.png
│ ├── seo-and-web.png
│ └── bell.svg
├── extension-resources
├── images
│ └── icons
│ │ ├── icon-128.png
│ │ ├── icon-16.png
│ │ └── icon-32.png
├── index.html
└── manifest.json
├── .prettierrc
├── .editorconfig
├── index.html
├── .gitignore
├── vite.options-page.config.js
├── vite.config.js
├── makefile
├── package.json
└── README.md
/options.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {"typeAcquisition": {"include": ["chrome"]}}
2 |
--------------------------------------------------------------------------------
/src/constants/common.js:
--------------------------------------------------------------------------------
1 | export const CSP_NONCE = '74554953-7310-42e7-a47a-03995c0de861';
--------------------------------------------------------------------------------
/src/vendor/d3-state-visualizer/lib/index.js:
--------------------------------------------------------------------------------
1 | export { tree } from './charts/index.js';
2 |
--------------------------------------------------------------------------------
/chrome-web-store.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rbrahul/Awesome-JSON-Viewer/HEAD/chrome-web-store.png
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | autoprefixer: {},
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/src/vendor/d3-state-visualizer/lib/charts/index.js:
--------------------------------------------------------------------------------
1 | export { default as tree } from './tree/tree.js';
2 |
--------------------------------------------------------------------------------
/awesome-json-slideshow.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rbrahul/Awesome-JSON-Viewer/HEAD/awesome-json-slideshow.gif
--------------------------------------------------------------------------------
/images/icons/extensions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rbrahul/Awesome-JSON-Viewer/HEAD/images/icons/extensions.png
--------------------------------------------------------------------------------
/images/icons/interface.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rbrahul/Awesome-JSON-Viewer/HEAD/images/icons/interface.png
--------------------------------------------------------------------------------
/images/icons/json-file.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rbrahul/Awesome-JSON-Viewer/HEAD/images/icons/json-file.png
--------------------------------------------------------------------------------
/images/icons/organizer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rbrahul/Awesome-JSON-Viewer/HEAD/images/icons/organizer.png
--------------------------------------------------------------------------------
/images/icons/seo-and-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rbrahul/Awesome-JSON-Viewer/HEAD/images/icons/seo-and-web.png
--------------------------------------------------------------------------------
/src/vendor/d3-state-visualizer/lib/charts/tree/sortAndSerialize.d.ts:
--------------------------------------------------------------------------------
1 | export default function sortAndSerialize(obj: unknown): string;
2 |
--------------------------------------------------------------------------------
/extension-resources/images/icons/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rbrahul/Awesome-JSON-Viewer/HEAD/extension-resources/images/icons/icon-128.png
--------------------------------------------------------------------------------
/extension-resources/images/icons/icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rbrahul/Awesome-JSON-Viewer/HEAD/extension-resources/images/icons/icon-16.png
--------------------------------------------------------------------------------
/extension-resources/images/icons/icon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rbrahul/Awesome-JSON-Viewer/HEAD/extension-resources/images/icons/icon-32.png
--------------------------------------------------------------------------------
/src/vendor/d3tooltip/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.esm.base.json",
3 | "compilerOptions": {
4 | "outDir": "lib"
5 | },
6 | "include": ["src"]
7 | }
8 |
--------------------------------------------------------------------------------
/src/vendor/map2tree/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.esm.base.json",
3 | "compilerOptions": {
4 | "outDir": "lib"
5 | },
6 | "include": ["src"]
7 | }
8 |
--------------------------------------------------------------------------------
/src/vendor/d3-state-visualizer/lib/index.d.ts:
--------------------------------------------------------------------------------
1 | export { tree } from './charts/index.js';
2 | export type { HierarchyPointNode, Node, Options, StyleValue, } from './charts/index.js';
3 |
--------------------------------------------------------------------------------
/src/vendor/map2tree/tsconfig.test.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.esm.base.json",
3 | "compilerOptions": {
4 | "types": ["jest"]
5 | },
6 | "include": ["src", "test"]
7 | }
8 |
--------------------------------------------------------------------------------
/src/vendor/d3-state-visualizer/src/index.ts:
--------------------------------------------------------------------------------
1 | export { tree } from './charts/index.js';
2 | export type {
3 | HierarchyPointNode,
4 | Node,
5 | Options,
6 | StyleValue,
7 | } from './charts/index.js';
8 |
--------------------------------------------------------------------------------
/src/vendor/d3-state-visualizer/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.esm.base.json",
3 | "compilerOptions": {
4 | "outDir": "lib",
5 | "noEmit": false
6 | },
7 | "include": ["src"],
8 | }
9 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 4,
3 | "singleQuote": true,
4 | "semi": true,
5 | "trailingComma": "all",
6 | "arrowParens": "always",
7 | "endOfLine": "lf",
8 | "bracketSpacing": true
9 | }
10 |
--------------------------------------------------------------------------------
/src/utils/datetime.js:
--------------------------------------------------------------------------------
1 | export const currentDateTime = () => {
2 | const date = new Date();
3 | return `${date.getFullYear()}${date.getMonth()+1}${date.getDate()}${date.getHours()}${date.getMinutes()}${date.getSeconds()}`
4 | }
5 |
6 |
--------------------------------------------------------------------------------
/src/vendor/d3-state-visualizer/src/charts/index.ts:
--------------------------------------------------------------------------------
1 | export type { HierarchyPointNode } from 'd3';
2 | export type { StyleValue } from 'd3tooltip';
3 | export { default as tree } from './tree/tree.js';
4 | export type { Node, Options } from './tree/tree.js';
5 |
--------------------------------------------------------------------------------
/src/vendor/d3-state-visualizer/lib/charts/index.d.ts:
--------------------------------------------------------------------------------
1 | export type { HierarchyPointNode } from 'd3';
2 | export type { StyleValue } from '../../../d3tooltip';
3 | export { default as tree } from './tree/tree.js';
4 | export type { Node, Options } from './tree/tree.js';
5 |
--------------------------------------------------------------------------------
/src/vendor/d3tooltip/eslint.config.js:
--------------------------------------------------------------------------------
1 | import eslintJs from '../../eslint.js.config.base.mjs';
2 | import eslintTs from '../../eslint.ts.config.base.mjs';
3 |
4 | export default [
5 | ...eslintJs,
6 | ...eslintTs(import.meta.dirname),
7 | {
8 | ignores: ['lib'],
9 | },
10 | ];
11 |
--------------------------------------------------------------------------------
/src/vendor/map2tree/jest.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extensionsToTreatAsEsm: ['.ts'],
3 | moduleNameMapper: {
4 | '^(\\.{1,2}/.*)\\.js$': '$1',
5 | },
6 | transform: {
7 | '^.+\\.ts$': ['ts-jest', { tsconfig: 'tsconfig.test.json', useESM: true }],
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/src/hooks/usePrevious.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 |
3 |
4 | const usePrevious = (value) => {
5 | const ref = useRef();
6 | useEffect(() => {
7 | ref.current = value;
8 | },[value]);
9 | return ref.current;
10 | };
11 |
12 | export default usePrevious;
13 |
--------------------------------------------------------------------------------
/src/vendor/d3-state-visualizer/eslint.config.js:
--------------------------------------------------------------------------------
1 | import eslintJs from '../../eslint.js.config.base.mjs';
2 | import eslintTs from '../../eslint.ts.config.base.mjs';
3 |
4 | export default [
5 | ...eslintJs,
6 | ...eslintTs(import.meta.dirname),
7 | {
8 | ignores: ['examples', 'lib'],
9 | },
10 | ];
11 |
--------------------------------------------------------------------------------
/src/vendor/map2tree/lib/index.d.ts:
--------------------------------------------------------------------------------
1 | export interface Node {
2 | name: string;
3 | children?: this[];
4 | object?: unknown;
5 | value?: unknown;
6 | }
7 | export declare function map2tree(root: unknown, options?: {
8 | key?: string;
9 | pushMethod?: 'push' | 'unshift';
10 | }, tree?: Node): Node | {};
11 |
--------------------------------------------------------------------------------
/src/components/Logo/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import './style.css';
3 |
4 | const Logo = () => {
5 | return (
6 | {'{..}'}
7 | JSON Viewer Pro
8 |
9 | );
10 | }
11 |
12 | export default Logo;
13 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: https://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | end_of_line = lf
9 | insert_final_newline = true
10 | charset = utf-8
11 | indent_style = space
12 | indent_size = 4
13 | trim_trailing_whitespace = true
14 |
--------------------------------------------------------------------------------
/src/vendor/d3-state-visualizer/vite.config.js:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path'
2 | import {defineConfig} from 'vite';
3 | export default defineConfig({
4 | build: {
5 | lib: {
6 | entry: [resolve(__dirname, './src/index.ts')],
7 | name: 'd3state',
8 | fileName: (format) => `d3state.${format}.js`,
9 | },
10 | }
11 | })
--------------------------------------------------------------------------------
/src/vendor/map2tree/eslint.config.js:
--------------------------------------------------------------------------------
1 | import eslintJs from '../../eslint.js.config.base.mjs';
2 | import eslintTs from '../../eslint.ts.config.base.mjs';
3 | import eslintTsJest from '../../eslint.ts.jest.config.base.mjs';
4 |
5 | export default [
6 | ...eslintJs,
7 | ...eslintTs(import.meta.dirname),
8 | ...eslintTsJest(import.meta.dirname),
9 | {
10 | ignores: ['lib'],
11 | },
12 | ];
13 |
--------------------------------------------------------------------------------
/src/vendor/map2tree/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: '9.0'
2 |
3 | settings:
4 | autoInstallPeers: true
5 | excludeLinksFromLockfile: false
6 |
7 | importers:
8 |
9 | .:
10 | dependencies:
11 | lodash-es:
12 | specifier: ^4.17.21
13 | version: 4.17.21
14 |
15 | packages:
16 |
17 | lodash-es@4.17.21:
18 | resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
19 |
20 | snapshots:
21 |
22 | lodash-es@4.17.21: {}
23 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | JSON Viewer Pro
7 |
8 |
9 |
10 | {
11 | "success": {
12 | "message": "Welcome to JSON Viewer Pro",
13 | "status_code": 200
14 | }
15 | }
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | node_modules
3 |
4 | # testing
5 | coverage
6 |
7 | # production
8 | build
9 | chrome-extension
10 |
11 | # misc
12 | .idea
13 | .env
14 | npm-debug.log
15 | *.zip
16 | .DS_Store
17 | # Logs
18 | logs
19 | *.log
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 | pnpm-debug.log*
24 | lerna-debug.log*
25 |
26 | dist
27 | dist-options-page
28 | dist-ssr
29 | *.local
30 |
31 | # Editor directories and files
32 | .vscode/*
33 | !.vscode/extensions.json
34 | .idea
35 | .DS_Store
36 | *.suo
37 | *.ntvs*
38 | *.njsproj
39 | *.sln
40 | *.sw?
41 |
--------------------------------------------------------------------------------
/src/utils/dowloadFile.js:
--------------------------------------------------------------------------------
1 | const downloadFile = (fileContent, contentType, fileName) => {
2 | const downloadBtn = document.createElement('a');
3 | downloadBtn.id = 'rb-download-json';
4 | downloadBtn.download = fileName;
5 | downloadBtn.style = 'display:none;';
6 | downloadBtn.href =
7 | `data:${contentType};charset=utf-8,` +
8 | encodeURIComponent(fileContent);
9 | document.body.appendChild(downloadBtn);
10 | downloadBtn.click();
11 | setTimeout(() => {
12 | document.body.removeChild(downloadBtn);
13 | }, 500);
14 | };
15 |
16 | export default downloadFile;
17 |
--------------------------------------------------------------------------------
/vite.options-page.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | //@ts-ignore
3 | import path from 'path';
4 | import terser from '@rollup/plugin-terser';
5 |
6 | export default defineConfig({
7 | build: {
8 | outDir: 'dist-options-page',
9 | rollupOptions: {
10 | input: {
11 | options: path.resolve(__dirname, '/src/options/js/options.js'),
12 | },
13 | output: {
14 | entryFileNames: `[name].js`,
15 | assetFileNames: `[name].[ext]`,
16 | format: 'iife',
17 | plugins: [terser()],
18 | },
19 | },
20 | },
21 | });
22 |
--------------------------------------------------------------------------------
/src/components/Editor/Toolbar/style.css:
--------------------------------------------------------------------------------
1 | .jv-toolbar {
2 | box-sizing: border-box;
3 | display: flex;
4 | flex-direction: row;
5 | justify-content: flex-start;
6 | width: 100%;
7 | padding: 10px;
8 | background-color: var(--toolbar-bg-color);
9 | border:1px solid var(--toolbar-border-color);
10 | }
11 |
12 | .jv-toolbar .jv-btn {
13 | margin-right: 8px;
14 | display: inline-flex;
15 | justify-content: center;
16 | align-items: center;
17 | }
18 |
19 | .jv-content-type-menu{
20 | width: 115px;
21 | margin-right: 10px;
22 | }
23 |
24 | .jv-content-type-btn{
25 | padding: 6px 8px;
26 | box-sizing: border-box;
27 | }
28 |
--------------------------------------------------------------------------------
/src/vendor/d3-state-visualizer/lib/charts/tree/sortAndSerialize.js:
--------------------------------------------------------------------------------
1 | function sortObject(obj, strict) {
2 | if (obj instanceof Array) {
3 | let ary;
4 | if (strict) {
5 | ary = obj.sort();
6 | }
7 | else {
8 | ary = obj;
9 | }
10 | return ary;
11 | }
12 | if (obj && typeof obj === 'object') {
13 | const tObj = {};
14 | Object.keys(obj)
15 | .sort()
16 | .forEach((key) => (tObj[key] = sortObject(obj[key])));
17 | return tObj;
18 | }
19 | return obj;
20 | }
21 | export default function sortAndSerialize(obj) {
22 | return JSON.stringify(sortObject(obj, true), undefined, 2);
23 | }
24 |
--------------------------------------------------------------------------------
/src/vendor/d3-state-visualizer/lib/charts/tree/utils.d.ts:
--------------------------------------------------------------------------------
1 | import type { InternalNode } from './tree.js';
2 | export declare function collapseChildren(node: InternalNode): void;
3 | export declare function expandChildren(node: InternalNode): void;
4 | export declare function toggleChildren(node: InternalNode): InternalNode;
5 | export declare function visit(parent: InternalNode, visitFn: (parent: InternalNode) => void, childrenFn: (parent: InternalNode) => InternalNode[] | null | undefined): void;
6 | export declare function getNodeGroupByDepthCount(rootNode: InternalNode): number[];
7 | export declare function getTooltipString(node: InternalNode, { indentationSize }: {
8 | indentationSize?: number | undefined;
9 | }): string;
10 |
--------------------------------------------------------------------------------
/src/vendor/d3-state-visualizer/src/charts/tree/sortAndSerialize.ts:
--------------------------------------------------------------------------------
1 | function sortObject(obj: unknown, strict?: boolean) {
2 | if (obj instanceof Array) {
3 | let ary;
4 | if (strict) {
5 | ary = obj.sort();
6 | } else {
7 | ary = obj;
8 | }
9 | return ary;
10 | }
11 |
12 | if (obj && typeof obj === 'object') {
13 | const tObj: { [key: string]: unknown } = {};
14 | Object.keys(obj)
15 | .sort()
16 | .forEach((key) => (tObj[key] = sortObject(obj[key as keyof typeof obj])));
17 | return tObj;
18 | }
19 |
20 | return obj;
21 | }
22 |
23 | export default function sortAndSerialize(obj: unknown) {
24 | return JSON.stringify(sortObject(obj, true), undefined, 2);
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/Logo/style.css:
--------------------------------------------------------------------------------
1 | .jv-logo {
2 | display: inline-flex;
3 | align-items: center;
4 | justify-content: flex-start;
5 | }
6 |
7 | .jv-logo-icon {
8 | background-color: #7c4bde;
9 | margin: 0 auto;
10 | width: 36px;
11 | height: 36px;
12 | border-radius: 100%;
13 | border: 2px solid white;
14 | display: inline-flex;
15 | justify-content: center;
16 | align-items: center;
17 | font-weight: bold;
18 | box-sizing: border-box;
19 | padding-bottom:3px;
20 | margin-right: 15px;
21 | color: white;
22 | }
23 |
24 | .jv-logo-title {
25 | display: inline-block;
26 | font-size:18px;
27 | color:var(--primary-text-color);
28 | line-height: 100%;
29 | }
30 |
--------------------------------------------------------------------------------
/extension-resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 | JSON Viewer Pro
12 |
13 |
14 |
15 |
16 | {
17 | "success": {
18 | "message": "Welcome to JSON Viewer Pro",
19 | "status_code": 200
20 | }
21 | }
22 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 | //@ts-ignore
4 | import path from 'path';
5 | import terser from '@rollup/plugin-terser';
6 |
7 | export default defineConfig({
8 | plugins: [react()],
9 | css: {
10 | postcss: './postcss.config.js',
11 | },
12 | build: {
13 | cssCodeSplit: false,
14 | rollupOptions: {
15 | input: {
16 | main: path.resolve(__dirname, '/src/main.jsx'),
17 | },
18 | output: {
19 | entryFileNames: `assets/[name].js`,
20 | assetFileNames: `assets/[name].[ext]`,
21 | format: 'iife',
22 | plugins: [terser()],
23 | },
24 | },
25 | },
26 | });
27 |
--------------------------------------------------------------------------------
/images/icons/bell.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/vendor/d3tooltip/lib/index.d.ts:
--------------------------------------------------------------------------------
1 | import type { BaseType, Selection } from 'd3';
2 | export type StyleValue = string | number | boolean;
3 | interface Options {
4 | left: number | undefined;
5 | top: number | undefined;
6 | offset: {
7 | left: number;
8 | top: number;
9 | };
10 | root: Selection | undefined;
11 | styles: {
12 | [key: string]: StyleValue;
13 | };
14 | text: string | ((datum: Datum) => string);
15 | }
16 | export declare function tooltip(className?: string, options?: Partial>): (selection: Selection) => void;
17 | export {};
18 |
--------------------------------------------------------------------------------
/src/vendor/map2tree/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "map2tree",
3 | "version": "4.0.0",
4 | "description": "Utility for mapping maps to trees",
5 | "private": true,
6 | "license": "MIT",
7 | "files": [
8 | "lib",
9 | "src"
10 | ],
11 | "main": "lib/index.js",
12 | "types": "lib/index.d.ts",
13 | "type": "module",
14 | "sideEffects": false,
15 | "scripts": {
16 | "build": "tsc",
17 | "clean": "rimraf lib",
18 | "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
19 | "lint": "eslint .",
20 | "type-check": "tsc --noEmit",
21 | "prepack": "pnpm run clean && pnpm run build",
22 | "prepublish": "pnpm run lint && pnpm run test"
23 | },
24 | "dependencies": {
25 | "lodash-es": "^4.17.21"
26 | },
27 | "devDependencies": {
28 | "@types/jest": "^29.5.13",
29 | "@types/lodash-es": "^4.17.12",
30 | "immutable": "^4.3.7",
31 | "jest": "^29.7.0",
32 | "rimraf": "^6.0.1",
33 | "ts-jest": "^29.2.5",
34 | "typescript": "~5.5.4"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/utils/json-viewer/json-viewer.css:
--------------------------------------------------------------------------------
1 | /* Syntax highlighting for JSON objects */
2 | ul.json-dict, ol.json-array {
3 | list-style-type: none;
4 | margin: 0 0 0 1px;
5 | border-left: 1px dotted #5D6D7E;
6 | padding-left: 2em;
7 | }
8 | .json-string {
9 | color: #0B7500;
10 | }
11 | .json-literal {
12 | color: #b568de;
13 | font-weight: bold;
14 | }
15 |
16 | /* Toggle button */
17 | a.json-toggle {
18 | position: relative;
19 | color: inherit;
20 | text-decoration: none;
21 | }
22 | a.json-toggle:focus {
23 | outline: none;
24 | }
25 | a.json-toggle:before {
26 | color: #aaa;
27 | content: "\25BC"; /* down arrow */
28 | position: absolute;
29 | display: inline-block;
30 | width: 1em;
31 | left: -1em;
32 | font-size: 14px;
33 | }
34 | a.json-toggle.collapsed:before {
35 | content: "\25B6"; /* left arrow */
36 | font-size: 14px;
37 | }
38 |
39 | /* Collapsable placeholder links */
40 | a.json-placeholder {
41 | color: #aaa;
42 | padding: 0 1em;
43 | text-decoration: none;
44 | }
45 | a.json-placeholder:hover {
46 | text-decoration: underline;
47 | }
48 |
--------------------------------------------------------------------------------
/src/constants/options.js:
--------------------------------------------------------------------------------
1 | export const DETECTION_METHOD_CONTENT_TYPE = 'contentType';
2 | export const DETECTION_METHOD_JSON_CONTENT = 'jsonContent';
3 |
4 | export const DEFAULT_SELECTED_CONTENT_TYPES = [
5 | 'application/json',
6 | 'text/json',
7 | 'application/javascript',
8 | ];
9 |
10 | export const DEFAULT_OPTIONS = {
11 | theme: 'default',
12 | collapsed: 0,
13 | css: `
14 | /**Write your CSS style **/
15 | #json-viewer {
16 | font-size: 15px;
17 | }
18 |
19 | .property{
20 | /*color:#994c9e;*/
21 | }
22 |
23 | .json-literal-numeric{
24 | /*color:#F5B041;*/
25 | }
26 |
27 | .json-literal-url {
28 | /*color: #34a632;*/
29 | }
30 |
31 | .json-literal-string{
32 | /*color:#0642b0;*/
33 | }
34 |
35 | .json-literal{
36 | /*color:#b568de;*/
37 | }
38 |
39 | .json-literal-boolean{
40 | /*color: #f23ebb;*/
41 | }`,
42 | jsonDetection: {
43 | method: DETECTION_METHOD_CONTENT_TYPE, // contentType | jsonContent
44 | selectedContentTypes: DEFAULT_SELECTED_CONTENT_TYPES,
45 | },
46 | };
47 |
48 | export const DARK_THEMES = ['default', 'dark-pro'];
49 |
--------------------------------------------------------------------------------
/src/vendor/d3tooltip/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 romseguy
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 |
--------------------------------------------------------------------------------
/src/utils/common.js:
--------------------------------------------------------------------------------
1 | export const calculateFileSize = (size) => {
2 | for (let unit of ['bytes', 'KB', 'MB', 'GB', 'TB']) {
3 | if (size < 1000.0) {
4 | if (unit !== 'bytes') {
5 | size = size.toFixed(1);
6 | }
7 | return `${size} ${unit}`;
8 | }
9 | size /= 1000.0;
10 | }
11 | return size;
12 | };
13 |
14 | export const iconFillColor = (isDarkMode) => {
15 | return {
16 | fillColor: isDarkMode ? '#FFFFFF' : '#000000',
17 | };
18 | };
19 |
20 | export const getURL = (assetPath) => {
21 | const optionUrl = window.extensionOptions?.optionPageURL ?? '';
22 | try {
23 | const url = new URL(optionUrl);
24 | return `${url.origin}/${assetPath}`;
25 | } catch (error) {
26 | return assetPath;
27 | }
28 | };
29 |
30 | const bigIntToStrTransformer = (key, value, context) => {
31 | if (typeof value === 'number' && value > Number.MAX_SAFE_INTEGER) {
32 | return String(context.source);
33 | }
34 | return value;
35 | };
36 |
37 | export const parseJson = (jsonStr) => {
38 | return JSON.parse(jsonStr, bigIntToStrTransformer);
39 | };
40 |
--------------------------------------------------------------------------------
/src/vendor/map2tree/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Romain Séguy
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 |
--------------------------------------------------------------------------------
/src/vendor/d3-state-visualizer/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Romain Séguy
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 |
--------------------------------------------------------------------------------
/src/utils/chart.ts:
--------------------------------------------------------------------------------
1 | export const getAppliedTransformation = (
2 | element:HTMLElement
3 | ):Record => {
4 | const translateAttr = element.getAttribute('transform');
5 | const appliedTransforms:Record = {};
6 | if (translateAttr) {
7 | const translatePattern =
8 | /translate\((-?\d+\.?\d*)+,\s*(-?\d+\.?\d*)+\)/;
9 | const scalePattern = /scale\((-?\d+\.?\d*)+\)/;
10 | const translateExecResult = translatePattern.exec(translateAttr);
11 |
12 | if (translateExecResult) {
13 | appliedTransforms['translate'] = translateExecResult?.[0];
14 | appliedTransforms['translateX'] = Number(translateExecResult?.[1]);
15 | appliedTransforms['translateY'] = Number(translateExecResult?.[2]);
16 | }
17 | const scalePatternExecResult = scalePattern.exec(translateAttr);
18 | if (scalePatternExecResult) {
19 | appliedTransforms['scale'] = scalePatternExecResult?.[0];
20 | appliedTransforms['scaleValue'] = Number(
21 | scalePatternExecResult?.[1],
22 | );
23 | }
24 | }
25 | return appliedTransforms;
26 | };
27 |
--------------------------------------------------------------------------------
/src/vendor/d3tooltip/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "d3tooltip",
3 | "version": "4.0.0",
4 | "description": "A highly configurable tooltip for d3",
5 | "keywords": [
6 | "d3",
7 | "tooltip"
8 | ],
9 | "homepage": "https://github.com/reduxjs/redux-devtools/tree/master/packages/d3tooltip",
10 | "bugs": {
11 | "url": "https://github.com/reduxjs/redux-devtools/issues"
12 | },
13 | "license": "MIT",
14 | "author": "romseguy",
15 | "files": [
16 | "lib",
17 | "src"
18 | ],
19 | "main": "lib/index.js",
20 | "types": "lib/index.d.ts",
21 | "type": "module",
22 | "sideEffects": false,
23 | "repository": {
24 | "type": "git",
25 | "url": "https://github.com/reduxjs/redux-devtools.git"
26 | },
27 | "scripts": {
28 | "build": "tsc",
29 | "clean": "rimraf lib",
30 | "lint": "eslint .",
31 | "type-check": "tsc --noEmit",
32 | "prepack": "pnpm run clean && pnpm run build",
33 | "prepublish": "pnpm run lint"
34 | },
35 | "devDependencies": {
36 | "@types/d3": "^7.4.3",
37 | "d3": "^7.9.0",
38 | "rimraf": "^6.0.1",
39 | "typescript": "~5.5.4"
40 | },
41 | "peerDependencies": {
42 | "@types/d3": "^7.4.3",
43 | "d3": "^7.9.0"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 | chrome_build_dir = $(shell pwd)/chrome-extension
2 |
3 | .PHONY: build package clean
4 |
5 | package:
6 | @rm -rf ${chrome_build_dir}
7 | @echo "✅ Deleted Existing Chrome Build Directory"
8 | @echo "🗂 Moving Required Files to the CHROME-EXTENSION"
9 | @mkdir ${chrome_build_dir} ${chrome_build_dir}/css ${chrome_build_dir}/js
10 | @cp ./dist/assets/main.js ${chrome_build_dir}/js
11 | @cp ./dist/assets/style.css ${chrome_build_dir}/css
12 | @cp -r ./extension-resources/ ${chrome_build_dir}/
13 | @cp -r ./images/ ${chrome_build_dir}/images
14 | @mkdir ${chrome_build_dir}/css/color-themes
15 | @cp -r ./src/css/color-themes/ ${chrome_build_dir}/css/color-themes
16 | @cp -r ./src/css/codemirror.css ${chrome_build_dir}/css/codemirror.css
17 | @cp -r ./dist-options-page/options.js ${chrome_build_dir}/js/
18 | @cp -r ./src/options/css/ ${chrome_build_dir}/css/
19 | @cp -r ./src/options/options.html ${chrome_build_dir}/
20 | @cp -r ./src/scripts/ ${chrome_build_dir}/js/
21 | @cd $(chrome_build_dir); \
22 | zip -r chrome-extension.zip .
23 | @echo "✅ Package has been created successfuly. And ready to be shiped 🎁"
24 |
25 | clean:
26 | @rm -rf ${chrome_build_dir}
27 | @rm -rf ./dist
28 | @rm -rf ./dist-options-page
29 | @echo "✅ Deleted BUILD and CHROME_EXTENSION Directories"
--------------------------------------------------------------------------------
/src/components/Editor/style.css:
--------------------------------------------------------------------------------
1 | .jv-editor {
2 | position:relative;
3 | margin:0 auto;
4 | margin-top: 80px;
5 | width: 80%;
6 | }
7 |
8 | .jv-code-editor{
9 | background-color: var(--json-input-bg-color);
10 | height: 600px;
11 | box-sizing: border-box;
12 | overflow:hidden;
13 | }
14 |
15 | .alert-close-btn{
16 | float: right;
17 | background: none;
18 | border: none;
19 | outline: none;
20 | color: var(--primary-text-color);
21 | font-size: 20px;
22 | padding:0px;
23 | margin:0px;
24 | }
25 |
26 | .editor-footer{
27 | position: absolute;
28 | bottom:-20px;
29 | right: 0px;
30 | left: 0px;
31 | background-color: var(--editor-footer-bg-color);
32 | color:var(--editor-footer-text-color);
33 | display: flex;
34 | flex-direction: row;
35 | justify-content: flex-start;
36 | align-items: center;
37 | font-size: 12px;
38 | padding: 5px 10px;
39 | border: 1px solid var(--editor-footer-border-color);
40 | }
41 |
42 | .message-area{
43 | box-sizing: border-box;
44 | padding: 0px 10px;
45 | width: 75%;
46 | }
47 |
48 | .info-area{
49 | box-sizing: border-box;
50 | width: 25%;
51 | border-left: 1px solid var(--editor-footer-border-color);
52 | padding: 0px 10px;
53 | }
54 |
55 | .cursor-position-info {
56 | margin-right: 20px;
57 | }
58 |
--------------------------------------------------------------------------------
/src/vendor/d3-state-visualizer/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "d3-state-visualizer",
3 | "version": "3.0.0",
4 | "description": "Visualize your app state with a range of reusable charts",
5 | "keywords": [
6 | "d3",
7 | "state",
8 | "store",
9 | "tree",
10 | "visualization"
11 | ],
12 | "homepage": "https://github.com/reduxjs/redux-devtools/tree/master/packages/d3-state-visualizer",
13 | "bugs": {
14 | "url": "https://github.com/reduxjs/redux-devtools/issues"
15 | },
16 | "license": "MIT",
17 | "author": "romseguy",
18 | "files": [
19 | "dist",
20 | "lib",
21 | "src"
22 | ],
23 | "main": "lib/index.js",
24 | "types": "lib/index.d.ts",
25 | "type": "module",
26 | "sideEffects": false,
27 | "repository": {
28 | "type": "git",
29 | "url": "https://github.com/reduxjs/redux-devtools.git"
30 | },
31 | "scripts": {
32 | "build": "tsc",
33 | "clean": "rimraf lib",
34 | "lint": "eslint .",
35 | "type-check": "tsc --noEmit",
36 | "prepack": "pnpm run clean && pnpm run build",
37 | "prepublish": "pnpm run lint"
38 | },
39 | "dependencies": {
40 | "@types/d3": "^7.4.3",
41 | "d3": "^7.9.0",
42 | "deepmerge": "^4.3.1",
43 | "lodash-es": "^4.17.21",
44 | "ramda": "^0.30.1"
45 | },
46 | "devDependencies": {
47 | "@types/ramda": "^0.30.2",
48 | "rimraf": "^6.0.1",
49 | "typescript": "~5.5.4"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/utils/convertContent.js:
--------------------------------------------------------------------------------
1 | import { dump, load } from 'js-yaml';
2 | import { XMLParser, XMLBuilder } from 'fast-xml-parser';
3 | import { parseJson } from './common';
4 |
5 | const XMLArrayRoot = 'XMLArrayRoot';
6 |
7 | export const trimXMLArrayRoot = (jsObject) => {
8 | if (!Array.isArray(jsObject) && XMLArrayRoot in jsObject) {
9 | jsObject = jsObject[XMLArrayRoot];
10 | }
11 | return jsObject;
12 | };
13 |
14 | export const convertToJsObject = (content, currentFormat) => {
15 | if (currentFormat === 'YAML') {
16 | return load(content);
17 | }
18 |
19 | if (currentFormat === 'XML') {
20 | const parser = new XMLParser();
21 | return parser.parse(content);
22 | }
23 |
24 | if (currentFormat === 'JSON') {
25 | return parseJson(content);
26 | }
27 | };
28 |
29 | export const convertContent = (data, from, to) => {
30 | let result;
31 | let jsObject = convertToJsObject(data, from);
32 |
33 | if (to === 'JSON') {
34 | jsObject = trimXMLArrayRoot(jsObject);
35 | result = JSON.stringify(jsObject, null, 4);
36 | } else if (to === 'YAML') {
37 | jsObject = trimXMLArrayRoot(jsObject);
38 | result = dump(jsObject);
39 | } else if (to === 'XML') {
40 | const parserOptions = {
41 | arrayNodeName: XMLArrayRoot,
42 | format: true,
43 | };
44 | const builder = new XMLBuilder(parserOptions);
45 | result = builder.build(jsObject);
46 | }
47 | return result;
48 | };
49 |
--------------------------------------------------------------------------------
/extension-resources/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "JSON Viewer Pro",
3 | "short_name": "JSON Viewer",
4 | "offline_enabled": true,
5 | "version": "1.0.6",
6 | "manifest_version": 3,
7 | "icons": {
8 | "16": "images/icons/icon-16.png",
9 | "38": "images/icons/icon-32.png",
10 | "128": "images/icons/icon-128.png"
11 | },
12 | "background": {
13 | "service_worker": "/js/background.js"
14 | },
15 | "options_page": "options.html",
16 | "content_scripts": [
17 | {
18 | "matches": [
19 | "http://*/*",
20 | "https://*/*",
21 | "ftp://*/*",
22 | "file:///*",
23 | "*://*/*"
24 | ],
25 | "js": ["/js/contentScript.js"],
26 | "run_at": "document_start",
27 | "all_frames": false
28 | }
29 | ],
30 | "action": {
31 | "default_icon": "images/icons/icon-32.png"
32 | },
33 | "permissions": ["contextMenus", "storage"],
34 | "host_permissions": ["*://*/*"],
35 | "web_accessible_resources": [
36 | {
37 | "resources": [
38 | "/js/main.js",
39 | "/css/style.css",
40 | "/css/color-themes/dark-pro.css",
41 | "/css/color-themes/mdn-light.css",
42 | "/css/codemirror.css",
43 | "/images/icons/bell.svg",
44 | "options.html"
45 | ],
46 | "matches": ["*://*/*", "ftp://*/*", "file:///*"]
47 | }
48 | ]
49 | }
50 |
--------------------------------------------------------------------------------
/src/vendor/map2tree/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## 4.0.0
4 |
5 | ### Major Changes
6 |
7 | - 191d419: Convert d3 packages to ESM
8 |
9 | ## 3.0.0
10 |
11 | ### Major Changes
12 |
13 | - b323f77d: Remove UMD build.
14 |
15 | ## 2.0.0
16 |
17 | - Adds ESM build (https://github.com/reduxjs/redux-devtools/pull/997) and switches the default export to a named export in order to ensure that the CommonJS output and the ESM output are [interchangeable](https://rollupjs.org/guide/en/#outputexports):
18 |
19 | ```diff
20 | - import map2tree from 'map2tree';
21 | + import { map2tree } from 'map2tree';
22 | ```
23 |
24 | ## [1.5.0](https://github.com/reduxjs/redux-devtools/compare/map2tree@1.4.2...map2tree@1.5.0) (2021-03-06)
25 |
26 | ### Features
27 |
28 | - **d3-state-visualizer:** convert to TypeScript ([#640](https://github.com/reduxjs/redux-devtools/issues/640)) ([0c78a5a](https://github.com/reduxjs/redux-devtools/commit/0c78a5a9a76ee7eff37dcd8e39272d98c03e0869))
29 | - **d3tooltip:** convert to TypeScript ([#639](https://github.com/reduxjs/redux-devtools/issues/639)) ([3b580da](https://github.com/reduxjs/redux-devtools/commit/3b580dad4cb36abc395f9be139b2c3f94e872d87))
30 | - **map2tree:** convert to TypeScript ([#638](https://github.com/reduxjs/redux-devtools/issues/638)) ([3b027f4](https://github.com/reduxjs/redux-devtools/commit/3b027f400e0e326596eedc2ee17ab45a8383080d))
31 |
32 | ## 1.4.2 (2020-08-14)
33 |
34 | ### Bug Fixes
35 |
36 | - **map2tree:** consolidate immutable version ([#538](https://github.com/reduxjs/redux-devtools/issues/538)) ([999ed2a](https://github.com/reduxjs/redux-devtools/commit/999ed2ad8b4a09eddd55c2a944f5488ecce6bc7b))
37 |
--------------------------------------------------------------------------------
/src/vendor/d3tooltip/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## 4.0.0
4 |
5 | ### Major Changes
6 |
7 | - 191d419: Convert d3 packages to ESM
8 |
9 | ## 3.0.1
10 |
11 | ### Patch Changes
12 |
13 | - 7f5bddbd: Widen peer dependencies
14 |
15 | ## 3.0.0
16 |
17 | ### Major Changes
18 |
19 | - b323f77d: Upgrade D3
20 |
21 | - Remove UMD build.
22 | - Upgrade d3 peer dependency from v3 to v7.
23 | - Remove `attr` configuration method.
24 | - Rename `style` configuration method to `styles` and move to options.
25 | - Move `text` configuration method to options.
26 | - Remove d3 parameter as first parameter for `tooltip`.
27 |
28 | ## 2.0.0
29 |
30 | - Adds ESM build (https://github.com/reduxjs/redux-devtools/pull/997) and switches the default export to a named export in order to ensure that the CommonJS output and the ESM output are [interchangeable](https://rollupjs.org/guide/en/#outputexports):
31 |
32 | ```diff
33 | - import d3tooltip from 'd3tooltip';
34 | + import { tooltip } from 'd3tooltip';
35 | ```
36 |
37 | ## [1.3.0](https://github.com/reduxjs/redux-devtools/compare/d3tooltip@1.2.3...d3tooltip@1.3.0) (2021-03-06)
38 |
39 | ### Features
40 |
41 | - **d3-state-visualizer:** convert to TypeScript ([#640](https://github.com/reduxjs/redux-devtools/issues/640)) ([0c78a5a](https://github.com/reduxjs/redux-devtools/commit/0c78a5a9a76ee7eff37dcd8e39272d98c03e0869))
42 | - **d3tooltip:** convert to TypeScript ([#639](https://github.com/reduxjs/redux-devtools/issues/639)) ([3b580da](https://github.com/reduxjs/redux-devtools/commit/3b580dad4cb36abc395f9be139b2c3f94e872d87))
43 |
44 | ## 1.2.3 (2020-08-14)
45 |
46 | **Note:** Version bump only for package d3tooltip
47 |
--------------------------------------------------------------------------------
/src/vendor/tooltip/style.css:
--------------------------------------------------------------------------------
1 | #tooltip-msg {
2 | position:absolute;
3 | padding: 15px;
4 | border-radius: 4px;
5 | color:#fff;
6 | font-size: 14px;
7 | transition: ease-in-ease-out .3s;
8 | background:black;
9 | z-index:100000000000;
10 | max-width:300px;
11 | }
12 |
13 | #tooltip-msg.small {
14 | padding: 8px;
15 | font-size: 12px;
16 | }
17 |
18 | #tooltip-msg.medium {
19 | padding: 10px;
20 | font-size: 13px;
21 | }
22 |
23 | #tooltip-msg:before {
24 | content: '';
25 | width:0px;
26 | height:0px;
27 | border:8px solid black;
28 | z-index:99999999;
29 | position:absolute;
30 | z-index:-1;
31 | }
32 | .hidden{
33 | display:none !important;
34 | }
35 | .visible{
36 | display:block !important;
37 | }
38 |
39 | #tooltip-msg.dir-right {
40 | transform: translateY(-50%);
41 | }
42 |
43 | #tooltip-msg.dir-right::before {
44 | left: -14px;
45 | top: calc(50% - 2px);
46 | transform: rotate(45deg) translateY(-50%);
47 | }
48 |
49 | #tooltip-msg.dir-bottom {
50 | transform: translateX(-50%);
51 | }
52 |
53 | #tooltip-msg.dir-bottom::before {
54 | left: 50%;
55 | top: -2px;
56 | transform: rotate(45deg) translateX(-50%);
57 | }
58 |
59 | #tooltip-msg.dir-left {
60 | transform: translateX(-100%) translateY(-50%);
61 | }
62 |
63 | #tooltip-msg.dir-left::before {
64 | right: -2px;
65 | top: calc(50% - 2px);
66 | transform: rotate(45deg) translateY(-50%);
67 | }
68 |
69 | #tooltip-msg.dir-top {
70 | transform: translateX(-50%) translateY(-100%);
71 | }
72 |
73 | #tooltip-msg.dir-top::before {
74 | left: 50%;
75 | bottom: -12px;
76 | transform: rotate(45deg) translateX(-50%);
77 | }
--------------------------------------------------------------------------------
/src/components/Icons/Tree.jsx:
--------------------------------------------------------------------------------
1 | const Tree = ({ className, fillColor = '#ffffff' }) => {
2 | return (
3 |
16 | );
17 | };
18 |
19 | export default Tree;
20 |
--------------------------------------------------------------------------------
/src/testserver/dummy.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "_id": "67fb877de766c949c7a54a37",
4 | "index": 0,
5 | "guid": "f809a75c-4d40-4416-b500-7f1ca560ab8f",
6 | "isActive": false,
7 | "balance": "$2,772.84",
8 | "picture": "http://placehold.it/32x32",
9 | "salary": 21,
10 | "eyeColor": "brown",
11 | "name": "Alford Ashley",
12 | "gender": "male",
13 | "company": "NIPAZ",
14 | "email": "alfordashley@nipaz.com",
15 | "phone": "+1 (828) 503-2560",
16 | "address": "659 Linden Boulevard, Southmont, Alabama, 8543"
17 | },
18 |
19 | {
20 | "_id": "67fb877d165ded4382e0ecd9",
21 | "index": 2,
22 | "guid": "279df46c-3690-4575-a4cc-62693078b50b",
23 | "isActive": false,
24 | "balance": "$1,483.39",
25 | "picture": "http://placehold.it/32x32",
26 | "salary": 30,
27 | "eyeColor": "green",
28 | "name": "Delia Medina",
29 | "gender": "female",
30 | "company": "GENEKOM",
31 | "email": "deliamedina@genekom.com",
32 | "phone": "+1 (820) 415-2907",
33 | "address": "192 Union Street, Waterford, Oregon, 5854"
34 | },
35 | {
36 | "_id": "67fb877dc434d258aa0ae48f",
37 | "index": 3,
38 | "guid": "45baeed3-b4d5-4563-9bca-399f7d0445d1",
39 | "isActive": true,
40 | "balance": "$1,633.62",
41 | "picture": "http://placehold.it/32x32",
42 | "salary": 24,
43 | "eyeColor": "blue",
44 | "name": "Stark Farrell",
45 | "gender": "male",
46 | "company": "POLARIUM",
47 | "email": "starkfarrell@polarium.com",
48 | "phone": "+1 (915) 503-3694",
49 | "address": "528 Bassett Avenue, Frizzleburg, Nebraska, 8538"
50 | }
51 | ]
52 |
--------------------------------------------------------------------------------
/src/vendor/switch/switch.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --switch-accent-color: #4868f7;
3 | --switch-btn-size: 26px;
4 | }
5 |
6 | .checkbox-ui-wrapper {
7 | position: relative;
8 | display: inline-block;
9 | border-radius: 50px;
10 | width: 60px;
11 | border: 1px solid rgb(196, 196, 196);
12 | box-sizing: border-box;
13 | padding: 3px;
14 | height: 30px;
15 | background-color: #efefef;
16 | background-image: linear-gradient(
17 | to right,
18 | var(--switch-accent-color),
19 | var(--switch-accent-color)
20 | );
21 | background-position: 0px 0px;
22 | background-size: 0%;
23 | background-repeat: no-repeat;
24 | transition: background-size 0.2s ease-in;
25 | cursor: pointer;
26 | }
27 |
28 | .checkbox-ui-btn {
29 | box-sizing: border-box;
30 | position: absolute;
31 | width: var(--switch-btn-size);
32 | height: var(--switch-btn-size);
33 | content: '';
34 | display: block;
35 | background: #fff;
36 | border: 1px solid rgb(196, 196, 196);
37 | border-radius: 50%;
38 | left: 0px;
39 | right: initial;
40 | top: 1px;
41 | pointer-events: none;
42 | user-select: none;
43 | transition: left 0.2s ease-in;
44 | }
45 |
46 | .checkbox-ui-checkbox-shadow {
47 | display: none;
48 | }
49 |
50 | .inline-middle {
51 | display: inline-flex;
52 | justify-content: flex-start;
53 | align-items: center;
54 | }
55 |
56 | .checkbox-ui-wrapper > input[type='checkbox']:checked + .checkbox-ui-btn,
57 | .checkbox-ui-wrapper > input[type='radio']:checked + .checkbox-ui-btn {
58 | left: calc(100% - var(--switch-btn-size));
59 | border-color: var(--switch-accent-color) !important;
60 | }
61 |
62 | .checkbox-ui-wrapper:has(> input[type='checkbox']:checked),
63 | .checkbox-ui-wrapper:has(> input[type='radio']:checked) {
64 | background-size: 100%;
65 | }
66 |
--------------------------------------------------------------------------------
/src/vendor/d3-state-visualizer/lib/charts/tree/tree.d.ts:
--------------------------------------------------------------------------------
1 | import type { HierarchyPointNode } from 'd3';
2 | import type { Node } from 'map2tree';
3 | import type { StyleValue } from 'd3tooltip';
4 | export interface Options {
5 | state?: {} | null;
6 | tree?: Node | {};
7 | rootKeyName: string;
8 | pushMethod: 'push' | 'unshift';
9 | id: string;
10 | chartStyles: {
11 | [key: string]: StyleValue;
12 | };
13 | nodeStyleOptions: {
14 | colors: {
15 | default: string;
16 | collapsed: string;
17 | parent: string;
18 | };
19 | radius: number;
20 | };
21 | textStyleOptions: {
22 | colors: {
23 | default: string;
24 | hover: string;
25 | };
26 | };
27 | linkStyles: {
28 | [key: string]: StyleValue;
29 | };
30 | size: number;
31 | aspectRatio: number;
32 | initialZoom: number;
33 | margin: {
34 | top: number;
35 | right: number;
36 | bottom: number;
37 | left: number;
38 | };
39 | isSorted: boolean;
40 | heightBetweenNodesCoeff: number;
41 | widthBetweenNodesCoeff: number;
42 | transitionDuration: number;
43 | blinkDuration: number;
44 | onClickText: (datum: HierarchyPointNode) => void;
45 | tooltipOptions: {
46 | disabled?: boolean;
47 | left?: number | undefined;
48 | top?: number | undefined;
49 | offset?: {
50 | left: number;
51 | top: number;
52 | };
53 | styles?: {
54 | [key: string]: StyleValue;
55 | } | undefined;
56 | indentationSize?: number;
57 | };
58 | }
59 | export interface InternalNode extends Node {
60 | _children?: this[] | undefined;
61 | id: string | number;
62 | }
63 | export default function (DOMNode: HTMLElement, options?: Partial): (nextState?: {} | null | undefined) => void;
64 | export type { Node };
65 |
--------------------------------------------------------------------------------
/src/vendor/d3-state-visualizer/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## 3.0.0
4 |
5 | ### Major Changes
6 |
7 | - 191d419: Convert d3 packages to ESM
8 |
9 | ### Patch Changes
10 |
11 | - Updated dependencies [191d419]
12 | - d3tooltip@4.0.0
13 | - map2tree@4.0.0
14 |
15 | ## 2.0.0
16 |
17 | ### Major Changes
18 |
19 | - b323f77d: Upgrade D3
20 |
21 | - Remove UMD build.
22 | - Split `style` option into `chartStyles`, `nodeStyleOptions`, `textStyleOptions`, and `linkStyles`.
23 | - The shape of the argument passed to the `onClickText` option has been updated.
24 | - Rename `InputOptions` to `Options`, `Primitive` to `StyleValue`, and `NodeWithId` to `HierarchyPointNode`.
25 |
26 | ### Patch Changes
27 |
28 | - Updated dependencies [b323f77d]
29 | - Updated dependencies [b323f77d]
30 | - d3tooltip@3.0.0
31 | - map2tree@3.0.0
32 |
33 | ## [1.4.0](https://github.com/reduxjs/redux-devtools/compare/d3-state-visualizer@1.3.4...d3-state-visualizer@1.4.0) (2021-03-06)
34 |
35 | ### Features
36 |
37 | - **d3-state-visualizer:** convert example to TypeScript ([#641](https://github.com/reduxjs/redux-devtools/issues/641)) ([300b60a](https://github.com/reduxjs/redux-devtools/commit/300b60a8b1f92a6d7c78510a1bea304490aa23be))
38 | - **d3-state-visualizer:** convert to TypeScript ([#640](https://github.com/reduxjs/redux-devtools/issues/640)) ([0c78a5a](https://github.com/reduxjs/redux-devtools/commit/0c78a5a9a76ee7eff37dcd8e39272d98c03e0869))
39 | - **redux-devtools-chart-monitor:** convert to TypeScript ([#642](https://github.com/reduxjs/redux-devtools/issues/642)) ([761baba](https://github.com/reduxjs/redux-devtools/commit/761baba0aa0f4dc672f8771f4b12bed3863557f7))
40 |
41 | ## [1.3.4](https://github.com/reduxjs/redux-devtools/compare/d3-state-visualizer@1.3.3...d3-state-visualizer@1.3.4) (2020-09-07)
42 |
43 | **Note:** Version bump only for package d3-state-visualizer
44 |
45 | ## 1.3.3 (2020-08-14)
46 |
47 | **Note:** Version bump only for package d3-state-visualizer
48 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "json-viewer-pro",
3 | "version": "1.0.6",
4 | "private": true,
5 | "type": "module",
6 | "devDependencies": {
7 | "@eslint/js": "^9.16.0",
8 | "@rollup/plugin-terser": "^0.4.4",
9 | "@types/react": "^18.3.12",
10 | "@types/react-dom": "^18.3.1",
11 | "@vitejs/plugin-react": "^4.3.4",
12 | "autoprefixer": "^10.4.21",
13 | "eslint": "^9.16.0",
14 | "eslint-plugin-react": "^7.37.2",
15 | "eslint-plugin-react-hooks": "^5.0.0",
16 | "eslint-plugin-react-refresh": "^0.4.14",
17 | "globals": "^15.13.0",
18 | "rollup-plugin-postcss": "^4.0.2",
19 | "sass-embedded": "^1.82.0",
20 | "vite": "^6.0.2"
21 | },
22 | "dependencies": {
23 | "@babel/runtime": "^7.26.9",
24 | "@codemirror/lang-css": "^6.3.1",
25 | "@codemirror/lang-json": "^6.0.1",
26 | "@codemirror/lang-xml": "^6.1.0",
27 | "@codemirror/lang-yaml": "^6.1.2",
28 | "@uiw/codemirror-theme-github": "^4.23.10",
29 | "clsx": "^2.1.1",
30 | "codemirror": "^6.0.1",
31 | "d3": "^7.9.0",
32 | "fast-xml-parser": "^5.0.8",
33 | "honey-toast": "^1.0.2",
34 | "jquery": "^3.1.1",
35 | "js-yaml": "^4.1.0",
36 | "jsonpath-plus": "^10.3.0",
37 | "prop-types": "^15.8.1",
38 | "react": "^18.3.1",
39 | "react-dom": "^18.3.1",
40 | "react-icons": "^5.5.0"
41 | },
42 | "scripts": {
43 | "start": "vite",
44 | "dev:option": "vite ./src/options",
45 | "build": "NODE_ENV=production vite build",
46 | "build:option": "NODE_ENV=production vite build -c ./vite.options-page.config.js",
47 | "package": "npm run build && npm run build:option && make package",
48 | "test:units": "node --test './src/utils/*.test.*'",
49 | "test:ctype": "node './src/testserver/index.js'",
50 | "lint": "eslint .",
51 | "preview": "vite preview"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/vendor/d3tooltip/lib/index.js:
--------------------------------------------------------------------------------
1 | import * as d3 from 'd3';
2 | const defaultOptions = {
3 | left: undefined, // mouseX
4 | top: undefined, // mouseY
5 | offset: { left: 0, top: 0 },
6 | root: undefined,
7 | styles: {},
8 | text: '',
9 | };
10 | export function tooltip(className = 'tooltip', options = {}) {
11 | const { left, top, offset, root, styles, text } = {
12 | ...defaultOptions,
13 | ...options,
14 | };
15 | let el;
16 | const anchor = root || d3.select('body');
17 | const rootNode = anchor.node();
18 | return function tip(selection) {
19 | selection.on('mouseover.tip', (event, datum) => {
20 | const [pointerX, pointerY] = d3.pointer(event, rootNode);
21 | const [x, y] = [
22 | left || pointerX + offset.left,
23 | top || pointerY - offset.top,
24 | ];
25 | anchor.selectAll(`div.${className}`).remove();
26 | el = anchor
27 | .append('div')
28 | .attr('class', className)
29 | .style('position', 'absolute')
30 | .style('z-index', 1001)
31 | .style('left', `${x}px`)
32 | .style('top', `${y}px`)
33 | .html(typeof text === 'function' ? () => text(datum) : () => text);
34 | for (const [key, value] of Object.entries(styles)) {
35 | el.style(key, value);
36 | }
37 | });
38 | selection.on('mousemove.tip', (event, datum) => {
39 | const [pointerX, pointerY] = d3.pointer(event, rootNode);
40 | const [x, y] = [
41 | left || pointerX + offset.left,
42 | top || pointerY - offset.top,
43 | ];
44 | el.style('left', `${x}px`)
45 | .style('top', `${y}px`)
46 | .html(typeof text === 'function' ? () => text(datum) : () => text);
47 | });
48 | selection.on('mouseout.tip', () => el.remove());
49 | };
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/Searchbar/style.scss:
--------------------------------------------------------------------------------
1 | .searchbar {
2 | display: flex;
3 | flex-direction: column;
4 | width: 350px;
5 | position: fixed;
6 | background-color: var(--searchbar-bg-color);
7 | top: 15px;
8 | right: 390px;
9 | border-top: 1px solid var(--searchbar-border-color);
10 | border-radius: 10px;
11 | padding: 6px;
12 | box-shadow: 1px 9px 30px 7px var(--searchbar-box-shadow-color);
13 | }
14 |
15 | .search-input-container {
16 | width: 100%;
17 | display: inline-flex;
18 | justify-content: flex-start;
19 | align-items: center;
20 | position: relative;
21 | }
22 |
23 | .search-info-container {
24 | width: 100%;
25 | display: flex;
26 | justify-content: flex-start;
27 | align-items: center;
28 | padding: 10px;
29 | font-size: 14px;
30 | color: var(--searchbar-info-text-color);
31 | }
32 |
33 | .search-input {
34 | background-color: var(--searchbar-text-input-bg-color);
35 | padding: 8px 10px;
36 | border-radius: 6px;
37 | border: 1px solid var(--searchbar-text-input-border-color);
38 | color: var(--searchbar-text-input-text-color);
39 | font-size: 14px;
40 | font-weight: bold;
41 | width: 350px;
42 | padding-right: 50px;
43 | box-sizing: border-box;
44 | font-family: monospace;
45 | }
46 |
47 | .search-input:focus {
48 | outline: 1px solid var(--searchbar-text-input-focus-outline-color);
49 | }
50 |
51 | .search-clear-btn {
52 | padding: 4px 4px 0px 4px;
53 | color: var(--search-clear-btn-text-color);
54 | margin-left: -34px;
55 | border-radius: 6px;
56 | box-sizing: border-box;
57 | border-left: none;
58 | font-size: 22px;
59 | background-color: var(--search-clear-btn-bg-color);
60 | cursor: pointer;
61 | }
62 |
63 | .path-suggestions {
64 | position: absolute;
65 | width: 280px;
66 | top: 40px;
67 | left: 40px;
68 |
69 | .path-autocompletion {
70 | .dropdown-list-items {
71 | top: 0px;
72 | font-family: monospace;
73 | max-height: 240px;
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/components/Select/index.jsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useMemo, useState } from "react"
2 | import PropTypes from 'prop-types';
3 |
4 | import DropDown from "../Dropdown";
5 |
6 |
7 | const findSelectedLabel = (label, items) => {
8 | if (label) {
9 | const matchedLabelItem = items.find((item) => item.label === label);
10 | if (matchedLabelItem) {
11 | return matchedLabelItem.label;
12 | }
13 | return label;
14 | }
15 | const selectedItem = items.find(({selected}) => !!selected);
16 | return !!selectedItem ? selectedItem.label: items?.[0]?.label ?? '';
17 | };
18 |
19 | const Select = ({
20 | onChange = () => {},
21 | size,
22 | label,
23 | items,
24 | ...props
25 | }) => {
26 | const [menuItems, setMenuItems] = useState(items);
27 |
28 | const onClick = useCallback((value) => {
29 | onChange(value);
30 | }, [onChange]);
31 |
32 | let selectedLabel = findSelectedLabel(label, menuItems);
33 |
34 | useEffect(() => {
35 | const modifiedListeItems = items.map(item => {
36 | item.onClick = onClick;
37 | if (item.label === label) {
38 | item.selected = true;
39 | } else {
40 | item.selected = false;
41 | }
42 | return item;
43 | });
44 | setMenuItems(modifiedListeItems);
45 | selectedLabel = findSelectedLabel(label, menuItems);
46 | }, [label, items]);
47 |
48 | return
49 | }
50 |
51 |
52 | Select.propTypes = {
53 | size: PropTypes.string,
54 | className: PropTypes.string,
55 | labelIcon: PropTypes.node,
56 | hasCaretIcon: PropTypes.string,
57 | open: PropTypes.bool,
58 | label: PropTypes.string,
59 | items: PropTypes.arrayOf(PropTypes.shape({
60 | label: PropTypes.string,
61 | iconUrl: PropTypes.string,
62 | onClick: PropTypes.func
63 | })),
64 | onChange: PropTypes.func,
65 | onClose: PropTypes.func,
66 | };
67 |
68 |
69 | export default Select;
70 |
--------------------------------------------------------------------------------
/src/components/Icons/Copy.jsx:
--------------------------------------------------------------------------------
1 | const Copy = ({ className, fillColor = '#ffffff' }) => {
2 | return (
3 |
27 | );
28 | };
29 |
30 | export default Copy;
31 |
--------------------------------------------------------------------------------
/src/utils/common.test.js:
--------------------------------------------------------------------------------
1 | import assert from 'node:assert/strict';
2 | import { describe, it } from 'node:test';
3 |
4 | import { parseJson } from './common.js';
5 |
6 | const getValidDummyJSON = () => {
7 | return `{
8 | "_id": "67e4330f7592be6a1d6b017b",
9 | "index": 0,
10 | "guid": "6a216e78-d103-426b-aa14-dc1d775e1513",
11 | "isActive": false,
12 | "balance": "$1,162.00",
13 | "picture": "http://placehold.it/32x32",
14 | "salary": 39,
15 | "eyeColor": "brown",
16 | "name": "Hopkins Stark",
17 | "gender": "male",
18 | "company": "NETUR",
19 | "email": "hopkinsstark@netur.com",
20 | "phone": "+1 (815) 542-3160",
21 | "address": "618 Stockholm Street, Darrtown, Arizona, 8470"
22 | }`;
23 | };
24 |
25 | const getValidDummyJSONWithLargeNumber = () => {
26 | return `{
27 | "largeNumberA": 149883901923910003,
28 | "largeNumberWithFloatValue": 149883901923910003.119,
29 | "largeNumberX": "114988390192391005",
30 | "more": {
31 | "largeNumberB": 149883901923910004
32 | }
33 | }`;
34 | };
35 |
36 | describe('parseJson', () => {
37 | it('should test parseJson parses a Json string', () => {
38 | const dummyJson = getValidDummyJSON();
39 | assert.doesNotThrow(() => {
40 | parseJson(dummyJson);
41 | });
42 | const result = parseJson(dummyJson);
43 | assert.deepEqual(result, JSON.parse(dummyJson));
44 | });
45 |
46 | it('should parse Json string and convert Big Number to string', () => {
47 | const dummyJson = getValidDummyJSONWithLargeNumber();
48 | assert.doesNotThrow(() => {
49 | parseJson(dummyJson);
50 | });
51 | const expectedObj = {
52 | largeNumberA: '149883901923910003',
53 | largeNumberWithFloatValue: '149883901923910003.119',
54 | largeNumberX: '114988390192391005',
55 | more: {
56 | largeNumberB: '149883901923910004',
57 | },
58 | };
59 | const result = parseJson(dummyJson);
60 | assert.deepEqual(result, expectedObj);
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/src/css/color-themes/mdn-light.css:
--------------------------------------------------------------------------------
1 | :root{
2 | --bg-color:#fff;
3 | --primary-text-color: #111;
4 | --breadcumb-border-color: #ccc;
5 | --breadcumb-bg-color: #f6f6f6;
6 | --copy-path-btn-img-filter: invert(1);
7 | --menu-item-img-filter: invert(1);
8 |
9 | --menu-item-border-color: #ccc;
10 | --menu-item-bg-color: #f6f6f6;
11 | --menu-item-active-border-color: #796cd9;
12 | --menu-item-active-bg-color: #E9EDFE;
13 | --breadcumb-item-hover-color: #796cd9;
14 | --breadcumb-arrow-color: #796cd9;
15 |
16 | --copyable-property-bg-color: rgba(231, 232, 233, 0.8);
17 | --copyable-property-border-color: #5d6d7e;
18 | --btn-bg-color: #fff;
19 | --btn-border-color: #ccc;
20 | --btn-default-bg-color: #fff;
21 | --btn-primary-bg-color: #433387;
22 |
23 | /* JSON Tree Color Scheme*/
24 | --property-color: #994c9e;
25 | --json-literal-numeric: #f5b041;
26 | --json-literal-url: #34a632;
27 | --json-literal-string: #0642b0;
28 | --json-literal-and-boolean: #f23ebb;
29 |
30 | --copier-bg-color: #fff;
31 | --copier-text-color: #2ecc71;
32 |
33 | /* chart view*/
34 | --json-chart-circle-bg: #01ff70;
35 |
36 | /* editor */
37 | /* --json-input-bg-color: white; */
38 | --json-input-border-color: #ccc;
39 | --error-msg-bg-color: #1f0604;
40 | --editor-footer-bg-color: #efefef;
41 | --editor-footer-text-color: #868585;
42 | --editor-footer-border-color: #ccc;
43 |
44 | /* dropdown */
45 | --dropdown-bg-primary: #fff;
46 | --dropdown-menu-item-bg: #efefef;
47 | --dropdown-menu-item-border-color: #ccc;
48 | --dropdown-menu-border-color: #ccc;
49 |
50 | /* json path search bar */
51 | --searchbar-bg-color:#ffffff;
52 | --searchbar-border-color:#ccc;
53 | --searchbar-box-shadow-color:rgba(135, 133, 133, 0.4);
54 | --searchbar-text-input-bg-color: #efefef;
55 | --searchbar-text-input-border-color: #ccc;
56 | --searchbar-text-input-text-color: #994c9e;
57 | --searchbar-text-input-focus-outline-color: #ccc;
58 | --searchbar-info-text-color: #565454;
59 | --search-clear-btn-text-color: var(--primary-text-color);
60 | --search-clear-btn-bg-color: transparent;
61 |
62 | /* common */
63 | --toolbar-bg-color: #efefef;
64 | --toolbar-border-color: #ccc;
65 | }
66 |
--------------------------------------------------------------------------------
/src/components/Icons/Branch.jsx:
--------------------------------------------------------------------------------
1 | const Branch = ({ className, fillColor = '#ffffff' }) => {
2 | return (
3 |
19 | );
20 | };
21 |
22 | export default Branch;
23 |
--------------------------------------------------------------------------------
/src/vendor/map2tree/lib/index.js:
--------------------------------------------------------------------------------
1 | import { isArray, isPlainObject, mapValues } from 'lodash-es';
2 | function visit(parent, visitFn, childrenFn) {
3 | if (!parent)
4 | return;
5 | visitFn(parent);
6 | const children = childrenFn(parent);
7 | if (children) {
8 | const count = children.length;
9 | for (let i = 0; i < count; i++) {
10 | visit(children[i], visitFn, childrenFn);
11 | }
12 | }
13 | }
14 | function getNode(tree, key) {
15 | let node = null;
16 | visit(tree, (d) => {
17 | if (d.name === key) {
18 | node = d;
19 | }
20 | }, (d) => d.children);
21 | return node;
22 | }
23 | export function map2tree(root, options = {}, tree = { name: options.key || 'state', children: [] }) {
24 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type
25 | if (!isPlainObject(root) && root && !root.toJS) {
26 | return {};
27 | }
28 | const { key: rootNodeKey = 'state', pushMethod = 'push' } = options;
29 | const currentNode = getNode(tree, rootNodeKey);
30 | if (currentNode === null) {
31 | return {};
32 | }
33 | mapValues(
34 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type
35 | root && root.toJS
36 | ? // eslint-disable-next-line @typescript-eslint/no-empty-object-type
37 | root.toJS()
38 | : // eslint-disable-next-line @typescript-eslint/no-empty-object-type
39 | root,
40 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type
41 | (maybeImmutable, key) => {
42 | const value = maybeImmutable && maybeImmutable.toJS
43 | ? maybeImmutable.toJS()
44 | : maybeImmutable;
45 | const newNode = { name: key };
46 | if (isArray(value)) {
47 | newNode.children = [];
48 | for (let i = 0; i < value.length; i++) {
49 | newNode.children[pushMethod]({
50 | name: `${key}[${i}]`,
51 | [isPlainObject(value[i]) ? 'object' : 'value']: value[i],
52 | });
53 | }
54 | }
55 | else if (isPlainObject(value)) {
56 | newNode.children = [];
57 | }
58 | else {
59 | newNode.value = value;
60 | }
61 | currentNode.children[pushMethod](newNode);
62 | map2tree(value, { key, pushMethod }, tree);
63 | });
64 | return tree;
65 | }
66 |
--------------------------------------------------------------------------------
/src/components/Dropdown/style.scss:
--------------------------------------------------------------------------------
1 |
2 | .dropdown {
3 | display: inline-block;
4 | background-color: var(--dropdown-bg-primary);
5 | border: 1px solid var(--dropdown-menu-border-color);
6 | position: relative;
7 | width: 100%;
8 | border-radius: 4px;
9 | cursor: pointer;
10 | user-select: none;
11 |
12 | &-btn {
13 | position: relative;
14 | padding: 5px;
15 | font-size: 14px;
16 |
17 | .dropdown-caret-bottom-icon {
18 | padding-left:10px;
19 | display: inline;
20 | align-content: flex-end;
21 | float: right;
22 | }
23 | }
24 |
25 | img.selected-icon {
26 | margin-right: 10px;
27 | width: 20px;
28 | height: 20px;
29 | }
30 |
31 | .selected-label {
32 | width: 100%;
33 | }
34 |
35 | ul.list-items {
36 | width: 100%;
37 | position: absolute;
38 | top: 38px;
39 | right: 0px;
40 | list-style: none;
41 | margin: 0px;
42 | padding: 0px;
43 | background: var(--dropdown-menu-item-bg);
44 | border: 1px solid var(--dropdown-menu-item-border-color);
45 | border-radius: 4px;
46 | display: none;
47 | z-index: 99;
48 | box-shadow: 0px 3px 6px rgba($color: #000000, $alpha: 0.6);
49 | max-height: 40vh;
50 | overflow-y: scroll;
51 | scrollbar-width: none;
52 |
53 | li {
54 | padding: 10px 15px;
55 | border-bottom: 1px solid var(--dropdown-menu-item-border-color);
56 | cursor: pointer;
57 | font-size: 15px;
58 | display: flex;
59 | justify-content: flex-start;
60 | align-items: center;
61 | background-color: var(--dropdown-bg-primary);
62 |
63 | .icon {
64 | width: 18px;
65 | height: 18px;
66 | margin-right: 10px;
67 | }
68 | .selected-icon {
69 | display: inline-block;
70 | width: 16px;
71 | margin-left: 10px;
72 | }
73 | }
74 |
75 | li:hover, li.active {
76 | background: var(--dropdown-menu-item-bg);
77 | }
78 | }
79 |
80 |
81 | &.expanded {
82 | .select-icon {
83 | transform: rotate(180deg);
84 | }
85 |
86 | ul.list-items {
87 | display: block;
88 | }
89 | ul.list-items:focus {
90 | outline: none;
91 | }
92 | }
93 | }
94 |
95 | .dropdown-btn{
96 | overflow: hidden;
97 | }
98 |
99 |
--------------------------------------------------------------------------------
/src/testserver/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Test Content Type
7 |
38 |
39 |
40 |
41 |
42 |
Test Content-Type:
43 |
44 | prerequisite: Install/Enable JSON Viewer Pro Chrome
45 | Extension
46 |
47 |
48 |
49 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/src/css/color-themes/dark-pro.css:
--------------------------------------------------------------------------------
1 | /*
2 | * !IMPORTANT!
3 | * dart-pro.scss contains all the color variables and considered as base color-scheme.
4 | * To create a new color schemes simply create a new YOUR_SCHEME.scss file and overwrite the color variables that dark-pro.scss offers.
5 | */
6 |
7 | :root{
8 | --bg-color:#11101E;
9 | --primary-text-color: #fff;
10 | --primary-font-family: sans-serif;
11 | --breadcumb-border-color: rgb(45, 42, 57);
12 | --breadcumb-bg-color: #1A1B28;
13 | --copy-path-btn-img-filter: none;
14 | --base-font-size: 15px;
15 | --breadcumb-item-hover-color: #4fdee5;
16 | --breadcumb-arrow-color: #01ff70;
17 | --menu-item-border-color: #292938;
18 | --menu-item-bg-color: #1A1B28;
19 | --menu-item-active-border-color: rgb(105 75 226);
20 | --menu-item-active-bg-color: #433387;
21 | --menu-item-img-filter: none;
22 | --copyable-property-bg-color: rgba(33, 47, 61, 0.8);
23 | --copyable-property-border-color: #5d6d7e;
24 | --btn-bg-color: #33354a;
25 | --btn-border-color: #111;
26 | --btn-default-bg-color: #1A1B28;
27 | --btn-primary-bg-color: #433387;
28 | --btn-primary-text-color: white;
29 |
30 | /* JSON Tree Color Scheme*/
31 | --property-color: #4fdee5;
32 | --json-literal-numeric: #f5b041;
33 | --json-literal-string: #01ff70;
34 | --json-literal-url: #0184ff;
35 | --json-literal-and-boolean: #af7ac5;
36 | --copier-bg-color: #2ecc71;
37 | --copier-text-color: #2ecc71;
38 |
39 | /* chart view*/
40 | --json-chart-circle-bg: #01ff70;
41 |
42 | /* editor */
43 | --json-input-bg-color: #070707;
44 | --json-input-border-color: #363537;
45 | --error-msg-bg-color: #1f0604;
46 | --editor-footer-bg-color: #111;
47 | --editor-footer-text-color: #868585;
48 | --editor-footer-border-color: #222;
49 |
50 | /* dropdown */
51 | --dropdown-bg-primary: #1A1B28;
52 | --dropdown-menu-item-bg: #43455d;
53 | --dropdown-menu-item-border-color: #222;
54 | --dropdown-menu-border-color: #111;
55 |
56 | /* json path search bar */
57 | --searchbar-bg-color:rgba(29, 27, 61, 1);
58 | --searchbar-border-color:#111111;
59 | --searchbar-box-shadow-color:rgba(6, 6, 6, 0.7);
60 | --searchbar-text-input-bg-color: #000000;
61 | --searchbar-text-input-border-color: #222;
62 | --searchbar-text-input-text-color: #01ff70;
63 | --searchbar-text-input-focus-outline-color: #4a4173;
64 | --searchbar-info-text-color: #d8d6d6;
65 | --search-clear-btn-text-color: #4a4173;
66 | --search-clear-btn-bg-color: #000000;
67 |
68 | /* common */
69 | --toolbar-bg-color: #000;
70 | --toolbar-border-color: #222;
71 | }
72 |
--------------------------------------------------------------------------------
/src/vendor/d3-state-visualizer/lib/charts/tree/utils.js:
--------------------------------------------------------------------------------
1 | import { is, join, pipe, replace } from 'ramda';
2 | import sortAndSerialize from './sortAndSerialize.js';
3 | export function collapseChildren(node) {
4 | if (node.children) {
5 | node._children = node.children;
6 | node._children.forEach(collapseChildren);
7 | node.children = undefined;
8 | }
9 | }
10 | export function expandChildren(node) {
11 | if (node._children) {
12 | node.children = node._children;
13 | node.children.forEach(expandChildren);
14 | node._children = undefined;
15 | }
16 | }
17 | export function toggleChildren(node) {
18 | if (node.children) {
19 | node._children = node.children;
20 | node.children = undefined;
21 | }
22 | else if (node._children) {
23 | node.children = node._children;
24 | node._children = undefined;
25 | }
26 | return node;
27 | }
28 | export function visit(parent, visitFn, childrenFn) {
29 | if (!parent) {
30 | return;
31 | }
32 | visitFn(parent);
33 | const children = childrenFn(parent);
34 | if (children) {
35 | const count = children.length;
36 | for (let i = 0; i < count; i++) {
37 | visit(children[i], visitFn, childrenFn);
38 | }
39 | }
40 | }
41 | export function getNodeGroupByDepthCount(rootNode) {
42 | const nodeGroupByDepthCount = [1];
43 | const traverseFrom = function traverseFrom(node, depth = 0) {
44 | if (!node.children || node.children.length === 0) {
45 | return 0;
46 | }
47 | if (nodeGroupByDepthCount.length <= depth + 1) {
48 | nodeGroupByDepthCount.push(0);
49 | }
50 | nodeGroupByDepthCount[depth + 1] += node.children.length;
51 | node.children.forEach((childNode) => {
52 | traverseFrom(childNode, depth + 1);
53 | });
54 | };
55 | traverseFrom(rootNode);
56 | return nodeGroupByDepthCount;
57 | }
58 | export function getTooltipString(node, { indentationSize = 4 }) {
59 | if (!is(Object, node))
60 | return '';
61 | const spacer = join(' ');
62 | const cr2br = replace(/\n/g, '
');
63 | const spaces2nbsp = replace(/\s{2}/g, spacer(new Array(indentationSize)));
64 | const json2html = pipe(sortAndSerialize, cr2br, spaces2nbsp);
65 | const children = node.children || node._children;
66 | if (typeof node.value !== 'undefined')
67 | return json2html(node.value);
68 | if (typeof node.object !== 'undefined')
69 | return json2html(node.object);
70 | if (children && children.length)
71 | return `childrenCount: ${children.length}`;
72 | return 'empty';
73 | }
74 |
--------------------------------------------------------------------------------
/src/components/Editor/Toolbar/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Select from '../../Select';
3 | import { FiUploadCloud, FiFileText, FiPlay, FiSave } from 'react-icons/fi';
4 |
5 | import './style.css';
6 |
7 | const Toolbar = ({
8 | onImportBtnClick,
9 | onParseJson,
10 | onContentTypeChange,
11 | onSaveBtnClick,
12 | contentType,
13 | }) => {
14 | const contentTypMenuItems = {
15 | size: 'small',
16 | label: contentType,
17 | items: [
18 | {
19 | label: 'JSON',
20 | iconUrl: null,
21 | selected: true,
22 | },
23 | {
24 | label: 'YAML',
25 | iconUrl: null,
26 | },
27 | {
28 | label: 'XML',
29 | iconUrl: null,
30 | },
31 | ],
32 | };
33 | return (
34 |
35 |
36 |
39 | }
42 | onChange={onContentTypeChange}
43 | className="jv-content-type-btn"
44 | />
45 |
46 |
56 |
66 |
67 |
68 |
78 |
79 |
80 | );
81 | };
82 |
83 | export default Toolbar;
84 |
--------------------------------------------------------------------------------
/src/testserver/index.js:
--------------------------------------------------------------------------------
1 | import { createServer } from 'node:http';
2 | import { readFile } from 'fs/promises';
3 |
4 | const createURLFromReqURl = (urlPart) => {
5 | return new URL(`http://localhost${urlPart}`);
6 | };
7 |
8 | const proecesRequests = (routes, req, res) => {
9 | const url = createURLFromReqURl(req.url);
10 | let found = false;
11 | routes = routes.sort((a, b) => b - a);
12 | routes.forEach(({ path, method, cb }) => {
13 | if (method === req.method && url.pathname == path) {
14 | found = true;
15 | return cb({ ...req, parsedUrl: url }, res);
16 | }
17 | });
18 | if (!found) {
19 | res.writeHead(404, { 'Content-Type': 'text/html' });
20 | const reqObj = JSON.stringify(url, null, 4);
21 | res.end(
22 | `404: Notfound
No route has been registered
${reqObj}`,
23 | );
24 | }
25 | };
26 |
27 | const initialiseServer = () => {
28 | const registeredRoutes = [];
29 |
30 | const requestHandler = async (req, res) => {
31 | proecesRequests(registeredRoutes, req, res);
32 | };
33 |
34 | const server = createServer(requestHandler);
35 |
36 | const utils = {
37 | registerPath(method, path, cb) {
38 | registeredRoutes.push({
39 | path,
40 | method,
41 | cb,
42 | });
43 | },
44 | get(path, cb) {
45 | this.registerPath('GET', path, cb);
46 | },
47 | post(path, cb) {
48 | this.registerPath('POST', path, cb);
49 | },
50 | listen(port, host, cb) {
51 | server.listen(port, host, cb);
52 | },
53 | };
54 | return utils;
55 | };
56 |
57 | const app = initialiseServer();
58 |
59 | const render = async (res, filePath, statusCode = 200, ctype = 'text/html') => {
60 | const data = await readFile(filePath, {
61 | encoding: 'utf8',
62 | });
63 | res.writeHead(statusCode, { 'Content-Type': ctype });
64 | res.end(data);
65 | };
66 |
67 | app.get('/', (_, res) => {
68 | return render(res, './src/testserver/index.html');
69 | });
70 |
71 | app.get('/api', async (req, res) => {
72 | const data = await readFile('./src/testserver/dummy.json', {
73 | encoding: 'utf8',
74 | });
75 |
76 | const contentType = {
77 | 'Content-Type':
78 | decodeURIComponent(req.parsedUrl.searchParams?.get('ctype')) ||
79 | 'text/html',
80 | };
81 | res.writeHead(200, contentType);
82 | const dataToRespond = {
83 | ...contentType,
84 | data: JSON.parse(data),
85 | };
86 | return res.end(JSON.stringify(dataToRespond, null, 4));
87 | });
88 |
89 | app.listen(3000, '127.0.0.1', () => {
90 | console.log('Listening on http://127.0.0.1:3000');
91 | });
92 |
--------------------------------------------------------------------------------
/src/vendor/d3-state-visualizer/src/charts/tree/utils.ts:
--------------------------------------------------------------------------------
1 | import { is, join, pipe, replace } from 'ramda';
2 | import sortAndSerialize from './sortAndSerialize.js';
3 | import type { InternalNode } from './tree.js';
4 |
5 | export function collapseChildren(node: InternalNode) {
6 | if (node.children) {
7 | node._children = node.children;
8 | node._children.forEach(collapseChildren);
9 | node.children = undefined;
10 | }
11 | }
12 |
13 | export function expandChildren(node: InternalNode) {
14 | if (node._children) {
15 | node.children = node._children;
16 | node.children.forEach(expandChildren);
17 | node._children = undefined;
18 | }
19 | }
20 |
21 | export function toggleChildren(node: InternalNode) {
22 | if (node.children) {
23 | node._children = node.children;
24 | node.children = undefined;
25 | } else if (node._children) {
26 | node.children = node._children;
27 | node._children = undefined;
28 | }
29 | return node;
30 | }
31 |
32 | export function visit(
33 | parent: InternalNode,
34 | visitFn: (parent: InternalNode) => void,
35 | childrenFn: (parent: InternalNode) => InternalNode[] | null | undefined,
36 | ) {
37 | if (!parent) {
38 | return;
39 | }
40 |
41 | visitFn(parent);
42 |
43 | const children = childrenFn(parent);
44 | if (children) {
45 | const count = children.length;
46 |
47 | for (let i = 0; i < count; i++) {
48 | visit(children[i], visitFn, childrenFn);
49 | }
50 | }
51 | }
52 |
53 | export function getNodeGroupByDepthCount(rootNode: InternalNode) {
54 | const nodeGroupByDepthCount = [1];
55 |
56 | const traverseFrom = function traverseFrom(node: InternalNode, depth = 0) {
57 | if (!node.children || node.children.length === 0) {
58 | return 0;
59 | }
60 |
61 | if (nodeGroupByDepthCount.length <= depth + 1) {
62 | nodeGroupByDepthCount.push(0);
63 | }
64 |
65 | nodeGroupByDepthCount[depth + 1] += node.children.length;
66 |
67 | node.children.forEach((childNode) => {
68 | traverseFrom(childNode, depth + 1);
69 | });
70 | };
71 |
72 | traverseFrom(rootNode);
73 | return nodeGroupByDepthCount;
74 | }
75 |
76 | export function getTooltipString(node: InternalNode, { indentationSize = 4 }) {
77 | if (!is(Object, node)) return '';
78 |
79 | const spacer = join(' ');
80 | const cr2br = replace(/\n/g, '
');
81 | const spaces2nbsp = replace(/\s{2}/g, spacer(new Array(indentationSize)));
82 | const json2html = pipe(sortAndSerialize, cr2br, spaces2nbsp);
83 |
84 | const children = node.children || node._children;
85 |
86 | if (typeof node.value !== 'undefined') return json2html(node.value);
87 | if (typeof node.object !== 'undefined') return json2html(node.object);
88 | if (children && children.length) return `childrenCount: ${children.length}`;
89 | return 'empty';
90 | }
91 |
--------------------------------------------------------------------------------
/src/components/Icons/Brackets.jsx:
--------------------------------------------------------------------------------
1 | const Brackets = ({ className, fillColor = '#ffffff' }) => {
2 | return (
3 |
33 | );
34 | };
35 |
36 | export default Brackets;
37 |
--------------------------------------------------------------------------------
/src/vendor/map2tree/src/index.ts:
--------------------------------------------------------------------------------
1 | import { isArray, isPlainObject, mapValues } from 'lodash-es';
2 |
3 | export interface Node {
4 | name: string;
5 | children?: this[];
6 | object?: unknown;
7 | value?: unknown;
8 | }
9 |
10 | function visit(
11 | parent: Node,
12 | visitFn: (parent: Node) => void,
13 | childrenFn: (parent: Node) => Node[] | undefined | null,
14 | ) {
15 | if (!parent) return;
16 |
17 | visitFn(parent);
18 |
19 | const children = childrenFn(parent);
20 | if (children) {
21 | const count = children.length;
22 | for (let i = 0; i < count; i++) {
23 | visit(children[i], visitFn, childrenFn);
24 | }
25 | }
26 | }
27 |
28 | function getNode(tree: Node, key: string): Node | null {
29 | let node = null;
30 |
31 | visit(
32 | tree,
33 | (d) => {
34 | if (d.name === key) {
35 | node = d;
36 | }
37 | },
38 | (d) => d.children,
39 | );
40 |
41 | return node;
42 | }
43 |
44 | export function map2tree(
45 | root: unknown,
46 | options: { key?: string; pushMethod?: 'push' | 'unshift' } = {},
47 | tree: Node = { name: options.key || 'state', children: [] },
48 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type
49 | ): Node | {} {
50 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type
51 | if (!isPlainObject(root) && root && !(root as { toJS: () => {} }).toJS) {
52 | return {};
53 | }
54 |
55 | const { key: rootNodeKey = 'state', pushMethod = 'push' } = options;
56 | const currentNode = getNode(tree, rootNodeKey);
57 |
58 | if (currentNode === null) {
59 | return {};
60 | }
61 |
62 | mapValues(
63 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type
64 | root && (root as { toJS: () => {} }).toJS
65 | ? // eslint-disable-next-line @typescript-eslint/no-empty-object-type
66 | (root as { toJS: () => {} }).toJS()
67 | : // eslint-disable-next-line @typescript-eslint/no-empty-object-type
68 | (root as {}),
69 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type
70 | (maybeImmutable: { toJS?: () => {} }, key) => {
71 | const value =
72 | maybeImmutable && maybeImmutable.toJS
73 | ? maybeImmutable.toJS()
74 | : maybeImmutable;
75 | const newNode: Node = { name: key };
76 |
77 | if (isArray(value)) {
78 | newNode.children = [];
79 |
80 | for (let i = 0; i < value.length; i++) {
81 | newNode.children[pushMethod]({
82 | name: `${key}[${i}]`,
83 | [isPlainObject(value[i]) ? 'object' : 'value']: value[i],
84 | });
85 | }
86 | } else if (isPlainObject(value)) {
87 | newNode.children = [];
88 | } else {
89 | newNode.value = value;
90 | }
91 |
92 | currentNode.children;
93 |
94 | map2tree(value, { key, pushMethod }, tree);
95 | },
96 | );
97 |
98 | return tree;
99 | }
100 |
--------------------------------------------------------------------------------
/src/components/Icons/Gear.jsx:
--------------------------------------------------------------------------------
1 | const Gear = ({ className, fillColor = '#ffffff' }) => {
2 | return (
3 |
25 | );
26 | };
27 |
28 | export default Gear;
29 |
--------------------------------------------------------------------------------
/src/vendor/d3tooltip/src/index.ts:
--------------------------------------------------------------------------------
1 | import * as d3 from 'd3';
2 | import type { BaseType, Selection } from 'd3';
3 |
4 | export type StyleValue = string | number | boolean;
5 |
6 | interface Options<
7 | Datum,
8 | RootGElement extends BaseType,
9 | RootDatum,
10 | RootPElement extends BaseType,
11 | RootPDatum,
12 | > {
13 | left: number | undefined;
14 | top: number | undefined;
15 | offset: {
16 | left: number;
17 | top: number;
18 | };
19 | root:
20 | | Selection
21 | | undefined;
22 | styles: { [key: string]: StyleValue };
23 | text: string | ((datum: Datum) => string);
24 | }
25 |
26 | const defaultOptions: Options = {
27 | left: undefined, // mouseX
28 | top: undefined, // mouseY
29 | offset: { left: 0, top: 0 },
30 | root: undefined,
31 | styles: {},
32 | text: '',
33 | };
34 |
35 | export function tooltip<
36 | GElement extends BaseType,
37 | Datum,
38 | PElement extends BaseType,
39 | PDatum,
40 | RootGElement extends BaseType,
41 | RootDatum,
42 | RootPElement extends BaseType,
43 | RootPDatum,
44 | >(
45 | className = 'tooltip',
46 | options: Partial<
47 | Options
48 | > = {},
49 | ) {
50 | const { left, top, offset, root, styles, text } = {
51 | ...defaultOptions,
52 | ...options,
53 | } as Options;
54 |
55 | let el: Selection;
56 | const anchor: Selection<
57 | RootGElement,
58 | RootDatum,
59 | RootPElement | HTMLElement,
60 | RootPDatum
61 | > = root || d3.select('body');
62 | const rootNode = anchor.node()!;
63 |
64 | return function tip(selection: Selection) {
65 | selection.on('mouseover.tip', (event, datum) => {
66 | const [pointerX, pointerY] = d3.pointer(event, rootNode);
67 | const [x, y] = [
68 | left || pointerX + offset.left,
69 | top || pointerY - offset.top,
70 | ];
71 |
72 | anchor.selectAll(`div.${className}`).remove();
73 |
74 | el = anchor
75 | .append('div')
76 | .attr('class', className)
77 | .style('position', 'absolute')
78 | .style('z-index', 1001)
79 | .style('left', `${x}px`)
80 | .style('top', `${y}px`)
81 | .html(typeof text === 'function' ? () => text(datum) : () => text);
82 |
83 | for (const [key, value] of Object.entries(styles)) {
84 | el.style(key, value);
85 | }
86 | });
87 |
88 | selection.on('mousemove.tip', (event, datum) => {
89 | const [pointerX, pointerY] = d3.pointer(event, rootNode);
90 | const [x, y] = [
91 | left || pointerX + offset.left,
92 | top || pointerY - offset.top,
93 | ];
94 |
95 | el.style('left', `${x}px`)
96 | .style('top', `${y}px`)
97 | .html(typeof text === 'function' ? () => text(datum) : () => text);
98 | });
99 |
100 | selection.on('mouseout.tip', () => el.remove());
101 | };
102 | }
103 |
--------------------------------------------------------------------------------
/src/scripts/background.js:
--------------------------------------------------------------------------------
1 | const dbName = 'rb-awesome-json-viewer-options';
2 |
3 | chrome.action.onClicked.addListener(async (tab) => {
4 | const extensionOptions = await getBackwardCompatibleOptions(dbName);
5 | let queryParam = '';
6 | if (extensionOptions && extensionOptions.theme) {
7 | queryParam = `?options=${encodeURIComponent(
8 | JSON.stringify(extensionOptions),
9 | )}`;
10 | }
11 | const url = chrome.runtime.getURL('index.html');
12 | chrome.tabs.create(
13 | { url: queryParam ? url + queryParam : url },
14 | function (tab) {},
15 | );
16 | });
17 |
18 | const RB_DOWNLOAD_JSON_MENU = 'RB_DOWNLOAD_JSON_MENU';
19 | const RB_OPEN_SETTINGS = 'RB_OPEN_SETTINGS';
20 |
21 | function genericOnClick(info, tab) {
22 | switch (info.menuItemId) {
23 | case RB_DOWNLOAD_JSON_MENU:
24 | chrome.tabs.sendMessage(tab.id, { action: 'rb_download_json' });
25 | break;
26 | case RB_OPEN_SETTINGS:
27 | chrome.tabs.create({
28 | url: chrome.runtime.getURL('options.html'),
29 | });
30 | break;
31 | }
32 | }
33 |
34 | chrome.contextMenus.onClicked.addListener(genericOnClick);
35 |
36 | const createContextMenu = async () => {
37 | try {
38 | await chrome.contextMenus.removeAll();
39 | } catch (error) {
40 | console.log('Context Menu related Error Found:', error);
41 | } finally {
42 | chrome.contextMenus.create({
43 | id: RB_DOWNLOAD_JSON_MENU,
44 | title: 'Download JSON',
45 | contexts: ['all'],
46 | type: 'normal',
47 | documentUrlPatterns: ['*://*/*'],
48 | });
49 |
50 | chrome.contextMenus.create({
51 | id: RB_OPEN_SETTINGS,
52 | title: 'Settings',
53 | contexts: ['all'],
54 | type: 'normal',
55 | documentUrlPatterns: ['*://*/*'],
56 | });
57 | }
58 | };
59 |
60 | const getBackwardCompatibleOptions = async (key) => {
61 | try {
62 | const data = await chrome.storage.local.get([key]);
63 | const existingData = data[key];
64 | if (existingData && typeof existingData === 'string') {
65 | try {
66 | const parsedData = JSON.parse(existingData);
67 | if (parsedData && Object.keys(parsedData).length > 0) {
68 | return parsedData;
69 | }
70 | } catch (error) {
71 | console.log('Error while parsing the existing options:', error);
72 | return;
73 | }
74 | }
75 | return existingData;
76 | } catch (e) {
77 | console.error("Your browser doesn't support chrome storage api", e);
78 | }
79 | return;
80 | };
81 |
82 | const sendOptions = async () => {
83 | let options = await getBackwardCompatibleOptions(dbName);
84 | if (!options) {
85 | options = {};
86 | }
87 |
88 | options = {
89 | ...{
90 | theme: 'default',
91 | css: '',
92 | collapsed: 0,
93 | filteredURL: [],
94 | jsonDetection: {
95 | method: 'contentType',
96 | selectedContentTypes: [
97 | 'application/json',
98 | 'text/json',
99 | 'application/javascript',
100 | ],
101 | },
102 | },
103 | ...options,
104 | };
105 |
106 | options.optionPageURL = chrome.runtime.getURL('options.html');
107 |
108 | try {
109 | const tabs = await chrome.tabs.query({});
110 | tabs.forEach(async (tab) => {
111 | try {
112 | await chrome.tabs.sendMessage(tab.id, {
113 | action: 'options_received',
114 | options: options,
115 | });
116 | } catch (error) {}
117 | });
118 | } catch (error) {
119 | console.log(
120 | 'Error Found while sending options from background.js:',
121 | error,
122 | );
123 | }
124 | };
125 |
126 | chrome.runtime.onMessage.addListener(async (message) => {
127 | switch (message.action) {
128 | case 'give_me_options':
129 | try {
130 | await sendOptions();
131 | } catch (error) {
132 | console.log('Error Found:', error);
133 | }
134 | break;
135 | }
136 | });
137 |
138 | chrome.runtime.onInstalled.addListener((details) => {
139 | if (['install', 'update'].includes(details.reason)) {
140 | createContextMenu();
141 | }
142 | });
143 |
--------------------------------------------------------------------------------
/src/vendor/switch/switch.js:
--------------------------------------------------------------------------------
1 | const SWITCH_APPLIED_CLASS_NAME = 'checkbox-ui-checkbox-shadow';
2 | const SWITCH_WRAPPER_CLASS_NAME = 'checkbox-ui-wrapper';
3 |
4 | class Switch {
5 | changeEvent = new CustomEvent('change');
6 | options = {
7 | selector: '.switch',
8 | onDestroy: () => {},
9 | onInitialise: () => {},
10 | onCheckAll: () => {},
11 | onUncheckAll: () => {},
12 | };
13 |
14 | constructor(options = {}) {
15 | this.options = {
16 | ...this.options,
17 | ...options,
18 | };
19 | this.initialise();
20 | }
21 |
22 | onChangeHandler = (e) => {};
23 |
24 | #toggleCheckedState = (checkBox, shouldMarkAsChecked) => {
25 | if (shouldMarkAsChecked) {
26 | this.#setChecked(checkBox);
27 | } else {
28 | this.#setUnchecked(checkBox);
29 | }
30 | };
31 |
32 | #setChecked = (checkBox) => {
33 | if (checkBox.checked) {
34 | return;
35 | }
36 | checkBox.setAttribute('checked', 'checked');
37 | checkBox.checked = true;
38 | checkBox.dispatchEvent(this.changeEvent);
39 | };
40 |
41 | #setUnchecked = (checkBox) => {
42 | if (!checkBox.checked) {
43 | return;
44 | }
45 | checkBox.checked = false;
46 | checkBox.removeAttribute('checked');
47 | checkBox.dispatchEvent(this.changeEvent);
48 | };
49 |
50 | onClickHandler = (e) => {
51 | const checkBox = e.target.querySelector(
52 | `.${SWITCH_APPLIED_CLASS_NAME}`,
53 | );
54 | if (!checkBox) {
55 | return;
56 | }
57 | this.#toggleCheckedState(checkBox, !checkBox.checked);
58 | };
59 |
60 | initialise = () => {
61 | const checkBoxes = document.querySelectorAll(this.options.selector);
62 | checkBoxes.forEach((checkBox) => {
63 | if (checkBox.classList.contains(SWITCH_APPLIED_CLASS_NAME)) {
64 | return;
65 | }
66 | const wrapper = document.createElement('div');
67 | wrapper.addEventListener('click', this.onClickHandler);
68 | wrapper.classList.add(SWITCH_WRAPPER_CLASS_NAME);
69 | const button = document.createElement('div');
70 | button.classList.add('checkbox-ui-btn');
71 |
72 | const clonedCheckbox = checkBox.cloneNode(false);
73 | clonedCheckbox.addEventListener('change', this.onChangeHandler);
74 | clonedCheckbox.classList.add(SWITCH_APPLIED_CLASS_NAME);
75 |
76 | const color =
77 | clonedCheckbox.dataset.color || this.options.accentColor;
78 | if (color) {
79 | wrapper.style.cssText = `--switch-accent-color: ${color}`;
80 | }
81 |
82 | const className = clonedCheckbox.dataset.class;
83 | if (className && !wrapper.classList.contains(className)) {
84 | wrapper.classList.add(className);
85 | }
86 |
87 | wrapper.appendChild(clonedCheckbox);
88 | wrapper.appendChild(button);
89 | const checkBoxParent = checkBox.parentElement;
90 | checkBoxParent.insertBefore(wrapper, checkBox);
91 | checkBox.remove();
92 | });
93 | this.options.onInitialise?.();
94 | };
95 |
96 | destroy = () => {
97 | const allCheckboxWrappers = document.querySelectorAll(
98 | `.${SWITCH_WRAPPER_CLASS_NAME}`,
99 | );
100 | allCheckboxWrappers.forEach((wrapper) => {
101 | const checkBox = wrapper.querySelector(
102 | `${this.options.selector}.${SWITCH_APPLIED_CLASS_NAME}`,
103 | );
104 | if (!checkBox) {
105 | return;
106 | }
107 | wrapper.removeEventListener('click', this.onClickHandler);
108 | checkBox.classList.remove(SWITCH_APPLIED_CLASS_NAME);
109 | checkBox.removeEventListener('change', this.onChangeHandler);
110 | wrapper.insertAdjacentElement('afterend', checkBox);
111 | wrapper.remove();
112 | });
113 | this.options.onDestroy?.();
114 | };
115 |
116 | checkAll = () => {
117 | document
118 | .querySelectorAll(`${this.options.selector}`)
119 | .forEach(this.#setChecked);
120 | this.options.onCheckAll?.();
121 | };
122 |
123 | uncheckAll = () => {
124 | document
125 | .querySelectorAll(`${this.options.selector}`)
126 | .forEach(this.#setUnchecked);
127 | this.options.onUncheckAll?.();
128 | };
129 | }
130 |
131 | export default Switch;
132 | window.Switch = Switch;
133 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # JSON Viewer Pro (Completely Free)
2 |
3 | A browser extension to visualize JSON response in awesome Tree and Chart view with great user experience and options.
4 |
5 | # Welcome to the JSON Viewer Pro. It is absolutely free and has no annoying advertisements.
6 |
7 | [](https://chrome.google.com/webstore/detail/json-viewer-pro/eifflpmocdbdmepbjaopkkhbfmdgijcc)
8 |
9 | ## Features
10 |
11 | - ✅ Beautify JSON response from API
12 | - ✅ Visual representation of JSON
13 | - ✅ Depth traversing of JSON property using breadcrumbs
14 | - ✅ Write custom JSON in Input area
15 | - ✅ Access JSON response via Path navigation prompt with Auto-completion
16 | - ✅ Import local JSON file
17 | - ✅ Download JSON file using Context Menu
18 | - ✅ URL black listing to ignore on certain websites
19 | - ✅ Change Themes
20 | - ✅ Custom CSS
21 | - ✅ Cool User Interface.
22 | - ✅ Copy property and value
23 | - ✅ Access JSON in your console using only `json` keyword
24 |
25 | ## Screen Shots
26 |
27 | 
28 |
29 | # Your donation is appreciated
30 |
31 | ### Your small contribution inspires us.
32 |
33 | [](https://www.paypal.com/donate?hosted_button_id=VQLQGCRJAAF3L)
34 |
35 | ## Online JSON viewer(without any installation needed):
36 |
37 | [Awesome JSON View Online Editor with very limited features](https://rbrahul.github.io/Awesome-JSON-Viewer/# 'Awesome JSON Viewer')
38 |
39 | ## Change Log:
40 |
41 | ### Version 1.0.0 on 04-10-2020
42 |
43 | **New Feature**
44 |
45 | - All the essential features of JSON Viewer
46 |
47 | ### Version 1.0.1 on 20-10-2021
48 |
49 | **New Feature**
50 |
51 | - Added ContentType application/json detection before initialising JSON Viewer Pro extension to webpage.
52 |
53 | **Bug Fix**
54 |
55 | - Fixed bug for NextJS framework implemented sites and Twitter Cards.
56 | - Added support for _localhost (http://localhost:port)_ sites blocking in Manage URL settings.
57 |
58 | ### Version 1.0.2 on 27-03-2024
59 |
60 | **Improvements:**
61 |
62 | - Upgraded to Manifest v3
63 | - Fixed the issue with missing gear icon.
64 | - Fixed issue with the reloading all tabs when settings are saved or set to default.
65 | - Collapse nested items settings was not working once the extension was opened via clicking on the extension icon.
66 | - Minor UI improvements in Toast Message and Buttons on the Setting page.
67 |
68 | ### Version 1.0.3 on 31-03-2024
69 |
70 | **Bug Fixes:**
71 |
72 | - JSON Viewer Pro doesn't recognize local JSON files once opened via browser.
73 |
74 | ### Version 1.0.4 on 28-03-2025
75 |
76 | **New Features**
77 |
78 | - New Improved UI and Theme
79 | - Faster JSON Tree and Chart Rendering
80 | - Besides JSON, YAML and XML file can be imported inside editor and Parse as JSON
81 | - Access JSON via path navigation prompt with auto-completion support
82 | - Improved JSON Editor
83 | - Custom font-size can be configured. (To have this "Restore Default" needs to be clicked at least once to get the updated default custom-css. And change the font-size you prefer).
84 |
85 | **Bug Fixes:**
86 |
87 | - Any Number greater than Number.MAX_SAFE_INTEGER was rendered as wrong value
88 | - Sometimes Chart disappears while clicking on the nodes
89 | - Flickering effect in the JSON Tree view once auto-collapsed is turned on
90 | - Settings and Custom CSS were not applied properly.
91 |
92 | ### Version 1.0.5 on 31-03-2025
93 |
94 | **Bug Fixes:**
95 |
96 | - Sometimes the JSON tree collapse icon was not working after clicking
97 | - JSON Chart sometimes goes outside the visible area
98 | - First few lines in JSON Tree view were getting covered by the Menu for longer texts.
99 | - Menu Icons were turned into black for the very first installation as the default settings were not properly applied.
100 |
101 | ### Version 1.0.6 on 15-04-2025
102 |
103 | **Features:**
104 |
105 | - Implemented Content-Type/Mime-Type detection Configurable. Earlier only Content-Type: 'application/json' were supported. Regardless of Content-Type, users can also select if the content should be parsed if it is valid JSON structured data.
106 |
107 | **Note:**
108 | Besides these above mentioned points, there were lot of improvements made in the build process and development practices. I have mroe plans to improve the code quality and the performance of the application rendering in upcoming days.
109 |
110 | Enjoy the JSON Viewer Pro.
111 |
112 | **Developed with ♥ using ReactJS and D3**
113 |
--------------------------------------------------------------------------------
/src/vendor/switch/example.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Switch
7 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
64 |
65 |
66 |
67 |
68 |
100 |
101 |
102 |
135 |
136 |
137 |
--------------------------------------------------------------------------------
/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 |
4 | import App from './App.jsx';
5 | import { getURL, parseJson } from './utils/common';
6 | import { DARK_THEMES, DEFAULT_OPTIONS } from './constants/options.js';
7 |
8 | const codeMirrorStyleSheetId = 'codemirror-css';
9 | const COLOR_THEME_LINK_TAG_ID = 'color-theme-css';
10 | const CUSTOM_CSS_STYLE_TAG_ID = 'custom-css';
11 |
12 | const isJSONViewerProExtensionPage = () => {
13 | return (
14 | window.location.href.includes('chrome-extension://') &&
15 | window.location.search.includes('options')
16 | );
17 | };
18 |
19 | const injectStyleSheet = (stylesheetUrl, idSelector) => {
20 | const linkTag = document.createElement('link');
21 | linkTag.setAttribute('href', stylesheetUrl);
22 | linkTag.rel = 'stylesheet';
23 | linkTag.type = 'text/css';
24 | linkTag.id = idSelector;
25 | document.head.appendChild(linkTag);
26 | return linkTag;
27 | };
28 |
29 | const applyOptionsIfChromeExtensionPage = (options) => {
30 | if (!isJSONViewerProExtensionPage()) {
31 | return;
32 | }
33 | const themes = {
34 | default: 'dark-pro.css',
35 | mdn: 'mdn-light.css',
36 | };
37 | let colorThemeStyleSheetLinkElement = document.getElementById(
38 | COLOR_THEME_LINK_TAG_ID,
39 | );
40 | const colorThemeStylesheetUrl = themes[options.theme] || themes['default'];
41 | const cssURL = getURL('css/color-themes/' + colorThemeStylesheetUrl);
42 |
43 | if (!colorThemeStyleSheetLinkElement) {
44 | injectStyleSheet(cssURL, COLOR_THEME_LINK_TAG_ID);
45 | } else if (
46 | colorThemeStyleSheetLinkElement &&
47 | colorThemeStyleSheetLinkElement.href.indexOf(colorThemeStylesheetUrl) <
48 | 0
49 | ) {
50 | colorThemeStyleSheetLinkElement.setAttribute('href', cssURL);
51 | }
52 |
53 | let customStyleElement = document.getElementById(CUSTOM_CSS_STYLE_TAG_ID);
54 | if (!customStyleElement) {
55 | customStyleElement = document.createElement('style');
56 | customStyleElement.id = CUSTOM_CSS_STYLE_TAG_ID;
57 | document.head.appendChild(customStyleElement);
58 | }
59 |
60 | customStyleElement.textContent = options.css || '';
61 | };
62 |
63 | const getOptions = async () => {
64 | try {
65 | const metaDataElement = document.querySelector(
66 | 'meta[name="extension-options"]',
67 | );
68 | const options =
69 | metaDataElement && metaDataElement.getAttribute('content');
70 |
71 | if (options) {
72 | return JSON.parse(options);
73 | }
74 |
75 | if (isJSONViewerProExtensionPage()) {
76 | return JSON.parse(
77 | decodeURIComponent(
78 | new URLSearchParams(window.location.search).get('options'),
79 | ),
80 | );
81 | }
82 |
83 | const chrome = window.chrome;
84 | if (chrome && chrome.storage && chrome.storage.local) {
85 | return await chrome.storage.local.get([
86 | 'rb-awesome-json-viewer-options',
87 | ]);
88 | }
89 | } catch (error) {
90 | console.error('Error while fetching options', error);
91 | }
92 | return DEFAULT_OPTIONS;
93 | };
94 |
95 | const injectCodeMirrorStylesheet = () => {
96 | if (!!document.querySelector('#' + codeMirrorStyleSheetId)) {
97 | document.querySelector('#' + codeMirrorStyleSheetId).remove();
98 | }
99 |
100 | const cssFilePath = getURL('css/codemirror.css');
101 | injectStyleSheet(cssFilePath, codeMirrorStyleSheetId);
102 | };
103 |
104 | /*
105 | ** Some webistes for example: api.github.com rejects Style tag creation if following CSP is set in the Server Response Headers
106 | ** Content-Security-Policy: default-src 'none'
107 | ** CodeMirror 6 Does not provide external CSS file. It only injects styles in the style tag dynamicaly as it was developed
108 | ** based on CSS in JS.
109 | **/
110 | const detectCSPViolation = () => {
111 | document.addEventListener('securitypolicyviolation', function (e) {
112 | if (e.violatedDirective === 'style-src-elem') {
113 | injectCodeMirrorStylesheet();
114 | }
115 | });
116 | };
117 |
118 | (async () => {
119 | detectCSPViolation();
120 | try {
121 | let content = document.body?.innerText;
122 | content = content?.trim();
123 | const jsonData = parseJson(content);
124 | window.json = jsonData;
125 | window.extensionOptions = await getOptions();
126 | applyOptionsIfChromeExtensionPage(window.extensionOptions);
127 |
128 | const rootElement = document.createElement('div');
129 | rootElement.setAttribute('id', 'rbrahul-awesome-json');
130 | document.body.innerHTML = '';
131 | document.body.appendChild(rootElement);
132 | const root = createRoot(rootElement);
133 | root.render(
134 | ,
140 | );
141 | } catch (e) {
142 | console.error('Something went wrong at Awesome JSON Viewer Pro', e);
143 | }
144 | })();
145 |
--------------------------------------------------------------------------------
/src/vendor/map2tree/test/map2tree.spec.ts:
--------------------------------------------------------------------------------
1 | import { map2tree, Node } from '../src/index.js';
2 | import * as immutable from 'immutable';
3 |
4 | test('# rootNodeKey', () => {
5 | const map = {};
6 | const options = { key: 'foo' };
7 |
8 | expect((map2tree(map, options) as Node).name).toBe('foo');
9 | });
10 |
11 | describe('# shallow map', () => {
12 | test('## null', () => {
13 | const map = {
14 | a: null,
15 | };
16 |
17 | const expected = {
18 | name: 'state',
19 | children: [{ name: 'a', value: null }],
20 | };
21 |
22 | expect(map2tree(map)).toEqual(expected);
23 | expect(map2tree(immutable.fromJS(map))).toEqual(expected);
24 | });
25 |
26 | test('## value', () => {
27 | const map = {
28 | a: 'foo',
29 | b: 'bar',
30 | };
31 |
32 | const expected = {
33 | name: 'state',
34 | children: [
35 | { name: 'a', value: 'foo' },
36 | { name: 'b', value: 'bar' },
37 | ],
38 | };
39 |
40 | expect(map2tree(map)).toEqual(expected);
41 | expect(map2tree(immutable.fromJS(map))).toEqual(expected);
42 | });
43 |
44 | test('## object', () => {
45 | const map = {
46 | a: { aa: 'foo' },
47 | };
48 |
49 | const expected = {
50 | name: 'state',
51 | children: [{ name: 'a', children: [{ name: 'aa', value: 'foo' }] }],
52 | };
53 |
54 | expect(map2tree(map)).toEqual(expected);
55 | expect(map2tree(immutable.fromJS(map))).toEqual(expected);
56 | });
57 |
58 | test('## immutable Map', () => {
59 | const map = {
60 | a: immutable.fromJS({ aa: 'foo', ab: 'bar' }),
61 | };
62 |
63 | const expected = {
64 | name: 'state',
65 | children: [
66 | {
67 | name: 'a',
68 | children: [
69 | { name: 'aa', value: 'foo' },
70 | { name: 'ab', value: 'bar' },
71 | ],
72 | },
73 | ],
74 | };
75 |
76 | expect(map2tree(map)).toEqual(expected);
77 | });
78 | });
79 |
80 | describe('# deep map', () => {
81 | test('## null', () => {
82 | const map = {
83 | a: { aa: null },
84 | };
85 |
86 | const expected = {
87 | name: 'state',
88 | children: [
89 | {
90 | name: 'a',
91 | children: [
92 | {
93 | name: 'aa',
94 | value: null,
95 | },
96 | ],
97 | },
98 | ],
99 | };
100 |
101 | expect(map2tree(map)).toEqual(expected);
102 | expect(map2tree(immutable.fromJS(map))).toEqual(expected);
103 | });
104 |
105 | test('## object', () => {
106 | const map = {
107 | a: { aa: { aaa: 'foo' } },
108 | };
109 |
110 | const expected = {
111 | name: 'state',
112 | children: [
113 | {
114 | name: 'a',
115 | children: [
116 | {
117 | name: 'aa',
118 | children: [{ name: 'aaa', value: 'foo' }],
119 | },
120 | ],
121 | },
122 | ],
123 | };
124 |
125 | expect(map2tree(map)).toEqual(expected);
126 | expect(map2tree(immutable.fromJS(map))).toEqual(expected);
127 | });
128 | });
129 |
130 | describe('# array map', () => {
131 | const map = {
132 | a: [1, 2],
133 | };
134 |
135 | test('## push', () => {
136 | const expected = {
137 | name: 'state',
138 | children: [
139 | {
140 | name: 'a',
141 | children: [
142 | { name: 'a[0]', value: 1 },
143 | { name: 'a[1]', value: 2 },
144 | ],
145 | },
146 | ],
147 | };
148 |
149 | expect(map2tree(map)).toEqual(expected);
150 | expect(map2tree(immutable.fromJS(map))).toEqual(expected);
151 | });
152 |
153 | test('## unshift', () => {
154 | const options = { pushMethod: 'unshift' as const };
155 | const expected = {
156 | name: 'state',
157 | children: [
158 | {
159 | name: 'a',
160 | children: [
161 | { name: 'a[1]', value: 2 },
162 | { name: 'a[0]', value: 1 },
163 | ],
164 | },
165 | ],
166 | };
167 |
168 | expect(map2tree(map, options)).toEqual(expected);
169 | expect(map2tree(immutable.fromJS(map), options)).toEqual(expected);
170 | });
171 |
172 | test('## null', () => {
173 | const map = {
174 | a: [null],
175 | };
176 |
177 | const expected = {
178 | name: 'state',
179 | children: [
180 | {
181 | name: 'a',
182 | children: [{ name: 'a[0]', value: null }],
183 | },
184 | ],
185 | };
186 |
187 | expect(map2tree(map)).toEqual(expected);
188 | expect(map2tree(immutable.fromJS(map))).toEqual(expected);
189 | });
190 | });
191 |
192 | describe('# collection map', () => {
193 | test('## value', () => {
194 | const map = {
195 | a: [{ aa: 1 }, { aa: 2 }],
196 | };
197 |
198 | const expected = {
199 | name: 'state',
200 | children: [
201 | {
202 | name: 'a',
203 | children: [
204 | { name: 'a[0]', object: { aa: 1 } },
205 | { name: 'a[1]', object: { aa: 2 } },
206 | ],
207 | },
208 | ],
209 | };
210 |
211 | expect(map2tree(map)).toEqual(expected);
212 | expect(map2tree(immutable.fromJS(map))).toEqual(expected);
213 | });
214 |
215 | test('## object', () => {
216 | const map = {
217 | a: [{ aa: { aaa: 'foo' } }],
218 | };
219 |
220 | const expected = {
221 | name: 'state',
222 | children: [
223 | {
224 | name: 'a',
225 | children: [{ name: 'a[0]', object: { aa: { aaa: 'foo' } } }],
226 | },
227 | ],
228 | };
229 |
230 | expect(map2tree(map)).toEqual(expected);
231 | expect(map2tree(immutable.fromJS(map))).toEqual(expected);
232 | });
233 | });
234 |
--------------------------------------------------------------------------------
/src/Menus.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { FiTerminal, FiX } from 'react-icons/fi';
3 | import Tree from './components/Icons/Tree';
4 | import Branch from './components/Icons/Branch';
5 | import Brackets from './components/Icons/Brackets';
6 | import Gear from './components/Icons/Gear';
7 | import { iconFillColor } from './utils/common';
8 |
9 | class Menus extends Component {
10 | constructor(props) {
11 | super(props);
12 | this.state = {
13 | selectedPan: props.selectedTab,
14 | };
15 | }
16 |
17 | setActive(tab) {
18 | this.setState({ selectedPan: tab });
19 | this.props.changeTabSelection(tab);
20 | }
21 |
22 | componentWillMount() {
23 | this.prepareComponentState(this.props);
24 | }
25 |
26 | componentWillReceiveProps(nextProps) {
27 | this.prepareComponentState(nextProps);
28 | }
29 |
30 | prepareComponentState(props) {
31 | this.setState({
32 | selectedPan: props.selectedTab,
33 | });
34 | }
35 |
36 | showSearchBar() {
37 | this.props.showSearchBar();
38 | }
39 |
40 | render() {
41 | return (
42 |
141 | );
142 | }
143 | }
144 |
145 | export default Menus;
146 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Menus from './Menus.jsx';
3 | import Tooltip from './vendor/tooltip';
4 | import TreeView from './TreeView.jsx';
5 | import ChartViews from './ChartView.jsx';
6 | import Editor from './components/Editor';
7 | import downloadFile from './utils/dowloadFile';
8 | import { currentDateTime } from './utils/datetime';
9 |
10 | import './vendor/tooltip/style.css';
11 | import './css/style.scss';
12 |
13 | class App extends Component {
14 | constructor(props) {
15 | super(props);
16 | window.json = props.json;
17 | this.state = {
18 | selectedTab: 'tree',
19 | json: props.json,
20 | selectedJSON: props.json,
21 | isSearchBarVisible: false,
22 | };
23 | this.showLogInConsole();
24 | this.locationHashChanged = this.locationHashChanged.bind(this);
25 | this.showSearchBar = this.showSearchBar.bind(this);
26 | this.hideSearchBar = this.hideSearchBar.bind(this);
27 | this.changeJSON = this.changeJSON.bind(this);
28 | this.restoreOriginalJSON = this.restoreOriginalJSON.bind(this);
29 | this.tooltip = React.createRef();
30 | this.intervalIdRef = React.createRef();
31 | this.originalJSONRef = React.createRef(props.json);
32 | }
33 |
34 | changeTabSelection(tab) {
35 | this.setState({ selectedTab: tab });
36 | }
37 |
38 | showSearchBar() {
39 | this.setState({ isSearchBarVisible: true });
40 | }
41 |
42 | hideSearchBar() {
43 | this.setState({ isSearchBarVisible: false });
44 | }
45 |
46 | changeJSON(json, openTreeView = true) {
47 | this.setState(
48 | {
49 | json,
50 | selectedJSON: json,
51 | },
52 | () => {
53 | window.json = json;
54 | if (openTreeView) {
55 | this.changeTabSelection('tree');
56 | }
57 | },
58 | );
59 | }
60 |
61 | mutateOriginalJSONAndRender(json) {
62 | this.originalJSONRef.current = json;
63 | window.json = json;
64 | this.changeJSON(this.originalJSONRef.current);
65 | }
66 |
67 | restoreOriginalJSON() {
68 | this.changeJSON(this.originalJSONRef.current, false);
69 | }
70 |
71 | locationHashChanged() {
72 | if (window.location.href.includes('downloadJSON=true')) {
73 | this.downloadAsJSON();
74 | setTimeout(() => {
75 | window.location.hash = '';
76 | }, 2000);
77 | }
78 | }
79 |
80 | downloadAsJSON() {
81 | downloadFile(
82 | JSON.stringify(window.json, null, 2),
83 | 'text/json',
84 | `data-${currentDateTime()}.json`,
85 | );
86 | }
87 |
88 | componentDidMount() {
89 | if (this.props.json !== this.originalJSONRef.current) {
90 | this.originalJSONRef.current = this.props.json;
91 | }
92 |
93 | if (!this.tooltip.current) {
94 | this.tooltip.current = new Tooltip();
95 | } else {
96 | this.tooltip.current.initialiseTooltip();
97 | }
98 |
99 | this.intervalIdRef.current = window.setInterval(() => {
100 | this.tooltip.current?.initialiseTooltip();
101 | }, 3_000);
102 |
103 | window.addEventListener('hashchange', this.locationHashChanged, false);
104 | }
105 |
106 | componentWillUnmount() {
107 | window.removeEventListener(
108 | 'hashchange',
109 | this.locationHashChanged,
110 | false,
111 | );
112 |
113 | if (this.tooltip.current) {
114 | this.tooltip.current.destroy();
115 | }
116 | if (this.intervalIdRef.current) {
117 | clearInterval(this.intervalIdRef.current);
118 | }
119 | }
120 |
121 | showLogInConsole() {
122 | console.log(
123 | '%cTo access the JSON data just write: %cjson',
124 | 'font-size:14px; color: #4fdee5;background:black;padding:8px;padding-right:0px',
125 | 'font-size:14px;color:orange;font-weight:bold;background:black;padding:8px;padding-left:0px',
126 | );
127 | }
128 |
129 | render() {
130 | return (
131 |
132 |
140 |
141 | {this.state.selectedTab === 'tree' && (
142 |
147 | )}
148 | {this.state.selectedTab === 'chart' && (
149 |
154 | )}
155 | {this.state.selectedTab === 'jsonInput' && (
156 |
162 | )}
163 |
164 |
165 | );
166 | }
167 | }
168 |
169 | export default App;
170 |
--------------------------------------------------------------------------------
/src/utils/json-viewer/json-viewer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Check if arg is either an array with at least 1 element, or a dict with at least 1 key
3 | * @return boolean
4 | */
5 | function isCollapsable(arg) {
6 | return arg instanceof Object && Object.keys(arg).length > 0;
7 | }
8 |
9 | /**
10 | * Check if a string represents a valid url
11 | * @return boolean
12 | */
13 | function isUrl(string) {
14 | var regexp =
15 | /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/;
16 | return regexp.test(string);
17 | }
18 |
19 | const getJsonToggleClassNames = (isCollapsed) => {
20 | let classNames = 'json-toggle';
21 | return isCollapsed ? classNames + ' collapsed' : classNames;
22 | };
23 |
24 | const getToggleStyle = (isCollapsed) => {
25 | return isCollapsed ? 'style="display:none;"' : '';
26 | };
27 |
28 | const createPlaceHolderNode = (isCollapsed, count) => {
29 | return isCollapsed && count > 0
30 | ? `${count} item${
31 | count > 1 ? 's' : ''
32 | }`
33 | : '';
34 | };
35 |
36 | /**
37 | * Transform a json object into html representation
38 | * @return string
39 | */
40 | function json2html(json, options, nestedLevel = []) {
41 | var html = '';
42 | const isCollapsed = options.collapsed && nestedLevel.length > 0;
43 | if (typeof json === 'string') {
44 | // Escape tags
45 | json = json
46 | .replace(/&/g, '&')
47 | .replace(//g, '>');
49 | if (isUrl(json))
50 | html +=
51 | '"' +
54 | json +
55 | '"';
56 | else html += '"' + json + '"';
57 | } else if (typeof json === 'number') {
58 | html += '' + json + '';
59 | } else if (typeof json === 'boolean') {
60 | html += '' + json + '';
61 | } else if (json === null) {
62 | html += 'null';
63 | } else if (json instanceof Array) {
64 | if (json.length > 0) {
65 | html += `[`;
66 | for (var i = 0; i < json.length; ++i) {
67 | html += '- ';
68 | // Add toggle button if item is collapsable
69 | if (isCollapsable(json[i])) {
70 | html += ``;
73 | }
74 | nestedLevel.push(1);
75 | html += json2html(json[i], options, nestedLevel);
76 | // Add comma if item is not last
77 | if (i < json.length - 1) {
78 | html += ',';
79 | }
80 | html += '
';
81 | }
82 | html += `
${createPlaceHolderNode(isCollapsed, json.length)}]`;
83 | } else {
84 | html += '[]';
85 | }
86 | } else if (typeof json === 'object') {
87 | var key_count = Object.keys(json).length;
88 | if (key_count > 0) {
89 | html += `{`;
90 | for (var key in json) {
91 | if (json.hasOwnProperty(key)) {
92 | html += '- ';
93 | var keyRepr = options.withQuotes
94 | ? '"' + key + '"'
95 | : key;
96 | // Add toggle button if item is collapsable
97 | if (isCollapsable(json[key])) {
98 | html +=
99 | `` +
102 | keyRepr +
103 | '';
104 | } else {
105 | html += keyRepr;
106 | }
107 | nestedLevel.push(1);
108 | html += ': ' + json2html(json[key], options, nestedLevel);
109 | // Add comma if item is not last
110 | if (--key_count > 0) html += ',';
111 | html += '
';
112 | }
113 | }
114 | html += `
${createPlaceHolderNode(
115 | isCollapsed,
116 | Object.keys(json).length,
117 | )}}`;
118 | } else {
119 | html += '{}';
120 | }
121 | }
122 | return html;
123 | }
124 |
125 | /**
126 | * jQuery plugin method
127 | * @param json: a javascript object
128 | * @param options: an optional options hash
129 | */
130 | export const initPlugin = function (node, jQuery, json, option) {
131 | (function ($, node, json, options) {
132 | options = options || { collapsed: false };
133 | // jQuery chaining
134 | return $(node).each(function () {
135 | // Transform to HTML
136 | var html = json2html(json, options, []);
137 | if (isCollapsable(json))
138 | html = `` + html;
139 |
140 | // Insert HTML in target DOM element
141 | $(this).html(html);
142 |
143 | // Bind click on toggle buttons
144 | $(this).off('click');
145 | $(this).on('click', 'span.property', function (e) {
146 | $('li').removeClass('copyable');
147 | $(this).parents('li').first().addClass('copyable');
148 | });
149 |
150 | // Simulate click on toggle button when placeholder is clicked
151 | $(this).on('click', 'a.json-placeholder', function () {
152 | $(this).siblings('a.json-toggle').click();
153 | return false;
154 | });
155 | });
156 | })(jQuery, node, json, option);
157 | };
158 |
--------------------------------------------------------------------------------
/src/vendor/tooltip/index.js:
--------------------------------------------------------------------------------
1 | class Tooltip {
2 | direction = 'bottom';
3 | distance = 10;
4 | container = document;
5 | tooltipMessageId = 'tooltip-msg';
6 | styleTagId = 'tooltip-style';
7 | _allowedDirection = ['top', 'right', 'bottom', 'left'];
8 | size = 'medium'; // small | medium | large
9 |
10 | constructor(options = {}) {
11 | if (options.container) {
12 | this.container = options.container;
13 | }
14 | if (options.direction) {
15 | this.direction = options.direction;
16 | }
17 |
18 | if (options.distance) {
19 | this.distance = options.distance;
20 | }
21 |
22 | if (options.size) {
23 | this.size = options.size;
24 | }
25 |
26 | this.initialiseTooltip();
27 | }
28 |
29 | getMatchedClasses(classList, expectedClasses = []) {
30 | return expectedClasses.filter((expectedClass) =>
31 | classList.contains(expectedClass),
32 | );
33 | }
34 |
35 | addTooltip() {
36 | const tooltip = document.createElement('div');
37 | const toolTipId = this.tooltipMessageId;
38 | tooltip.id = toolTipId;
39 | tooltip.classList.add('hidden', this.size || 'medium');
40 | if (!document.body.querySelector(`#${toolTipId}`)) {
41 | document.body.append(tooltip);
42 | }
43 | }
44 |
45 | getOffset(target, direction = this.direction) {
46 | const rect = target.getBoundingClientRect();
47 | let offsetTop = rect.top + window.scrollY + rect.height / 2;
48 | let offsetLeft =
49 | rect.left + window.scrollX + rect.width + this.distance;
50 | const halfWidth = target.width / 2;
51 | switch (direction) {
52 | case 'top':
53 | offsetTop = rect.top + window.scrollY - this.distance;
54 | offsetLeft = rect.left + window.scrollX + halfWidth;
55 | break;
56 | case 'right':
57 | offsetTop = rect.top + window.scrollY + rect.height / 2;
58 | offsetLeft =
59 | rect.left + window.scrollX + rect.width + this.distance;
60 | break;
61 | case 'left':
62 | offsetTop = rect.top + window.scrollY + rect.height / 2;
63 | offsetLeft = rect.left + window.scrollX - this.distance;
64 | break;
65 | case 'bottom':
66 | offsetTop =
67 | rect.top + window.scrollY + rect.height + this.distance;
68 | offsetLeft = rect.left + window.scrollX + rect.width / 2;
69 | break;
70 | }
71 | return {
72 | top: offsetTop,
73 | left: offsetLeft,
74 | };
75 | }
76 |
77 | showToolTip(event) {
78 | this.hideTooltip();
79 | let direction = this.direction;
80 | const dataset = event.currentTarget.dataset;
81 |
82 | if ('direction' in dataset) {
83 | if (!this._allowedDirection.includes(dataset.direction)) {
84 | // console.warn(`Invalid data-direction attribute was set. Using the default direction:${this.direction}`)
85 | } else {
86 | direction = dataset.direction;
87 | }
88 | }
89 |
90 | const { top: offsetTop, left: offsetLeft } = this.getOffset(
91 | event.target,
92 | direction,
93 | );
94 |
95 | const tooltipNode = document.querySelector(`#${this.tooltipMessageId}`);
96 |
97 | if ('tooltip' in dataset) {
98 | const tooltipMsg = dataset.tooltip;
99 | if (!tooltipNode) return;
100 |
101 | const matchedClasses = this.getMatchedClasses(
102 | tooltipNode.classList,
103 | this._allowedDirection.map((className) => `dir-${className}`),
104 | );
105 |
106 | if (!!matchedClasses.length) {
107 | matchedClasses.forEach((className) =>
108 | tooltipNode.classList.remove(className),
109 | );
110 | }
111 |
112 | tooltipNode.classList.add('dir-' + direction);
113 |
114 | tooltipNode.style = `top:${offsetTop}px;left:${offsetLeft}px;`;
115 | tooltipNode.textContent = tooltipMsg;
116 |
117 | if (tooltipNode.classList.contains('hidden')) {
118 | tooltipNode.classList.remove('hidden');
119 | }
120 |
121 | if (!tooltipNode.classList.contains('visible')) {
122 | tooltipNode.classList.add('visible');
123 | }
124 | }
125 | if ('tooltipSize' in dataset) {
126 | if (
127 | tooltipNode?.classList &&
128 | !tooltipNode.classList.contains(dataset.tooltipSize)
129 | ) {
130 | tooltipNode.classList.add(dataset.tooltipSize);
131 | }
132 | } else {
133 | if (
134 | tooltipNode?.classList &&
135 | tooltipNode.classList.contains(dataset.tooltipSize)
136 | ) {
137 | tooltipNode.classList.remove(dataset.tooltipSize);
138 | }
139 | }
140 | }
141 |
142 | hideTooltip() {
143 | const tooltipNode = document.querySelector(`#${this.tooltipMessageId}`);
144 | if (!tooltipNode) return;
145 |
146 | if (tooltipNode.classList.contains('visible')) {
147 | tooltipNode.classList.remove('visible');
148 | }
149 |
150 | if (!tooltipNode.classList.contains('hidden')) {
151 | tooltipNode.classList.add('hidden');
152 | }
153 | }
154 |
155 | initialiseTooltip() {
156 | this.addTooltip();
157 |
158 | this.container.querySelectorAll('[data-tooltip]').forEach((element) => {
159 | element.removeEventListener(
160 | 'mouseenter',
161 | this.showToolTip.bind(this),
162 | );
163 | element.addEventListener('mouseenter', this.showToolTip.bind(this));
164 |
165 | element.removeEventListener(
166 | 'mouseleave',
167 | this.hideTooltip.bind(this),
168 | );
169 | element.addEventListener('mouseleave', this.hideTooltip.bind(this));
170 | });
171 | }
172 |
173 | cleanUpEvent() {
174 | this.container.querySelectorAll('[data-tooltip]').forEach((element) => {
175 | element.removeEventListener(
176 | 'mouseenter',
177 | this.showToolTip.bind(this),
178 | );
179 | element.removeEventListener(
180 | 'mouseleave',
181 | this.hideTooltip.bind(this),
182 | );
183 | });
184 | }
185 |
186 | destroy() {
187 | const styleTag = document.querySelector(`#${this.styleTagId}`);
188 | if (styleTag) {
189 | styleTag.remove();
190 | }
191 |
192 | const tooltipMsgElement = document.querySelector(
193 | `#${this.tooltipMessageId}`,
194 | );
195 | if (tooltipMsgElement) {
196 | tooltipMsgElement.remove();
197 | }
198 | this.cleanUpEvent();
199 | }
200 | }
201 |
202 | export default Tooltip;
203 |
--------------------------------------------------------------------------------
/src/components/Dropdown/index.jsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useState, useEffect, useRef } from 'react';
2 | import { FiCheck, FiChevronDown } from 'react-icons/fi';
3 | import clsx from 'clsx';
4 | import PropTypes from 'prop-types';
5 | import './style.scss';
6 |
7 | const findSelectedByLabel = (items, value) => {
8 | return items.findIndex(({ label }) => label === value);
9 | };
10 |
11 | const findSelected = (items) => {
12 | return items.findIndex(({ selected }) => !!selected);
13 | };
14 |
15 | const DropDown = ({
16 | labelIcon,
17 | label,
18 | items,
19 | className = '',
20 | hasCaretIcon = true,
21 | isButtonVisible = true,
22 | open = false,
23 | onClose,
24 | }) => {
25 | const expandedRef = useRef(open);
26 | const listContainerRef = useRef();
27 | const [isExpanded, setIsExpanded] = useState(open);
28 | const index = findSelected(items ?? []);
29 | const [selectedIndex, setSelectedIndex] = useState(index < 0 ? 0 : index);
30 | const onChangeHandler = useCallback(
31 | (cb, value) => (_) => {
32 | cb(value);
33 | setIsExpanded(!isExpanded);
34 | const selectedIndex = findSelectedByLabel(items, value);
35 | setSelectedIndex(selectedIndex);
36 | },
37 | [isExpanded, selectedIndex, items],
38 | );
39 |
40 | const onClickHandler = useCallback(
41 | (_) => {
42 | setIsExpanded(!isExpanded);
43 | },
44 | [isExpanded],
45 | );
46 |
47 | const bodyClickHandler = useCallback(
48 | (e) => {
49 | const target = e.target;
50 | if (!!target?.closest('.dropdown')) {
51 | return;
52 | }
53 | setIsExpanded(false);
54 | if (typeof onClose === 'function') {
55 | onClose();
56 | }
57 | },
58 | [isExpanded],
59 | );
60 |
61 | const scrollThroughList = (element, stepHeight, increase, nextIndex) => {
62 | let totalScrolled = element.scrollTop;
63 | const scrollHeight = element.scrollHeight;
64 | const visibleHeight = stepHeight * 5;
65 | const currentPositionHeight = (nextIndex + 1) * stepHeight;
66 |
67 | if (
68 | increase &&
69 | currentPositionHeight < scrollHeight &&
70 | currentPositionHeight > visibleHeight
71 | ) {
72 | totalScrolled += stepHeight;
73 | } else if (!increase && totalScrolled > 0) {
74 | totalScrolled -= stepHeight;
75 | }
76 |
77 | element.scroll(0, totalScrolled);
78 | };
79 |
80 | const bodyKeyDownHandler = useCallback(
81 | (e) => {
82 | if (!isExpanded || items.length == 0) {
83 | return;
84 | }
85 |
86 | if ([38, 40].includes(e.which)) {
87 | setSelectedIndex((currentIndex) => {
88 | let nextValue = currentIndex;
89 | if (e.which === 38) {
90 | nextValue = currentIndex - 1;
91 | } else if (e.which === 40) {
92 | nextValue = currentIndex + 1;
93 | }
94 | if (nextValue < 0) {
95 | return 0;
96 | } else if (nextValue >= items.length) {
97 | return items.length - 1;
98 | }
99 |
100 | const listItemHeight =
101 | listContainerRef.current?.querySelector('li')
102 | .clientHeight - 1;
103 | scrollThroughList(
104 | listContainerRef.current,
105 | listItemHeight,
106 | currentIndex < nextValue,
107 | nextValue,
108 | );
109 | return nextValue;
110 | });
111 | }
112 |
113 | if ([9, 13].includes(e.which)) {
114 | const selectedItem = items[selectedIndex];
115 | const onClickHandler = selectedItem?.onClick;
116 | setIsExpanded(!isExpanded);
117 | onClickHandler?.(selectedItem?.label);
118 | onClose();
119 | }
120 | },
121 | [items, selectedIndex, isExpanded],
122 | );
123 |
124 | useEffect(() => {
125 | document.body.addEventListener('click', bodyClickHandler, false);
126 | document.body.addEventListener('keydown', bodyKeyDownHandler, false);
127 | return () => {
128 | document.body.removeEventListener('click', bodyClickHandler, false);
129 | document.body.removeEventListener(
130 | 'keydown',
131 | bodyKeyDownHandler,
132 | false,
133 | );
134 | };
135 | }, [bodyKeyDownHandler, bodyClickHandler]);
136 |
137 | useEffect(() => {
138 | if (expandedRef.current !== open && isExpanded !== open) {
139 | setIsExpanded(open);
140 | expandedRef.current = open;
141 | }
142 | }, [isExpanded, open]);
143 |
144 | return (
145 |
149 | {isButtonVisible && (
150 |
151 | {labelIcon && (
152 | {labelIcon}
153 | )}
154 | {label && {label}}
155 | {hasCaretIcon && (
156 |
157 |
158 |
159 | )}
160 |
161 | )}
162 |
166 | {items.map(({ label, iconUrl, onClick, selected }, index) => (
167 | -
172 | {iconUrl && (
173 |
174 | )}
175 | {label}
176 | {selected && (
177 |
178 |
179 |
180 | )}
181 |
182 | ))}
183 |
184 |
185 | );
186 | };
187 |
188 | DropDown.propTypes = {
189 | size: PropTypes.string,
190 | className: PropTypes.string,
191 | labelIcon: PropTypes.node,
192 | hasCaretIcon: PropTypes.bool,
193 | open: PropTypes.bool,
194 | label: PropTypes.string,
195 | items: PropTypes.arrayOf(
196 | PropTypes.shape({
197 | label: PropTypes.string,
198 | iconUrl: PropTypes.string,
199 | onClick: PropTypes.func,
200 | }),
201 | ),
202 | onChange: PropTypes.func,
203 | onClose: PropTypes.func,
204 | };
205 |
206 | export default DropDown;
207 |
--------------------------------------------------------------------------------
/src/scripts/contentScript.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const BASE_STYLE_LINK_TAG_ID = 'main-css';
4 | const COLOR_THEME_LINK_TAG_ID = 'color-theme-css';
5 | const CUSTOM_CSS_STYLE_TAG_ID = 'custom-css';
6 | const MAINJS_SCRIPT_TAG_ID = 'main-script';
7 | const ALLOWED_CONTENT_TYPES = [
8 | 'application/json',
9 | 'text/json',
10 | 'application/javascript',
11 | 'application/x-javascript',
12 | 'application/vnd.api+json',
13 | 'text/x-json',
14 | 'text/x-javascript',
15 | ];
16 |
17 | function getCleanTextContent() {
18 | const clonedDom = document.documentElement.cloneNode(true);
19 | clonedDom.querySelectorAll('script, style').forEach((el) => el.remove());
20 | let bodyInnerText = clonedDom.innerText;
21 | if (typeof bodyInnerText === 'string') {
22 | return bodyInnerText.trim();
23 | }
24 | return bodyInnerText;
25 | }
26 |
27 | const isDocumentContentTypeJSON = () =>
28 | ALLOWED_CONTENT_TYPES.includes(document.contentType);
29 |
30 | const isJsonViewerLoaded = () =>
31 | !!document.getElementById('rbrahul-awesome-json') ||
32 | window.JSON_VIEWER_PRO_INITIALISED;
33 |
34 | const addBodyTagIfMissing = () => {
35 | if (!document.querySelector('body')) {
36 | const body = document.createElement('body');
37 | document.querySelector('html').appendChild(body);
38 | }
39 | };
40 |
41 | const addHeadTagIfMissing = () => {
42 | if (!document.querySelector('head')) {
43 | const headNode = document.createElement('head');
44 | document
45 | .querySelector('html')
46 | .insertBefore(headNode, document.querySelector('body'));
47 | }
48 | };
49 |
50 | const injectStyleSheet = (stylesheetUrl, idSelector) => {
51 | const styleTag = document.createElement('link');
52 | styleTag.setAttribute('href', stylesheetUrl);
53 | styleTag.rel = 'stylesheet';
54 | styleTag.type = 'text/css';
55 | styleTag.id = idSelector;
56 | document.head.appendChild(styleTag);
57 | return styleTag;
58 | };
59 |
60 | const injectCssUrlAndStyleTag = () => {
61 | if (!!document.getElementById(BASE_STYLE_LINK_TAG_ID)) {
62 | return;
63 | }
64 | const baseStyleCssFilePath = chrome.runtime.getURL('/css/style.css');
65 | injectStyleSheet(baseStyleCssFilePath, BASE_STYLE_LINK_TAG_ID);
66 |
67 | const colorThemeCssFilePath = chrome.runtime.getURL(
68 | '/css/color-themes/dark-pro.css',
69 | );
70 | injectStyleSheet(colorThemeCssFilePath, COLOR_THEME_LINK_TAG_ID);
71 |
72 | const customStyleTag = document.createElement('style');
73 | customStyleTag.id = CUSTOM_CSS_STYLE_TAG_ID;
74 | document.head.appendChild(customStyleTag);
75 | };
76 |
77 | const injectScriptTag = () => {
78 | if (!!document.getElementById(MAINJS_SCRIPT_TAG_ID)) {
79 | return;
80 | }
81 | const scriptTag = document.createElement('script');
82 | scriptTag.id = MAINJS_SCRIPT_TAG_ID;
83 | const jsFilePath = chrome.runtime.getURL('/js/main.js');
84 | scriptTag.setAttribute('src', jsFilePath);
85 | document.querySelector('body').appendChild(scriptTag);
86 | };
87 |
88 | // In Manifest V3 we can't use inline scripts so as a work around we pass options by exposing into meta content
89 | const injectOptionsAsMetaContent = (extensionOptions = {}) => {
90 | const meta = document.createElement('meta');
91 | meta.name = 'extension-options';
92 | meta.content = JSON.stringify(extensionOptions);
93 | document.head.appendChild(meta);
94 | };
95 |
96 | const initApplication = (options = {}) => {
97 | addBodyTagIfMissing();
98 | addHeadTagIfMissing();
99 | injectCssUrlAndStyleTag();
100 | injectScriptTag();
101 | injectOptionsAsMetaContent(options);
102 | };
103 |
104 | const applyOptions = (options) => {
105 | const themes = {
106 | default: 'dark-pro.css',
107 | mdn: 'mdn-light.css',
108 | };
109 | const styleNode = document.getElementById(COLOR_THEME_LINK_TAG_ID);
110 | const colorThemeStylesheetUrl = themes[options.theme] || themes['default'];
111 | const cssURL = chrome.runtime.getURL(
112 | '/css/color-themes/' + colorThemeStylesheetUrl,
113 | );
114 |
115 | if (styleNode.href.indexOf(colorThemeStylesheetUrl) < 0) {
116 | styleNode.setAttribute('href', cssURL);
117 | }
118 | document.getElementById('custom-css').textContent = options.css || '';
119 | };
120 |
121 | const renderApplicationWithURLFiltering = (options) => {
122 | const urls = (options || {}).filteredURL || [];
123 | const isURLBlocked = urls.some((url) =>
124 | window.location.href.startsWith(url),
125 | );
126 |
127 | if (
128 | !isURLBlocked &&
129 | (matchesContentType(options) ||
130 | (isJSONContentDetectionEnabled(options) &&
131 | doesPageContainsValidJSON()))
132 | ) {
133 | initApplication(options);
134 | applyOptions(options);
135 | window.JSON_VIEWER_PRO_INITIALISED = true;
136 | }
137 | };
138 |
139 | const matchesContentType = (extensionOptions) => {
140 | return (
141 | extensionOptions.jsonDetection.method === 'contentType' &&
142 | extensionOptions.jsonDetection.selectedContentTypes.includes(
143 | document.contentType,
144 | )
145 | );
146 | };
147 |
148 | const isJSONContentDetectionEnabled = (extensionOptions) => {
149 | return extensionOptions.jsonDetection.method === 'jsonContent';
150 | };
151 |
152 | const doesPageContainsValidJSON = () => {
153 | try {
154 | const textContent = getCleanTextContent();
155 | JSON.parse(textContent);
156 | return true;
157 | } catch (error) {
158 | return false;
159 | }
160 | };
161 |
162 | const isContentTypeDetectionEnabled = (extensionOptions) => {
163 | return extensionOptions.jsonDetection.method === 'contentType';
164 | };
165 |
166 | const messageReceiver = () => {
167 | chrome.runtime.onMessage.addListener((message) => {
168 | switch (message.action) {
169 | case 'options_received':
170 | window.extensionOptions = message.options;
171 | renderApplicationWithURLFiltering(message.options);
172 | break;
173 |
174 | case 'settings_updated':
175 | const previousOptions = window.extensionOptions;
176 | const newOptions = message.options;
177 |
178 | const contentTypeDetectionEnabled =
179 | (isContentTypeDetectionEnabled(previousOptions) ||
180 | isContentTypeDetectionEnabled(newOptions)) &&
181 | isDocumentContentTypeJSON();
182 |
183 | const jsonContentDetectionEnabled =
184 | isJSONContentDetectionEnabled(previousOptions) ||
185 | isJSONContentDetectionEnabled(newOptions);
186 |
187 | // window.extensionOptions is the previsous state once options are updated
188 | // Previsously rendered page needs to be reloaded to reflect updated options
189 | if (
190 | isJsonViewerLoaded() ||
191 | contentTypeDetectionEnabled ||
192 | jsonContentDetectionEnabled
193 | ) {
194 | window.location.reload();
195 | }
196 | break;
197 |
198 | case 'rb_download_json':
199 | location.hash = 'downloadJSON=true';
200 | break;
201 |
202 | default:
203 | break;
204 | }
205 | });
206 | };
207 |
208 | messageReceiver();
209 |
210 | // alternative to DOMContentLoaded event
211 | document.onreadystatechange = function () {
212 | if (document.readyState === 'interactive') {
213 | chrome.runtime.sendMessage({ action: 'give_me_options' });
214 | }
215 | };
216 |
--------------------------------------------------------------------------------
/src/TreeView.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, createRef } from 'react';
2 | import $ from 'jquery';
3 | var jQuery = $;
4 | import { initPlugin } from './utils/json-viewer/json-viewer.js';
5 | import { iconFillColor } from './utils/common';
6 | import SearchBar from './components/Searchbar';
7 | import CopyIcon from './components/Icons/Copy';
8 |
9 | import './utils/json-viewer/json-viewer.css';
10 |
11 | class TreeView extends Component {
12 | constructor(props) {
13 | super(props);
14 | this.state = {
15 | top: 0,
16 | showCopier: false,
17 | actualPath: null,
18 | value: null,
19 | data: props.data,
20 | };
21 | this.jsonRenderer = createRef();
22 | this.changeCopyIconLocation = this.changeCopyIconLocation.bind(this);
23 | this.toggleSection = this.toggleSection.bind(this);
24 | }
25 |
26 | copy(event, type) {
27 | event.preventDefault();
28 | let context;
29 | if (type === 'path') {
30 | context = this.state.actualPath;
31 | } else {
32 | context = this.state.value;
33 | }
34 | let selElement, selRange, selection;
35 | selElement = document.createElement('span');
36 | selRange = document.createRange();
37 | selElement.innerText = context;
38 | document.body.appendChild(selElement);
39 | selRange.selectNodeContents(selElement);
40 | selection = window.getSelection();
41 | selection.removeAllRanges();
42 | selection.addRange(selRange);
43 | document.execCommand('Copy');
44 | document.body.removeChild(selElement);
45 | }
46 |
47 | changeCopyIconLocation(e) {
48 | const self = this;
49 | this.findPath(self, e);
50 | self.setState({
51 | top: $(e.target).offset().top,
52 | showCopier: true,
53 | });
54 | return false;
55 | }
56 |
57 | getArrayIndex(path) {
58 | const arrayIndexBracketStartAt = path.lastIndexOf('[');
59 | const arrayIndexBracketEndAt = path.lastIndexOf(']');
60 | if (arrayIndexBracketStartAt > -1) {
61 | return path.substring(
62 | arrayIndexBracketStartAt + 1,
63 | arrayIndexBracketEndAt,
64 | );
65 | }
66 | return path;
67 | }
68 |
69 | createValidPath(pathArray) {
70 | let path = '';
71 | pathArray.forEach((item, index) => {
72 | if (index === 0) {
73 | path = path.concat(item);
74 | } else {
75 | if (item.indexOf('-') > -1 || item.indexOf(' ') > -1) {
76 | path = `${path}['${item}']`;
77 | } else if (isNaN(item) === false) {
78 | path = `${path}[${item}]`;
79 | } else {
80 | path = path.concat('.').concat(item);
81 | }
82 | }
83 | });
84 | return path;
85 | }
86 |
87 | findPath(self, e) {
88 | let keys = [];
89 | let keyValueString = $(e.target).parents('li').first().text();
90 | let firstIndexOfColone = keyValueString.indexOf(':');
91 | let value = keyValueString.substring(firstIndexOfColone + 1);
92 | let nodes = $(e.target).parentsUntil('#json-viewer');
93 | $(nodes).each(function (i, node) {
94 | if (
95 | $(node).get(0).tagName == 'LI' &&
96 | $(node).parent()[0].tagName == 'UL'
97 | ) {
98 | let parentKey = $(node).find('span.property').eq(0).text();
99 | keys.push(self.getArrayIndex(parentKey.replace(/\"+/g, '')));
100 | }
101 |
102 | if (
103 | $(node).get(0).tagName == 'LI' &&
104 | $(node).parent()[0].tagName == 'OL'
105 | ) {
106 | var parentKey =
107 | $(node)
108 | .parent('OL')
109 | .parent('li')
110 | .find('span.property')
111 | .eq(0)
112 | .text() +
113 | '[' +
114 | $(node).index() +
115 | ']';
116 | keys.push(self.getArrayIndex(parentKey.replace(/\"+/g, '')));
117 | }
118 | });
119 |
120 | if (value[value.length - 1] === ',') {
121 | value = value.substring(0, value.length - 1);
122 | }
123 | self.setState({
124 | actualPath: self.createValidPath(keys.reverse()),
125 | value,
126 | });
127 | }
128 |
129 | toggleSection(e) {
130 | e.preventDefault();
131 | e.stopPropagation();
132 | const carretIcon = $(e.currentTarget);
133 | const collapsibleNode = carretIcon.siblings('ul.json-dict, ol.json-array');
134 |
135 |
136 | // Going to toggle the class and visibility of the carret icon
137 | // Class will be set to 'collapsed' and collapsibleNode will have style display none since it's visible
138 | const isVislbe = collapsibleNode.get(0).checkVisibility();
139 | if (isVislbe) {
140 | const count = collapsibleNode.children('li').length;
141 | const placeholder = count + (count > 1 ? ' items' : ' item');
142 | collapsibleNode.after(
143 | '' + placeholder + '',
144 | );
145 | } else {
146 | collapsibleNode.siblings('.json-placeholder').remove();
147 | }
148 |
149 | collapsibleNode.toggle();
150 | carretIcon.toggleClass('collapsed');
151 | }
152 |
153 | componentDidUpdate(prevProps) {
154 | if (prevProps.data !== this.props.data) {
155 | this.reRenderTree();
156 | }
157 | }
158 |
159 | reRenderTree(json) {
160 | this.cleanUpEventListeners();
161 | this.renderJSONTree(json);
162 | window.scrollTo(0, 0);
163 | if (this.state.showCopier) {
164 | this.setState({
165 | showCopier: false,
166 | });
167 | }
168 | }
169 |
170 | restoreOriginalJSON() {
171 | this.reRenderTree(this.props.data);
172 | }
173 |
174 | renderJSONTree(json) {
175 | const data = json ?? this.props.data;
176 | this.$node = $(this.jsonRenderer.current);
177 | if ($) {
178 | const pluginOptions = {
179 | collapsed: window.extensionOptions?.collapsed === 1,
180 | withQuotes: true,
181 | };
182 | initPlugin(this.$node, $, data, pluginOptions);
183 | $(document).on(
184 | 'click',
185 | 'span.property',
186 | this.changeCopyIconLocation,
187 | );
188 | $(document).off('click', 'a.json-toggle');
189 | $(document).on('click', 'a.json-toggle', this.toggleSection);
190 | }
191 | }
192 |
193 | componentDidMount() {
194 | this.renderJSONTree();
195 | }
196 |
197 | cleanUpEventListeners() {
198 | $(document).off('click', 'span.property', this.changeCopyIconLocation);
199 | $(document).off('click', 'a.json-toggle', this.toggleSection);
200 | }
201 |
202 | componentWillUnmount() {
203 | this.cleanUpEventListeners();
204 | }
205 |
206 | render() {
207 | window.json = this.props.data;
208 | return (
209 |
210 |
217 |
221 |
233 |
234 |
235 | {this.props.isSearchBarVisible && (
236 |
243 | )}
244 |
245 | );
246 | }
247 | }
248 |
249 | export default TreeView;
250 |
--------------------------------------------------------------------------------
/src/css/style.scss:
--------------------------------------------------------------------------------
1 | @use './color-themes/dark-pro.css';
2 |
3 | body {
4 | background-color: var(--bg-color);
5 | color: var(--primary-text-color);
6 | font-size: var(--base-font-size);
7 | margin: 0;
8 | padding: 0;
9 | font-family: var(--primary-font-family);
10 | }
11 |
12 | .tab-container {
13 | margin: 20px;
14 | margin-left: 40px;
15 | }
16 |
17 | .breadcumb {
18 | position: fixed;
19 | top: 20px;
20 | left: 20px;
21 | display: inline-flex;
22 | border: 1px solid var(--breadcumb-border-color);
23 | background-color: var(--breadcumb-bg-color);
24 | border-radius: 3px;
25 | padding: 0px;
26 | user-select: none;
27 |
28 | > ul {
29 | margin: 0;
30 | padding: 0;
31 | max-width: 600px;
32 |
33 | & > li {
34 | list-style: none;
35 | display: inline-block;
36 | }
37 | }
38 | }
39 |
40 | .breadcumb > ul > li > a {
41 | display: block;
42 | padding: 10px;
43 | padding-right: 0px;
44 | text-decoration: none;
45 | color: var(--primary-text-color);
46 | width: auto;
47 | border-right: none;
48 | font-size: 14px;
49 | border-right: none;
50 | border-left: none;
51 | cursor: pointer;
52 |
53 | &:hover {
54 | color: var(--breadcumb-item-hover-color);
55 | }
56 | }
57 |
58 | .breadcumb > ul > li:last-child > a {
59 | padding-right: 10px;
60 | }
61 |
62 | .breadcumb > ul > li:first-child > a {
63 | padding-left: 10px;
64 | }
65 |
66 | .breadcumb > ul > li > a::after {
67 | content: '\276F'; /* left arrow */
68 | color: var(--breadcumb-arrow-color);
69 | cursor: auto;
70 | margin-left: 7px;
71 | }
72 |
73 | .breadcumb > ul > li:last-child > a::after {
74 | content: ''; /* no arrow after last child*/
75 | }
76 |
77 | .copy-breadcumb-btn {
78 | text-align: center;
79 | align-items: center;
80 | display: flex;
81 | padding: 0 7px;
82 | border-right: 1px solid var(--breadcumb-border-color);
83 | cursor: pointer;
84 |
85 | img.path-copy-icon {
86 | filter: var(--copy-path-btn-img-filter);
87 | }
88 | }
89 |
90 | .action-area {
91 | width: auto;
92 | position: fixed;
93 | top: 20px;
94 | right: 10px;
95 | user-select: none;
96 | }
97 |
98 | .action-area > ul.menus {
99 | margin: 0;
100 | padding: 0;
101 |
102 | li {
103 | list-style: none;
104 | display: inline-block;
105 | overflow: hidden;
106 | margin-right: 6px;
107 |
108 | > a {
109 | display: inline-flex;
110 | padding: 8px 10px;
111 | text-decoration: none;
112 | color: var(--primary-text-color);
113 | width: auto;
114 | border-right: none;
115 | font-size: 12px;
116 | font-weight: bold;
117 | align-items: center;
118 | border: 1px solid var(--menu-item-border-color);
119 | background-color: var(--menu-item-bg-color);
120 | border-radius: 6px;
121 | img {
122 | filter: var(--menu-item-img-filter);
123 | }
124 | }
125 | }
126 | }
127 |
128 | .rotate-90 {
129 | transform: rotate(90deg);
130 | }
131 |
132 | .rotate-270 {
133 | transform: rotate(270deg);
134 | }
135 |
136 | .sm-icon {
137 | width: 20px;
138 | height: 20px;
139 | }
140 |
141 | .sm-icon:has(+ span.menu-label) {
142 | margin-right: 5px;
143 | }
144 |
145 | .sm-icon.option-icon {
146 | width: 24px;
147 | height: 24px;
148 | }
149 |
150 | .action-area > ul.menus > li.active > a {
151 | border: 1px solid var(--menu-item-active-border-color);
152 | background-color: var(--menu-item-active-bg-color);
153 | }
154 |
155 | .action-area > ul.menus > li > a.option-menu {
156 | padding: 6px !important;
157 | }
158 |
159 | ul.json-dict li {
160 | width: auto;
161 | display: block;
162 | line-height: 180%;
163 | }
164 |
165 | .copyable {
166 | background: var(--copyable-property-bg-color);
167 | width: auto;
168 | border: 1px dotted var(--copyable-property-border-color);
169 | }
170 |
171 | .copyable > ul.json-dict,
172 | .copyable > ol.json-array {
173 | border-left: none;
174 | }
175 |
176 | .property {
177 | color: var(--property-color); /*#3DAAE0;*/
178 | line-height: 160%;
179 | font-weight: bold;
180 | }
181 |
182 | .json-literal-numeric {
183 | color: var(--json-literal-numeric);
184 | }
185 |
186 | .json-literal-string {
187 | color: var(--json-literal-string);
188 | white-space: normal;
189 | }
190 |
191 | .json-literal-url {
192 | color: var(--json-literal-url);
193 | white-space: normal;
194 | }
195 |
196 | .json-literal-boolean,
197 | .json-literal {
198 | color: var(--json-literal-and-boolean);
199 | }
200 |
201 | .nodeCircle {
202 | fill: var(--json-chart-circle-bg);
203 | }
204 |
205 | .chart-holder {
206 | font-size: 14px;
207 | }
208 |
209 | .json-input-section {
210 | width: 100%;
211 | }
212 |
213 | .json-input-section h1 {
214 | color: var(--primary-text-color);
215 | font-size: 30px;
216 | font-weight: 100;
217 | line-height: 110%;
218 | text-align: center;
219 | }
220 |
221 | .save-btn-area {
222 | text-align: center;
223 | }
224 |
225 | .save-btn-area button:not(:first-child) {
226 | margin-left: 1em;
227 | }
228 |
229 | .json-input-error-msg {
230 | color: var(--primary-text-color);
231 | padding: 12px 15px;
232 | background-color: var(--error-msg-bg-color);
233 | margin: 0 auto;
234 | box-shadow: 0 7px 7px 0 #111;
235 | border: 1px solid rgb(201, 9, 9);
236 | }
237 |
238 | .copier {
239 | background: transparent;
240 | padding: 3px 3px 0px 3px;
241 | display: none;
242 | top: 10px;
243 | left: 10px;
244 | position: absolute;
245 | z-index: 1001;
246 | box-sizing: border-box;
247 | }
248 |
249 | .copy-btn {
250 | width: 26px;
251 | height: auto;
252 | margin-top: -5px;
253 | }
254 |
255 | ul.copyMenu {
256 | left: 30px;
257 | top: -20px;
258 | padding: 0;
259 | list-style: none;
260 | position: absolute;
261 | z-index: 1001;
262 | display: none;
263 | border: 1px solid var(--dropdown-menu-item-border-color);
264 | }
265 |
266 | .copier:hover > ul.copyMenu {
267 | display: block;
268 | }
269 |
270 | ul.copyMenu::before {
271 | width: 20px;
272 | height: 20px;
273 | content: '';
274 | transform: rotate(45deg);
275 | background: var(--dropdown-bg-primary);
276 | position: absolute;
277 | top: 4px;
278 | left: -10px;
279 | z-index: -100;
280 | border: 1px solid var(--dropdown-menu-item-border-color);
281 | }
282 |
283 | ul.copyMenu {
284 | box-shadow: 0 5px 10px 0 rgba(6, 6, 6, 0.8);
285 |
286 | li > a {
287 | background: var(--dropdown-bg-primary);
288 | color: var(--primary-text-color);
289 | font-size: 12px;
290 | font-weight: bold;
291 | width: 100px;
292 | padding: 6px 8px;
293 | border-bottom: 1px solid var(--dropdown-menu-item-border-color);
294 | display: block;
295 | cursor: pointer;
296 |
297 | &:hover {
298 | background: var(--dropdown-menu-item-bg);
299 | color: var(--primary-text-color);
300 | }
301 | }
302 |
303 | li:first-child {
304 | border-radius: 4px 4px 0 0;
305 | }
306 |
307 | li:last-child {
308 | border-radius: 0 0 4px 4px;
309 | & > a {
310 | border-bottom: none;
311 | }
312 | }
313 | }
314 |
315 | .d-none {
316 | display: none !important;
317 | }
318 |
319 | text.nodeText {
320 | color: var(--primary-text-color);
321 | }
322 |
323 | .jv-btn {
324 | border: 1px solid var(--btn-border-color);
325 | font-size: 15px;
326 | border-radius: 4px;
327 | background-color: var(--btn-bg-color);
328 | }
329 |
330 | .jv-btn-md {
331 | padding: 10px 16px;
332 | font-size: 16px;
333 | border-radius: 4px;
334 | }
335 |
336 | .jv-btn-sm {
337 | padding: 6px 12px;
338 | font-size: 14px;
339 | border-radius: 4px;
340 | }
341 |
342 | .jv-btn-default {
343 | background-color: var(--btn-default-bg-color);
344 | color: var(--primary-text-color);
345 | }
346 |
347 | .jv-btn-primary {
348 | background-color: var(--btn-primary-bg-color);
349 | color: var(--btn-primary-text-color);
350 | }
351 |
352 | .jv-align-start {
353 | display: flex;
354 | flex-direction: row;
355 | justify-content: flex-start;
356 | }
357 |
358 | .jv-align-right {
359 | display: flex;
360 | flex-direction: row;
361 | justify-content: flex-end;
362 | }
363 |
364 | .jv-width-half {
365 | width: 50%;
366 | }
367 |
368 | .debug-cmd {
369 | font-size: 16px;
370 | margin: 5px;
371 | margin-left: 15px;
372 | }
373 |
374 | .sm-btn-icon {
375 | margin-right: 10px;
376 | display: inline-flex;
377 | align-items: center;
378 | }
379 |
380 | .inline-flex {
381 | display: inline-flex;
382 | align-items: center;
383 | }
384 |
385 | .search-icon-btn {
386 | font-size: 20px !important;
387 | padding: 8px !important;
388 | text-align: center;
389 | box-sizing: border-box;
390 | }
391 |
392 | .path-copy-icon {
393 | user-select: none;
394 | }
395 |
396 | .disabled-btn {
397 | user-select: none;
398 | -moz-user-select: none;
399 | pointer-events: none;
400 | }
401 |
402 | #json-viewer{
403 | margin-top:50px;
404 | }
405 |
--------------------------------------------------------------------------------
/src/components/Searchbar/index.jsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useRef, useState } from 'react';
2 | import { JSONPath } from 'jsonpath-plus';
3 |
4 | import './style.scss';
5 | import Select from '../Select';
6 | import { FiDelete } from 'react-icons/fi';
7 |
8 | const JSON_PATH_SEARCH_PATTERN_FOR_AUTO_SUGGESTION = /(\.|\[|\"|\'|\[")/;
9 |
10 | const makeListItem = (items, searchFlag) => {
11 | let filteredList = items;
12 | if (searchFlag.length > 0) {
13 | filteredList = items.filter((item) => item.startsWith(searchFlag));
14 | }
15 | return filteredList.map((listItem) => ({
16 | label: listItem,
17 | selected: false,
18 | }));
19 | };
20 |
21 | const SearchBar = ({ json, renderJSON, restoreOriginalJSON }) => {
22 | const searchInputRef = useRef();
23 | const [searchText, setSearchText] = useState('');
24 | const [searchInfo, setSearchInfo] = useState('');
25 | const [suggestions, setSuggestions] = useState([]);
26 | const [filteredSuggestions, setFilteredSuggestions] = useState([]);
27 | const [showSuggestion, setShowSuggestion] = useState(false);
28 | const [isJsonModified, setIsJsonModified] = useState(false);
29 |
30 | const parseViaJSONPath = (path) => {
31 | let jsonPath = path;
32 | if (jsonPath.startsWith('[')) {
33 | jsonPath = `$.${jsonPath}`;
34 | } else if (jsonPath.startsWith('.')) {
35 | jsonPath = `\$${jsonPath}`;
36 | }
37 | const result = JSONPath({
38 | path: jsonPath,
39 | json,
40 | eval: false,
41 | });
42 | if (result && Array.isArray(result) && result.length > 0) {
43 | return result[0];
44 | }
45 | };
46 |
47 | const onInputChange = (e) => {
48 | let path = e.target.value;
49 | if (path.length === 0) {
50 | onSearchTextClear();
51 | }
52 | setSearchText(path);
53 | setShowSuggestion(true);
54 | setSearchInfo('');
55 | try {
56 | const matchedDelemeterParts =
57 | path.endsWith('.') ||
58 | path.endsWith("'") ||
59 | path.endsWith('"') ||
60 | path.endsWith('[') ||
61 | path.endsWith('["');
62 |
63 | if (!matchedDelemeterParts) {
64 | return;
65 | }
66 | const resolvedPathValue = parseViaJSONPath(path);
67 | if (resolvedPathValue) {
68 | let suggestions = [];
69 | if (!Array.isArray(resolvedPathValue)) {
70 | suggestions = Object.keys(resolvedPathValue);
71 | } else {
72 | suggestions = new Array(resolvedPathValue.length)
73 | .fill(0)
74 | .map((_, i) => String(i));
75 | }
76 | setSuggestions(suggestions);
77 | } else {
78 | setSuggestions([]);
79 | }
80 | setSearchInfo('');
81 | } catch (e) {
82 | console.error('failed to parse json path:', e);
83 | setSearchInfo(
84 | 'Failed to retrieve value from the Path you provided',
85 | );
86 | }
87 | };
88 |
89 | const onkeyDown = useCallback(
90 | (e) => {
91 | // tab key press
92 | if (e.which === 9 && showSuggestion) {
93 | e.preventDefault();
94 | return;
95 | }
96 | if (e.which === 13) {
97 | const isSuggestionDropDownMenuActive =
98 | showSuggestion && filteredSuggestions.length > 0;
99 | if (
100 | searchText.length === '' ||
101 | isSuggestionDropDownMenuActive
102 | ) {
103 | return;
104 | }
105 |
106 | try {
107 | const resolvedPathValue = parseViaJSONPath(searchText);
108 | if (typeof resolvedPathValue === 'undefined') {
109 | setSearchInfo(
110 | 'Failed to retrieve value from the Path you provided',
111 | );
112 | return;
113 | }
114 | const newJsonToRender = {
115 | [searchText]: resolvedPathValue,
116 | };
117 | if (typeof renderJSON === 'function') {
118 | renderJSON(newJsonToRender);
119 | setIsJsonModified(true);
120 | }
121 | } catch (error) {
122 | setSearchInfo(
123 | 'Failed to retrieve value from the Path you provided',
124 | );
125 | }
126 | }
127 | if (e.which === 40 || e.which === 38) {
128 | e.preventDefault();
129 | }
130 | },
131 | [showSuggestion, filteredSuggestions, searchText, renderJSON],
132 | );
133 |
134 | useEffect(() => {
135 | const searchParts = searchText.split(
136 | JSON_PATH_SEARCH_PATTERN_FOR_AUTO_SUGGESTION,
137 | );
138 | const searchFlag =
139 | searchParts && searchParts.length > 0
140 | ? searchParts[searchParts.length - 1].trim()
141 | : '';
142 | const filteredSuggestedItems = makeListItem(suggestions, searchFlag);
143 | setFilteredSuggestions(filteredSuggestedItems);
144 | }, [suggestions, searchText]);
145 |
146 | const onSuggestionSelected = (value) => {
147 | const delemeterCompletionPairs = {
148 | "'": "'",
149 | '"': '"',
150 | '[': ']',
151 | '.': '',
152 | };
153 | const searchParts = searchText.split(
154 | JSON_PATH_SEARCH_PATTERN_FOR_AUTO_SUGGESTION,
155 | );
156 | if (searchParts && searchParts.length > 1) {
157 | let pathTillPathDelimeter = searchText.substring(
158 | 0,
159 | searchText.length - searchParts[searchParts.length - 1].length,
160 | );
161 | const startingDelemeter = searchParts[searchParts.length - 2];
162 | const closingDelemeter =
163 | startingDelemeter &&
164 | startingDelemeter in delemeterCompletionPairs
165 | ? delemeterCompletionPairs[startingDelemeter]
166 | : '';
167 | const completePathWithAutoSuggestionApplied = `${pathTillPathDelimeter}${value}${closingDelemeter}`;
168 | setSearchText(completePathWithAutoSuggestionApplied);
169 | searchInputRef.current.focus();
170 | searchInputRef.current.scroll(
171 | searchInputRef.current.scrollWidth,
172 | 0,
173 | );
174 | searchInputRef.current.setSelectionRange(
175 | completePathWithAutoSuggestionApplied.length,
176 | completePathWithAutoSuggestionApplied.length,
177 | );
178 | }
179 | setShowSuggestion(false);
180 | };
181 |
182 | const onSuggestionDropdownClosed = () => {
183 | setShowSuggestion(false);
184 | };
185 |
186 | const resetSuggestions = () => {
187 | setSearchText('');
188 | setShowSuggestion(false);
189 | setSuggestions([]);
190 | };
191 |
192 | const onSearchTextClear = () => {
193 | resetSuggestions();
194 | if (searchInfo) {
195 | setSearchInfo('');
196 | }
197 |
198 | if (isJsonModified) {
199 | restoreOriginalJSON();
200 | setIsJsonModified(false);
201 | }
202 | };
203 |
204 | useEffect(() => restoreOriginalJSON, []);
205 |
206 | return (
207 |
208 |
209 |
setShowSuggestion(true)}
220 | autoComplete="off"
221 | autoCorrect="off"
222 | />
223 |
233 |
234 | {filteredSuggestions &&
235 | filteredSuggestions.length > 0 &&
236 | showSuggestion && (
237 |
238 |
246 |
247 | )}
248 |
249 | {searchInfo && (
250 |
251 | Invalid JSON Path
252 |
253 | )}
254 |
255 | );
256 | };
257 |
258 | export default SearchBar;
259 |
--------------------------------------------------------------------------------
/src/components/Editor/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component, createRef } from 'react';
2 | import { basicSetup, EditorView } from 'codemirror';
3 | import { githubDarkInit } from '@uiw/codemirror-theme-github';
4 | import { json } from '@codemirror/lang-json';
5 | import { yaml } from '@codemirror/lang-yaml';
6 | import { xml } from '@codemirror/lang-xml';
7 |
8 | import {
9 | convertContent,
10 | convertToJsObject,
11 | trimXMLArrayRoot,
12 | } from '../../utils/convertContent';
13 | import { FiXCircle } from 'react-icons/fi';
14 |
15 | import downloadFile from '../../utils/dowloadFile';
16 | import { currentDateTime } from '../../utils/datetime';
17 | import { calculateFileSize } from '../../utils/common';
18 | import Logo from '../Logo';
19 | import Toolbar from './Toolbar';
20 | import './style.css';
21 |
22 | import { CSP_NONCE } from '../../constants/common';
23 |
24 | const editorLangMap = {
25 | json,
26 | xml,
27 | yaml,
28 | };
29 |
30 | class Editor extends Component {
31 | editorRef = null;
32 | codeMirror = null;
33 | constructor(props) {
34 | super(props);
35 | this.editorRef = createRef();
36 | this.codeMirror = createRef();
37 | this.state = {
38 | contentType: 'JSON',
39 | validationError: '',
40 | footerMessage: '',
41 | json: JSON.stringify(props.json, null, 4),
42 | cursorPosition: { line: 1, col: 0 },
43 | };
44 | }
45 |
46 | setCursorPosition = (selection) => {
47 | this.setState({
48 | cursorPosition: selection,
49 | });
50 | };
51 |
52 | showParseError(contentType) {
53 | this.setState({
54 | validationError: `Failed to parse the data as ${contentType}. Please check if you have entered valid ${this.state.contentType}`,
55 | });
56 | }
57 |
58 | setFooterMessage(msg) {
59 | this.setState({
60 | footerMessage: msg,
61 | });
62 | }
63 |
64 | onContentTypeChange(contentType) {
65 | if (this.state.contentType !== contentType) {
66 | const content = this.codeMirror.current.state.doc.toString().trim();
67 | if (!content) {
68 | return;
69 | }
70 | try {
71 | const result = convertContent(
72 | content,
73 | this.state.contentType,
74 | contentType,
75 | );
76 | this.codeMirror.current.destroy();
77 | this.initCodeMirror(result, contentType);
78 | this.resetErrors();
79 | } catch (e) {
80 | this.showParseError(contentType);
81 | return;
82 | }
83 | }
84 | this.setState({
85 | contentType,
86 | });
87 | }
88 |
89 | parseJSON(initialJSON = null) {
90 | this.resetErrors();
91 | let content =
92 | initialJSON && typeof initialJSON === 'string'
93 | ? initialJSON
94 | : this.codeMirror.current.state.doc.toString().trim();
95 | if (!content) {
96 | return;
97 | }
98 | const contentType = this.state.contentType ?? 'JSON';
99 | try {
100 | let json = convertToJsObject(content, contentType);
101 | json = trimXMLArrayRoot(json);
102 | this.props.renderJSON(json);
103 | } catch (e) {
104 | this.showParseError(contentType);
105 | }
106 | }
107 |
108 | showFileDialog() {
109 | const fileInput = document.getElementById('fileInput');
110 | if (fileInput) {
111 | fileInput.click();
112 | }
113 | }
114 |
115 | saveEditorContentAsFile() {
116 | const fileExtension = this.state.contentType.toLowerCase();
117 | downloadFile(
118 | this.codeMirror.current.state.doc.toString().trim(),
119 | 'text/' + fileExtension,
120 | `data-${currentDateTime()}.${fileExtension}`,
121 | );
122 | }
123 |
124 | handleFileInputChange(event) {
125 | const { files } = event.target;
126 | if (files.length) {
127 | var reader = new FileReader();
128 | const fileToRead = files[0];
129 | let contentType = fileToRead.type.split('/')?.[1]?.toLowerCase();
130 | if (['x-yaml', 'yml'].includes(contentType)) {
131 | contentType = 'yaml';
132 | }
133 | reader.onload = (file) => {
134 | this.codeMirror.current.destroy();
135 | this.initCodeMirror(file.target.result, contentType);
136 |
137 | this.setState({
138 | contentType: contentType.toUpperCase(),
139 | });
140 | };
141 | reader.readAsText(fileToRead);
142 | }
143 | }
144 |
145 | resetErrors() {
146 | this.setState({
147 | validationError: '',
148 | });
149 | }
150 |
151 | initCodeMirror = (data, contentType = 'JSON') => {
152 | const contentTypeInLowerCase = contentType.toLowerCase();
153 | const editorLanguageParser =
154 | editorLangMap[contentTypeInLowerCase] ?? json;
155 | if (this.editorRef.current) {
156 | this.codeMirror.current = new EditorView({
157 | doc: data,
158 | extensions: [
159 | basicSetup,
160 | githubDarkInit({
161 | settings: {
162 | background: '#070707',
163 | gutterBackground: '#111111',
164 |
165 | // background: '#fff',
166 | // gutterBackground: '#efefef',
167 | },
168 | }),
169 | editorLanguageParser(),
170 | EditorView.updateListener.of((update) => {
171 | const position = update.view.state.doc.lineAt(
172 | update.view.state.selection.main.head,
173 | );
174 | this.setCursorPosition({
175 | line: position.number,
176 | col:
177 | update.view.state.selection.main.head -
178 | position.from,
179 | });
180 | }),
181 | EditorView.cspNonce.of(CSP_NONCE),
182 | ],
183 | parent: this.editorRef.current,
184 | });
185 | }
186 |
187 | this.codeMirror.current.dom.style.height = '600px';
188 | };
189 |
190 | componentDidMount() {
191 | this.initCodeMirror(this.state.json);
192 | }
193 |
194 | componentWillUpdate(prevProps, nextState) {
195 | if (nextState.contentType === 'XML' && !this.state.footerMessage) {
196 | this.setFooterMessage(
197 | `Conversion between XML and JSON may lead to unexpected 'null' to empty string ("") or number to string type casting.`,
198 | );
199 | } else if (
200 | nextState.contentType !== 'XML' &&
201 | this.state.footerMessage
202 | ) {
203 | this.setFooterMessage('');
204 | }
205 | }
206 |
207 | render() {
208 | const totalNumberOfBytes = new Blob([
209 | this.codeMirror?.current?.state?.doc?.toString()?.trim() ?? '',
210 | ]).size;
211 | return (
212 | <>
213 |
214 |
215 |
224 |
225 | {this.state.validationError && (
226 |
227 | {this.state.validationError}
228 |
235 |
236 | )}
237 |
238 |
242 |
249 |
250 |
251 |
252 | {this.state.footerMessage}
253 |
254 |
255 |
256 | Line {this.state.cursorPosition.line}, Col{' '}
257 | {this.state.cursorPosition.col}
258 | {' '}
259 |
260 | Total Bytes:{' '}
261 | {calculateFileSize(totalNumberOfBytes)}
262 |
263 |
264 |
265 |
266 |
267 | >
268 | );
269 | }
270 | }
271 |
272 | export default Editor;
273 |
--------------------------------------------------------------------------------