From 342bcf3db0379a46880e994eb8f22e4ab1c95c44 Mon Sep 17 00:00:00 2001 From: pangl Date: Sat, 15 Feb 2025 17:51:40 +0800 Subject: [PATCH 1/3] feat: Support PureJsonView component --- dev-server/dist/index.html | 46 ++++ dev-server/src/JsonString.js | 200 +++++++++++++++ dev-server/src/index.js | 330 ++----------------------- dev-server/src/object.test-data.js | 120 +++++++++ dev-server/src/render.js | 236 ++++++++++++++++++ dev-server/src/string.test-data.js | 21 ++ index.d.ts | 10 + package.json | 2 + src/js/ReactJsonView.js | 272 ++++++++++++++++++++ src/js/ReactPureJsonView.js | 84 +++++++ src/js/helpers/bigNumberUtil.js | 69 ++++++ src/js/index.js | 276 +-------------------- test/tests/js/Index-jsonstring-test.js | 52 ++++ 13 files changed, 1141 insertions(+), 577 deletions(-) create mode 100644 dev-server/src/JsonString.js create mode 100644 dev-server/src/object.test-data.js create mode 100644 dev-server/src/render.js create mode 100644 dev-server/src/string.test-data.js create mode 100644 src/js/ReactJsonView.js create mode 100644 src/js/ReactPureJsonView.js create mode 100644 src/js/helpers/bigNumberUtil.js create mode 100644 test/tests/js/Index-jsonstring-test.js diff --git a/dev-server/dist/index.html b/dev-server/dist/index.html index 945bb6e..90116df 100644 --- a/dev-server/dist/index.html +++ b/dev-server/dist/index.html @@ -29,6 +29,42 @@ src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js" > +
+ + Index + | + Json[ReactPureJsonView] + vs. + Json String[ReactPureJsonView] + +
+

diff --git a/dev-server/src/JsonString.js b/dev-server/src/JsonString.js new file mode 100644 index 0000000..06d1bf4 --- /dev/null +++ b/dev-server/src/JsonString.js @@ -0,0 +1,200 @@ +'use strict' + +//import react and reactDom for browser rendering +import React from 'react' + +// import test json data +import { + getExampleArray, + getExampleJson1, + getExampleJson2, + getExampleJson3, + getExampleJson4, + getExampleWithStringEscapeSequences +} from './test.json-data' + +//import the react-json-view component (installed with npm) +import JsonViewer, { defaultBigNumberImplement as BigNumber} from './../../src/js/index' + +export default ( +
+ {/* just pass in your JSON to the src attribute */} + { + console.log('edit callback', e) + if (e.new_value == 'error') { + return false + } + }} + onDelete={e => { + console.log('delete callback', e) + }} + onAdd={e => { + console.log('add callback', e) + if (e.new_value == 'error') { + return false + } + }} + onSelect={e => { + console.log('select callback', e) + console.log(e.namespace) + }} + displayObjectSize={true} + name={'dev-server'} + enableClipboard={copy => { + console.log('you copied to clipboard!', copy) + }} + shouldCollapse={({ src, namespace, type }) => { + if (type === 'array' && src.indexOf('test') > -1) { + return true + } else if (namespace.indexOf('moment') > -1) { + return true + } + return false + }} + defaultValue='' + /> + +
+ + {/* use a base16 theme */} + { + console.log(e) + if (e.new_value === 'error') { + return false + } + }} + onDelete={e => { + console.log(e) + }} + onAdd={e => { + console.log(e) + if (e.new_value === 'error') { + return false + } + }} + name={false} + iconStyle='triangle' + shouldCollapse={({ src, type }) => + type === 'object' && + src.constructor && + src.constructor.name === 'Moment' + } + selectOnFocus + /> + +
+ + {/* initialize this one with a name and default collapsed */} + + +
+ + {/* initialize this one with a name and default collapsed */} + + +
+ + {/* initialize an example with a long string */} + + +
+ + {/*demo array support*/} + { + console.log(edit) + }} + /> + +
+ + {/* custom theme example */} + + namespace.indexOf('moment') > -1 + } + theme={{ + base00: 'white', + base01: '#ddd', + base02: '#ddd', + base03: '#444', + base04: 'purple', + base05: '#444', + base06: '#444', + base07: '#444', + base08: '#444', + base09: 'rgba(70, 70, 230, 1)', + base0A: 'rgba(70, 70, 230, 1)', + base0B: 'rgba(70, 70, 230, 1)', + base0C: 'rgba(70, 70, 230, 1)', + base0D: 'rgba(70, 70, 230, 1)', + base0E: 'rgba(70, 70, 230, 1)', + base0F: 'rgba(70, 70, 230, 1)' + }} + /> + + + + {/* Name as colored react component */} + + React Element as name + + } + src={getExampleJson2()} + /> + + {/* String with special escape sequences */} + +
+) diff --git a/dev-server/src/index.js b/dev-server/src/index.js index 229ae0e..4caf8a8 100644 --- a/dev-server/src/index.js +++ b/dev-server/src/index.js @@ -2,321 +2,35 @@ //import react and reactDom for browser rendering import React from 'react' -import ReactDom from 'react-dom' -import Moment from 'moment' +import render from './render' //import the react-json-view component (installed with npm) -import JsonViewer from './../../src/js/index' +import JsonViewer, { + ReactPureJsonView as PureJsonView +} from './../../src/js/index' -// custom big number class, You can use existing libraries like `bignumber.js`, `decimal.js`, `big.js` etc. -class BigNumber { - name = 'customName' - constructor(value) { - this.value = value - } - toString() { - return this.value.toString() - } -} - -//render 2 different examples of the react-json-view component -ReactDom.render( -
- {/* just pass in your JSON to the src attribute */} - { - console.log('edit callback', e) - if (e.new_value == 'error') { - return false - } - }} - onDelete={e => { - console.log('delete callback', e) - }} - onAdd={e => { - console.log('add callback', e) - if (e.new_value == 'error') { - return false - } - }} - onSelect={e => { - console.log('select callback', e) - console.log(e.namespace) - }} - displayObjectSize={true} - name={'dev-server'} - enableClipboard={copy => { - console.log('you copied to clipboard!', copy) - }} - shouldCollapse={({ src, namespace, type }) => { - if (type === 'array' && src.indexOf('test') > -1) { - return true - } else if (namespace.indexOf('moment') > -1) { - return true - } - return false - }} - defaultValue='' - /> - -
- - {/* use a base16 theme */} - { - console.log(e) - if (e.new_value === 'error') { - return false - } - }} - onDelete={e => { - console.log(e) - }} - onAdd={e => { - console.log(e) - if (e.new_value === 'error') { - return false - } - }} - name={false} - iconStyle='triangle' - shouldCollapse={({ src, type }) => - type === 'object' && - src.constructor && - src.constructor.name === 'Moment' - } - selectOnFocus - /> - -
- - {/* initialize this one with a name and default collapsed */} - - -
- - {/* initialize this one with a name and default collapsed */} - - -
- - {/* initialize an example with a long string */} - - -
- - {/*demo array support*/} - { - console.log(edit) - }} - /> - -
- - {/* custom theme example */} - - namespace.indexOf('moment') > -1 - } - theme={{ - base00: 'white', - base01: '#ddd', - base02: '#ddd', - base03: '#444', - base04: 'purple', - base05: '#444', - base06: '#444', - base07: '#444', - base08: '#444', - base09: 'rgba(70, 70, 230, 1)', - base0A: 'rgba(70, 70, 230, 1)', - base0B: 'rgba(70, 70, 230, 1)', - base0C: 'rgba(70, 70, 230, 1)', - base0D: 'rgba(70, 70, 230, 1)', - base0E: 'rgba(70, 70, 230, 1)', - base0F: 'rgba(70, 70, 230, 1)' - }} - /> +// import test json data +import * as defaultTestJsonData from './object.test-data' - +import { exported as stringTestJsonData } from './string.test-data' - {/* Name as colored react component */} - - React Element as name - - } - src={getExampleJson2()} - /> +const search = new URLSearchParams(location.search) +const json_string = search.get('json_string') - {/* String with special escape sequences */} - -
, - document.getElementById('app-container') -) +const appTitle = document.querySelector('#app-title') -/*-------------------------------------------------------------------------*/ -/* the following functions just contain test json data for display */ -/*-------------------------------------------------------------------------*/ - -//just a function to get an example JSON object -function getExampleJson1 () { - return { - string: 'this is a test string', - integer: 42, - empty_array: [], - empty_object: {}, - array: [1, 2, 3, 'test'], - float: -2.757, - undefined_var: undefined, - parent: { - sibling1: true, - sibling2: false, - sibling3: null, - isString: value => { - if (typeof value === 'string') { - return 'string' - } else { - return 'other' - } - } - }, - string_number: '1234', - date: new Date(), - moment: Moment(), - regexp: /[0-9]/gi, - bigNumber: new BigNumber('0.0060254656709730629123') - } -} - -//and another a function to get an example JSON object -function getExampleJson2 () { - return { - normalized: { - '1-grams': { - body: 1, - testing: 1 - }, - '2-grams': { - 'testing body': 1 - }, - '3-grams': {} - }, - noun_phrases: { - body: 1 - }, - lemmatized: { - '1-grams': { - test: 1, - body: 1 - }, - '2-grams': { - 'test body': 1 - }, - '3-grams': {} - }, - dependency: { - '1-grams': { - testingVERBROOTtestingVERB: 1, - bodyNOUNdobjtestingVERB: 1 - }, - '2-grams': { - 'testingVERBROOTtestingVERB bodyNOUNdobjtestingVERB': 1 - }, - '3-grams': {} - } +if (json_string) { + if (json_string === 'true') { + appTitle.textContent = 'ReactPureJsonView: json string for prop.src' + render(stringTestJsonData, PureJsonView, null) + } else if (json_string === 'false') { + appTitle.textContent = 'ReactPureJsonView: json object/array for prop.src' + render(defaultTestJsonData, PureJsonView, null) + } else { + console.error('invalid option') } -} - -function getExampleJson3 () { - return { - example_information: - 'this example has the collapsed prop set to true and the indentWidth prop is set to 8', - default_collapsed: true, - collapsed_array: [ - 'you expanded me', - 'try collapsing and expanding the root node', - 'i will still be expanded', - { - leaf_node: true - } - ] - } -} - -function getExampleJson4 () { - const large_array = new Array(225).fill('this is a large array full of items') - - large_array.push(getExampleArray()) - - large_array.push(new Array(75).fill(Math.random())) - - return large_array -} - -function getExampleArray () { - return [ - 'you can also display arrays!', - new Date(), - 1, - 2, - 3, - { - pretty_cool: true - } - ] -} - -function getExampleWithStringEscapeSequences () { - return { '\\\n\t\r\f\\n': '\\\n\t\r\f\\n' } +} else { + appTitle.textContent = 'ReactJsonView: json object/array for prop.src' + render(defaultTestJsonData, JsonViewer) } diff --git a/dev-server/src/object.test-data.js b/dev-server/src/object.test-data.js new file mode 100644 index 0000000..16ef175 --- /dev/null +++ b/dev-server/src/object.test-data.js @@ -0,0 +1,120 @@ +'use strict' + +/*-------------------------------------------------------------------------*/ +/* the following functions just contain test json data for display */ +/*-------------------------------------------------------------------------*/ + +import Moment from 'moment' +import { defaultBigNumberImplement as BigNumber} from './../../src/js/index' + +//just a function to get an example JSON object +export function getExampleJson1 () { + return { + string: 'this is a test string', + integer: 42, + empty_array: [], + empty_object: {}, + array: [1, 2, 3, 'test'], + float: -2.757, + undefined_var: undefined, + parent: { + sibling1: true, + sibling2: false, + sibling3: null, + isString: value => { + if (typeof value === 'string') { + return 'string' + } else { + return 'other' + } + } + }, + string_number: '1234', + date: new Date(), + moment: Moment(), + regexp: /[0-9]/gi, + bigNumber: new BigNumber('0.0060254656709730629123') + } +} + +//and another a function to get an example JSON object +export function getExampleJson2 () { + return { + normalized: { + '1-grams': { + body: 1, + testing: 1 + }, + '2-grams': { + 'testing body': 1 + }, + '3-grams': {} + }, + noun_phrases: { + body: 1 + }, + lemmatized: { + '1-grams': { + test: 1, + body: 1 + }, + '2-grams': { + 'test body': 1 + }, + '3-grams': {} + }, + dependency: { + '1-grams': { + testingVERBROOTtestingVERB: 1, + bodyNOUNdobjtestingVERB: 1 + }, + '2-grams': { + 'testingVERBROOTtestingVERB bodyNOUNdobjtestingVERB': 1 + }, + '3-grams': {} + } + } +} + +export function getExampleJson3 () { + return { + example_information: + 'this example has the collapsed prop set to true and the indentWidth prop is set to 8', + default_collapsed: true, + collapsed_array: [ + 'you expanded me', + 'try collapsing and expanding the root node', + 'i will still be expanded', + { + leaf_node: true + } + ] + } +} + +export function getExampleJson4 () { + const large_array = new Array(225).fill('this is a large array full of items') + + large_array.push(getExampleArray()) + + large_array.push(new Array(75).fill(Math.random())) + + return large_array +} + +export function getExampleArray () { + return [ + 'you can also display arrays!', + new Date(), + 1, + 2, + 3, + { + pretty_cool: true + } + ] +} + +export function getExampleWithStringEscapeSequences () { + return { '\\\n\t\r\f\\n': '\\\n\t\r\f\\n' } +} diff --git a/dev-server/src/render.js b/dev-server/src/render.js new file mode 100644 index 0000000..8253807 --- /dev/null +++ b/dev-server/src/render.js @@ -0,0 +1,236 @@ +'use strict' + +//import react and reactDom for browser rendering +import React from 'react' +import ReactDom from 'react-dom' + +import { + ReactPureJsonView, + defaultBigNumberImplement +} from './../../src/js/index' + +const render = ( + { + getExampleArray, + getExampleJson1, + getExampleJson2, + getExampleJson3, + getExampleJson4, + getExampleWithStringEscapeSequences, + + __isStringifyDataGetter__: isStringifyDataGetter + }, + JsonViewer, + bigNumber = defaultBigNumberImplement +) => { + const specialProps = { + ...(bigNumber ? { bigNumber } : undefined) + } + + const specialRender = + ReactPureJsonView === JsonViewer && isStringifyDataGetter ? ( +
+ + +
+ ) : null + + //render 2 different examples of the react-json-view component + ReactDom.render( +
+ {specialRender} + + {/* just pass in your JSON to the src attribute */} + { + console.log('edit callback', e) + if (e.new_value == 'error') { + return false + } + }} + onDelete={e => { + console.log('delete callback', e) + }} + onAdd={e => { + console.log('add callback', e) + if (e.new_value == 'error') { + return false + } + }} + onSelect={e => { + console.log('select callback', e) + console.log(e.namespace) + }} + displayObjectSize={true} + name={'dev-server'} + enableClipboard={copy => { + console.log('you copied to clipboard!', copy) + }} + shouldCollapse={({ src, namespace, type }) => { + if (type === 'array' && src.indexOf('test') > -1) { + return true + } else if (namespace.indexOf('moment') > -1) { + return true + } + return false + }} + defaultValue='' + /> + +
+ + {/* use a base16 theme */} + { + console.log(e) + if (e.new_value === 'error') { + return false + } + }} + onDelete={e => { + console.log(e) + }} + onAdd={e => { + console.log(e) + if (e.new_value === 'error') { + return false + } + }} + name={false} + iconStyle='triangle' + shouldCollapse={({ src, type }) => + type === 'object' && + src.constructor && + src.constructor.name === 'Moment' + } + selectOnFocus + /> + +
+ + {/* initialize this one with a name and default collapsed */} + + +
+ + {/* initialize this one with a name and default collapsed */} + + +
+ + {/* initialize an example with a long string */} + + +
+ + {/*demo array support*/} + { + console.log(edit) + }} + /> + +
+ + {/* custom theme example */} + + namespace.indexOf('moment') > -1 + } + theme={{ + base00: 'white', + base01: '#ddd', + base02: '#ddd', + base03: '#444', + base04: 'purple', + base05: '#444', + base06: '#444', + base07: '#444', + base08: '#444', + base09: 'rgba(70, 70, 230, 1)', + base0A: 'rgba(70, 70, 230, 1)', + base0B: 'rgba(70, 70, 230, 1)', + base0C: 'rgba(70, 70, 230, 1)', + base0D: 'rgba(70, 70, 230, 1)', + base0E: 'rgba(70, 70, 230, 1)', + base0F: 'rgba(70, 70, 230, 1)' + }} + /> + + + + {/* Name as colored react component */} + + React Element as name + + } + src={getExampleJson2()} + /> + + {/* String with special escape sequences */} + +
, + document.getElementById('app-container') + ) +} + +export default render diff --git a/dev-server/src/string.test-data.js b/dev-server/src/string.test-data.js new file mode 100644 index 0000000..d9d76ed --- /dev/null +++ b/dev-server/src/string.test-data.js @@ -0,0 +1,21 @@ +'use strict' + +import { defaultJSONImplement as JSONBig } from './../../src/js/index' + +// import test json data +import * as defaultTestJsonData from './object.test-data' + +const exported = { + __isStringifyDataGetter__: true +} + +Object.keys(defaultTestJsonData).forEach(key => { + exported[key] = (...args) => { + const data = defaultTestJsonData[key](...args) + + // Convert to json string, will throw the function which is not supported. + return JSONBig.stringify(data) + } +}) + +export { exported } diff --git a/index.d.ts b/index.d.ts index 3d49b19..9ecb416 100644 --- a/index.d.ts +++ b/index.d.ts @@ -167,6 +167,10 @@ export interface ReactJsonViewProps { escapeStrings?: boolean } +export interface ReactPureJsonViewProps extends ReactJsonViewProps { + src: ReactJsonViewProps['src'] | string +} + export interface OnCopyProps { /** * The JSON tree source object @@ -310,3 +314,9 @@ export type ThemeKeys = declare const ReactJson: React.ComponentType export default ReactJson + +declare const defaultBigNumberImplement: typeof import('bignumber.js').BigNumber +declare const defaultJSONImplement: typeof import('json-bigint') +declare const ReactPureJsonView: React.ComponentType + +export { ReactPureJsonView, defaultBigNumberImplement, defaultJSONImplement } diff --git a/package.json b/package.json index 8a56e40..b8ed3a3 100644 --- a/package.json +++ b/package.json @@ -207,6 +207,8 @@ "treeview" ], "dependencies": { + "bignumber.js": "~9.1.2", + "json-bigint": "~1.0.0", "react-base16-styling": "~0.9.0", "react-lifecycles-compat": "~3.0.4", "react-textarea-autosize": "~8.5.7" diff --git a/src/js/ReactJsonView.js b/src/js/ReactJsonView.js new file mode 100644 index 0000000..0e48d17 --- /dev/null +++ b/src/js/ReactJsonView.js @@ -0,0 +1,272 @@ +import React from 'react' +import { polyfill } from 'react-lifecycles-compat' +import JsonViewer from './components/JsonViewer' +import AddKeyRequest from './components/ObjectKeyModal/AddKeyRequest' +import ValidationFailure from './components/ValidationFailure' +import { toType, isTheme } from './helpers/util' +import ObjectAttributes from './stores/ObjectAttributes' + +// global theme +import Theme from './themes/getStyle' + +// forward src through to JsonObject component +class ReactJsonView extends React.PureComponent { + constructor (props) { + super(props) + this.state = { + // listen to request to add/edit a key to an object + addKeyRequest: false, + editKeyRequest: false, + validationFailure: false, + src: ReactJsonView.defaultProps.src, + name: ReactJsonView.defaultProps.name, + theme: ReactJsonView.defaultProps.theme, + validationMessage: ReactJsonView.defaultProps.validationMessage, + // the state object also needs to remember the prev prop values, because we need to compare + // old and new props in getDerivedStateFromProps(). + prevSrc: ReactJsonView.defaultProps.src, + prevName: ReactJsonView.defaultProps.name, + prevTheme: ReactJsonView.defaultProps.theme + } + } + + // reference id for this instance + rjvId = Date.now().toString() + + // all acceptable props and default values + static defaultProps = { + src: {}, + name: 'root', + theme: 'rjv-default', + collapsed: false, + collapseStringsAfterLength: false, + shouldCollapse: false, + sortKeys: false, + quotesOnKeys: true, + groupArraysAfterLength: 100, + indentWidth: 4, + enableClipboard: true, + escapeStrings: true, + displayObjectSize: true, + displayDataTypes: true, + onEdit: false, + onDelete: false, + onAdd: false, + onSelect: false, + iconStyle: 'triangle', + style: {}, + validationMessage: 'Validation Error', + defaultValue: null, + displayArrayKey: true, + selectOnFocus: false, + keyModifier: e => e.metaKey || e.ctrlKey, + bigNumber: null + } + + // will trigger whenever setState() is called, or parent passes in new props. + static getDerivedStateFromProps (nextProps, prevState) { + if ( + nextProps.src !== prevState.prevSrc || + nextProps.name !== prevState.prevName || + nextProps.theme !== prevState.prevTheme + ) { + // if we pass in new props, we re-validate + const newPartialState = { + src: nextProps.src, + name: nextProps.name, + theme: nextProps.theme, + validationMessage: nextProps.validationMessage, + prevSrc: nextProps.src, + prevName: nextProps.name, + prevTheme: nextProps.theme + } + return ReactJsonView.validateState(newPartialState) + } + return null + } + + componentDidMount () { + // initialize + ObjectAttributes.set(this.rjvId, 'global', 'src', this.state.src) + // bind to events + const listeners = this.getListeners() + for (const i in listeners) { + ObjectAttributes.on(i + '-' + this.rjvId, listeners[i]) + } + // reset key request to false once it's observed + this.setState({ + addKeyRequest: false, + editKeyRequest: false + }) + } + + componentDidUpdate (prevProps, prevState) { + // reset key request to false once it's observed + if (prevState.addKeyRequest !== false) { + this.setState({ + addKeyRequest: false + }) + } + if (prevState.editKeyRequest !== false) { + this.setState({ + editKeyRequest: false + }) + } + if (prevProps.src !== this.state.src) { + ObjectAttributes.set(this.rjvId, 'global', 'src', this.state.src) + } + } + + componentWillUnmount () { + const listeners = this.getListeners() + for (const i in listeners) { + ObjectAttributes.removeListener(i + '-' + this.rjvId, listeners[i]) + } + } + + getListeners = () => { + return { + reset: this.resetState, + 'variable-update': this.updateSrc, + 'add-key-request': this.addKeyRequest + } + } + + // make sure props are passed in as expected + static validateState = state => { + const validatedState = {} + // make sure theme is valid + if (toType(state.theme) === 'object' && !isTheme(state.theme)) { + console.error( + 'react-json-view error:', + 'theme prop must be a theme name or valid base-16 theme object.', + 'defaulting to "rjv-default" theme' + ) + validatedState.theme = 'rjv-default' + } + // make sure `src` prop is valid + if (toType(state.src) !== 'object' && toType(state.src) !== 'array') { + console.error( + 'react-json-view error:', + 'src property must be a valid json object' + ) + validatedState.name = 'ERROR' + validatedState.src = { + message: 'src property must be a valid json object' + } + } + return { + // get the original state + ...state, + // override the original state + ...validatedState + } + } + + render () { + const { + validationFailure, + validationMessage, + addKeyRequest, + theme, + src, + name + } = this.state + + const { style, defaultValue } = this.props + + return ( +
+ + + +
+ ) + } + + updateSrc = () => { + const { + name, + namespace, + new_value, + existing_value, + variable_removed, + updated_src, + type + } = ObjectAttributes.get(this.rjvId, 'action', 'variable-update') + const { onEdit, onDelete, onAdd } = this.props + + const { src } = this.state + + let result + + const on_edit_payload = { + existing_src: src, + new_value, + updated_src, + name, + namespace, + existing_value + } + + switch (type) { + case 'variable-added': + result = onAdd(on_edit_payload) + break + case 'variable-edited': + result = onEdit(on_edit_payload) + break + case 'variable-removed': + result = onDelete(on_edit_payload) + break + } + + if (result !== false) { + ObjectAttributes.set(this.rjvId, 'global', 'src', updated_src) + this.setState({ + src: updated_src + }) + } else { + this.setState({ + validationFailure: true + }) + } + } + + addKeyRequest = () => { + this.setState({ + addKeyRequest: true + }) + } + + resetState = () => { + this.setState({ + validationFailure: false, + addKeyRequest: false + }) + } +} + +polyfill(ReactJsonView) + +export default ReactJsonView diff --git a/src/js/ReactPureJsonView.js b/src/js/ReactPureJsonView.js new file mode 100644 index 0000000..24d7851 --- /dev/null +++ b/src/js/ReactPureJsonView.js @@ -0,0 +1,84 @@ +import React from 'react' +import { polyfill } from 'react-lifecycles-compat' + +import { + getEnsureSrc, + defaultBigNumberImplement, + defaultJSONImplement +} from './helpers/bigNumberUtil' +import ReactJsonView from './ReactJsonView' + +// Support **json** or **json string** data as `prop.src` value, and also has built-in support +// for big-number and higher precision float. Using to present the pure data (json file or json reponse) in recommend, +// but also support json object/array. +// +// Why this component should be included in recommend ? +// - Today, the data is easy to obtain and rich. It is likely to contain big number and higher precision float. +// Some data comes from golang or python program, and maybe the data validity exceeds the normal situation of javascript. +// Automatically handle big number and precision issues, bringing convenience to general users and reducing their psychological burden. +// This component will let `out of box` in your hand, and you donot handle javascript's big number or float precision problem. +// +// A). Use ReactJsonView to render data which will include big number. +// ```jsx +// const data = await fetch("data/camera_fov30.json") +// const src = JSONBig.parse(data) // Use `json-bigint` +// +// // Use `bignumber.js` +// return +// ``` +// +// B). Use ReactPureJsonView to render data which will include big number. +// ```jsx +// const data = await fetch("data/camera_fov30.json") +// return +// ``` +// +// Which one is better? It's obvious! +// +// Refer +// [A] - [float problem](https://stackoverflow.com/questions/55280847/floating-point-number-in-javascript-ieee-754) +// [B] - [BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) +class ReactPureJsonView extends React.PureComponent { + constructor (props) { + super(props) + + this.state = { + src: ReactPureJsonView.defaultProps.src, + prevSrc: ReactPureJsonView.defaultProps.src + } + } + + // will trigger whenever setState() is called, or parent passes in new props. + static getDerivedStateFromProps (nextProps, prevState) { + const nextSrc = getEnsureSrc(nextProps.src, defaultJSONImplement) + + if (nextSrc !== prevState.prevSrc) { + const newPartialState = { + src: nextSrc, + prevSrc: nextSrc + } + + return newPartialState + } + } + + static defaultProps = { + src: {} + } + + render () { + const props = { + ...this.props, + src: this.state.src, + bigNumber: defaultBigNumberImplement + } + + return + } +} + +polyfill(ReactPureJsonView) + +export default ReactPureJsonView + +export { defaultBigNumberImplement, defaultJSONImplement } diff --git a/src/js/helpers/bigNumberUtil.js b/src/js/helpers/bigNumberUtil.js new file mode 100644 index 0000000..3634f11 --- /dev/null +++ b/src/js/helpers/bigNumberUtil.js @@ -0,0 +1,69 @@ +import JSONBig from 'json-bigint' + +// Avoid: `import bigNumberImpl from 'bignumber.js'` +const bigNumberImpl = require('bignumber.js') + +import { toType } from './util' + +/** + * + * Refer https://github.com/sidorares/json-bigint?tab=readme-ov-file#json-bigint + * ``` + * JSON.parse/stringify with bigints support. Based on + * Douglas Crockford JSON.js package and bignumber.js library. + * `` + * + * @todo support the specified implement + */ +const defaultBigNumberImplement = bigNumberImpl +const defaultJSONImplement = JSONBig + +export { defaultJSONImplement, defaultBigNumberImplement } + +function getEnsureSrcDataType (src) { + const srcType = src === null || src === undefined ? undefined : toType(src) + const validSrcTypes = ['string', 'object', 'array'] + + if (validSrcTypes.find(type => type == srcType)) { + return srcType + } + + return undefined +} + +// Only array or object or error-message's object will be return +export function getEnsureSrc (src, JSONImpl = defaultJSONImplement) { + const type = getEnsureSrcDataType(src) + + // type should json string or json object + if (type === undefined) { + const errorMsg = 'src property must be a valid json string or json object' + console.error('react-json-view error:', errorMsg) + + // error object to warn + src = { + message: errorMsg + } + } else if (type === 'string') { + const errorMsg = 'only support array-json string or object-json string' + try { + const parsedSrc = JSONImpl.parse(src) + const type = getEnsureSrcDataType(parsedSrc) + + if (type !== 'object' && type !== 'array') { + throw new Error(errorMsg) + } + + return parsedSrc + } catch (err) { + console.error('react-json-view error:', errorMsg) + + // error object to warn + src = { + message: errorMsg + } + } + } + + return src +} diff --git a/src/js/index.js b/src/js/index.js index 538bb68..8bbed38 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -1,271 +1,9 @@ -import React from 'react' -import { polyfill } from 'react-lifecycles-compat' -import JsonViewer from './components/JsonViewer' -import AddKeyRequest from './components/ObjectKeyModal/AddKeyRequest' -import ValidationFailure from './components/ValidationFailure' -import { toType, isTheme } from './helpers/util' -import ObjectAttributes from './stores/ObjectAttributes' - -// global theme -import Theme from './themes/getStyle' - -// forward src through to JsonObject component -class ReactJsonView extends React.PureComponent { - constructor (props) { - super(props) - this.state = { - // listen to request to add/edit a key to an object - addKeyRequest: false, - editKeyRequest: false, - validationFailure: false, - src: ReactJsonView.defaultProps.src, - name: ReactJsonView.defaultProps.name, - theme: ReactJsonView.defaultProps.theme, - validationMessage: ReactJsonView.defaultProps.validationMessage, - // the state object also needs to remember the prev prop values, because we need to compare - // old and new props in getDerivedStateFromProps(). - prevSrc: ReactJsonView.defaultProps.src, - prevName: ReactJsonView.defaultProps.name, - prevTheme: ReactJsonView.defaultProps.theme - } - } - - // reference id for this instance - rjvId = Date.now().toString() + Math.random().toString(36).slice(2) - - // all acceptable props and default values - static defaultProps = { - src: {}, - name: 'root', - theme: 'rjv-default', - collapsed: false, - collapseStringsAfterLength: false, - shouldCollapse: false, - sortKeys: false, - quotesOnKeys: true, - groupArraysAfterLength: 100, - indentWidth: 4, - enableClipboard: true, - escapeStrings: true, - displayObjectSize: true, - displayDataTypes: true, - onEdit: false, - onDelete: false, - onAdd: false, - onSelect: false, - iconStyle: 'triangle', - style: {}, - validationMessage: 'Validation Error', - defaultValue: null, - displayArrayKey: true, - selectOnFocus: false, - keyModifier: e => e.metaKey || e.ctrlKey, - bigNumber: null - } - - // will trigger whenever setState() is called, or parent passes in new props. - static getDerivedStateFromProps (nextProps, prevState) { - if ( - nextProps.src !== prevState.prevSrc || - nextProps.name !== prevState.prevName || - nextProps.theme !== prevState.prevTheme - ) { - // if we pass in new props, we re-validate - const newPartialState = { - src: nextProps.src, - name: nextProps.name, - theme: nextProps.theme, - validationMessage: nextProps.validationMessage, - prevSrc: nextProps.src, - prevName: nextProps.name, - prevTheme: nextProps.theme - } - return ReactJsonView.validateState(newPartialState) - } - return null - } - - componentDidMount () { - // initialize - ObjectAttributes.set(this.rjvId, 'global', 'src', this.state.src) - // bind to events - const listeners = this.getListeners() - for (const i in listeners) { - ObjectAttributes.on(i + '-' + this.rjvId, listeners[i]) - } - // reset key request to false once it's observed - this.setState({ - addKeyRequest: false, - editKeyRequest: false - }) - } - - componentDidUpdate (prevProps, prevState) { - // reset key request to false once it's observed - if (prevState.addKeyRequest !== false) { - this.setState({ - addKeyRequest: false - }) - } - if (prevState.editKeyRequest !== false) { - this.setState({ - editKeyRequest: false - }) - } - if (prevProps.src !== this.state.src) { - ObjectAttributes.set(this.rjvId, 'global', 'src', this.state.src) - } - } - - componentWillUnmount () { - const listeners = this.getListeners() - for (const i in listeners) { - ObjectAttributes.removeListener(i + '-' + this.rjvId, listeners[i]) - } - } - - getListeners = () => { - return { - reset: this.resetState, - 'variable-update': this.updateSrc, - 'add-key-request': this.addKeyRequest - } - } - - // make sure props are passed in as expected - static validateState = state => { - const validatedState = {} - // make sure theme is valid - if (toType(state.theme) === 'object' && !isTheme(state.theme)) { - console.error( - 'react-json-view error:', - 'theme prop must be a theme name or valid base-16 theme object.', - 'defaulting to "rjv-default" theme' - ) - validatedState.theme = 'rjv-default' - } - // make sure `src` prop is valid - if (toType(state.src) !== 'object' && toType(state.src) !== 'array') { - console.error( - 'react-json-view error:', - 'src property must be a valid json object' - ) - validatedState.name = 'ERROR' - validatedState.src = { - message: 'src property must be a valid json object' - } - } - return { - // get the original state - ...state, - // override the original state - ...validatedState - } - } - - render () { - const { - validationFailure, - validationMessage, - addKeyRequest, - theme, - src, - name - } = this.state - - const { style, defaultValue } = this.props - - return ( -
- - - -
- ) - } - - updateSrc = () => { - const { - name, - namespace, - new_value: newValue, - existing_value: existingValue, - updated_src: updatedSrc, - type - } = ObjectAttributes.get(this.rjvId, 'action', 'variable-update') - const { onEdit, onDelete, onAdd } = this.props - - const { src } = this.state - - let result - - const onEditPayload = { - existing_src: src, - new_value: newValue, - updated_src: updatedSrc, - name, - namespace, - existing_value: existingValue - } - - switch (type) { - case 'variable-added': - result = onAdd(onEditPayload) - break - case 'variable-edited': - result = onEdit(onEditPayload) - break - case 'variable-removed': - result = onDelete(onEditPayload) - break - } - - if (result !== false) { - ObjectAttributes.set(this.rjvId, 'global', 'src', updatedSrc) - this.setState({ - src: updatedSrc - }) - } else { - this.setState({ - validationFailure: true - }) - } - } - - addKeyRequest = () => { - this.setState({ - addKeyRequest: true - }) - } - - resetState = () => { - this.setState({ - validationFailure: false, - addKeyRequest: false - }) - } -} - -polyfill(ReactJsonView) +import ReactJsonView from './ReactJsonView' +import ReactPureJsonView, { + defaultBigNumberImplement, + defaultJSONImplement +} from './ReactPureJsonView' export default ReactJsonView + +export { ReactPureJsonView, defaultBigNumberImplement, defaultJSONImplement } diff --git a/test/tests/js/Index-jsonstring-test.js b/test/tests/js/Index-jsonstring-test.js new file mode 100644 index 0000000..7413193 --- /dev/null +++ b/test/tests/js/Index-jsonstring-test.js @@ -0,0 +1,52 @@ +import React from 'react' +import { render } from 'enzyme' +import { expect } from 'chai' +import { JSDOM } from 'jsdom' + +import { ReactPureJsonView as Index} from './../../../src/js/index' + +const { window } = new JSDOM() +global.window = window +global.document = window.document + +describe('', function () { + it('index can have ArrayGroup root component', function () { + const wrapper = render( + + ) + expect(wrapper.find('.array-group')).to.have.length(3) + }) + + it('length is correct even if an object has a length property', function () { + const wrapper = render( + + ) + expect(wrapper.find('.object-size')).to.have.length(1) + }) + + it('bigint field is correct parsed with object\'s json string', function () { + const wrapper = render( + + ) + + expect(wrapper.find('.object-size')).to.have.length(1) + }) + + it('bigint field is correct parsed with array\'s json string', function () { + const wrapper = render( + + ) + + expect(wrapper.find('.object-size')).to.have.length(1) + }) +}) From 94c793010f56a194d6b3b924f3cc4f78633c7f35 Mon Sep 17 00:00:00 2001 From: pangl Date: Mon, 17 Feb 2025 13:26:16 +0800 Subject: [PATCH 2/3] refactor: re-export ReactJsonView --- src/js/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/js/index.js b/src/js/index.js index 8bbed38..5c7486f 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -6,4 +6,9 @@ import ReactPureJsonView, { export default ReactJsonView -export { ReactPureJsonView, defaultBigNumberImplement, defaultJSONImplement } +export { + ReactJsonView, + ReactPureJsonView, + defaultBigNumberImplement, + defaultJSONImplement +} From 1b8c4b808a8d167c243ed739521b9b48766a7fdf Mon Sep 17 00:00:00 2001 From: pangl Date: Sat, 15 Mar 2025 12:14:06 +0800 Subject: [PATCH 3/3] chore: update readme --- README.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/README.md b/README.md index ddbeb3e..df1667a 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,57 @@ The following object will be passed to your method: Returning `false` from a callback method will prevent the src from being affected. +### `ReactPureJsonView` component + +#### Why is it needed + +Users usually use this repository to directly connect and display `static data` (such as external JSON files, other API operation results, etc.). These static data may have (Javascript language) big number ([example](https://www.geeksforgeeks.org/how-to-deal-with-large-numbers-in-javascript/)) and floating point ([example](https://stackoverflow.com/a/55291279)) problems, and users are more interested in correctly displaying the static data and do not care about the data involving different languages ​​(Javascript) problems. + +#### Usage + +- JSON String Example + + ```js + import { ReactPureJsonView } from '@microlink/react-json-view' + + /** + * Get `src` (json string) from the external file + * + * @type {String} + */ + const data = await fetch('data/data.json') + + + ``` + +- JSON Object Example (legacy mode, the same as [ReactJsonView Usage](#usage)) + + ```js + import { ReactPureJsonView } from '@microlink/react-json-view' + + + ``` + +#### API + +| Name | Type | Default | Description | +|:-----------------------------|:-------------------------------------------------|:-------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `src` | `JSON Object` or `JSON String` | None | This property contains your input JSON (object or json-string). | + ### Theming #### Builtin theme