React API mostly working (WIP)
This commit is contained in:
parent
98f56efc47
commit
785ab5205c
3
.babelrc
3
.babelrc
|
@ -1,3 +1,4 @@
|
|||
{
|
||||
"presets": ["es2015", "stage-3", "stage-2"]
|
||||
"presets": ["es2015", "stage-3", "stage-2"],
|
||||
"plugins": ["transform-flow-strip-types"]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
[ignore]
|
||||
|
||||
[include]
|
||||
./src
|
||||
|
||||
[libs]
|
||||
|
||||
[options]
|
||||
module.name_mapper.extension='less' -> '<PROJECT_ROOT>/src/flow/LessModule.js'
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe
|
|
@ -0,0 +1,94 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>React component | JSONEditor</title>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.4.1/react.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.4.1/react-dom.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.4.4/babel.min.js"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
This demo shows how to load JSONEditor as a React Component
|
||||
</p>
|
||||
|
||||
<div id="root"></div>
|
||||
|
||||
<!-- load JSONEditor -->
|
||||
<script src="../dist/jsoneditor.js"></script>
|
||||
|
||||
<script>
|
||||
// FIXME: should use JSONEditor source code instead of bundled version (multiple versions of React can give conflicts)
|
||||
// use React and ReactDOM as embedded in the library
|
||||
const React = jsoneditor.React
|
||||
const ReactDOM = jsoneditor.ReactDOM
|
||||
</script>
|
||||
|
||||
<script type="text/babel">
|
||||
// FIXME: should use JSONEditor source code instead of bundled version (multiple versions of React can give conflicts)
|
||||
// Note that in a full React application, the editor would be loaded as:
|
||||
// import {JSONEditor} from 'jsoneditor'
|
||||
const JSONEditor = jsoneditor.JSONEditor
|
||||
|
||||
const json = {
|
||||
'array': [1, 2, 3],
|
||||
'boolean': true,
|
||||
'null': null,
|
||||
'number': 123,
|
||||
'object': {'a': 'b', 'c': 'd'},
|
||||
'string': 'Hello World'
|
||||
}
|
||||
|
||||
class App extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
text: JSON.stringify(json, null, 2)
|
||||
}
|
||||
|
||||
this.onChange = this.onChange.bind(this)
|
||||
this.onChangeText = this.onChangeText.bind(this)
|
||||
}
|
||||
|
||||
render () {
|
||||
return <JSONEditor
|
||||
mode="code"
|
||||
modes={['text', 'code', 'tree', 'form', 'view']}
|
||||
text={this.state.text}
|
||||
onChange={this.onChange}
|
||||
onChangeText={this.onChangeText}
|
||||
/>
|
||||
}
|
||||
|
||||
onChange (json) {
|
||||
console.log('onChange', json)
|
||||
}
|
||||
|
||||
onChangeText (text) {
|
||||
console.log('onChangeText', text)
|
||||
|
||||
this.setState({ text })
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<App />,
|
||||
document.getElementById('root')
|
||||
)
|
||||
</script>
|
||||
|
||||
<script>
|
||||
Array.prototype.slice
|
||||
.call(document.querySelectorAll('script[type="text/babel"]'))
|
||||
.forEach(function(script) {
|
||||
var el = document.createElement('script');
|
||||
el.innerHTML = Babel.transform(script.innerText, { presets: ['es2015', 'react'] }).code;
|
||||
script.parentNode.insertBefore(el, script);
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -47,7 +47,8 @@ var loaders = [
|
|||
|
||||
// create a single instance of the compiler to allow caching
|
||||
var plugins = [
|
||||
bannerPlugin,]
|
||||
bannerPlugin
|
||||
]
|
||||
if (!WATCHING) {
|
||||
plugins.push(new webpack.optimize.UglifyJsPlugin())
|
||||
plugins.push(new webpack.DefinePlugin({
|
||||
|
|
25
package.json
25
package.json
|
@ -20,25 +20,28 @@
|
|||
"scripts": {
|
||||
"start": "gulp watch",
|
||||
"build": "gulp",
|
||||
"flow": "flow; test $? -eq 0 -o $? -eq 2",
|
||||
"test": "ava test/*.test.js test/**/*.test.js --verbose"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": "4.8.2",
|
||||
"ajv": "4.9.2",
|
||||
"brace": "0.9.0",
|
||||
"javascript-natural-sort": "0.7.1",
|
||||
"lodash": "4.16.6",
|
||||
"react": "^15.3.2",
|
||||
"react-dom": "^15.3.2"
|
||||
"lodash": "4.17.2",
|
||||
"react": "15.4.1",
|
||||
"react-dom": "15.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ava": "0.16.0",
|
||||
"babel-core": "6.18.2",
|
||||
"babel-loader": "6.2.7",
|
||||
"ava": "0.17.0",
|
||||
"babel-core": "6.20.0",
|
||||
"babel-loader": "6.2.9",
|
||||
"babel-plugin-transform-flow-strip-types": "6.18.0",
|
||||
"babel-preset-stage-2": "6.18.0",
|
||||
"babel-preset-stage-3": "6.17.0",
|
||||
"browser-sync": "2.17.5",
|
||||
"css-loader": "0.25.0",
|
||||
"graceful-fs": "4.1.10",
|
||||
"browser-sync": "2.18.2",
|
||||
"css-loader": "0.26.1",
|
||||
"flow-bin": "0.36.0",
|
||||
"graceful-fs": "4.1.11",
|
||||
"gulp": "3.9.1",
|
||||
"gulp-shell": "0.5.2",
|
||||
"gulp-util": "3.0.7",
|
||||
|
@ -48,7 +51,7 @@
|
|||
"mkdirp": "0.5.1",
|
||||
"style-loader": "0.13.1",
|
||||
"svg-url-loader": "1.1.0",
|
||||
"webpack": "1.13.3"
|
||||
"webpack": "1.14.0"
|
||||
},
|
||||
"ava": {
|
||||
"require": [
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// @flow
|
||||
|
||||
import { createElement as h, Component } from 'react'
|
||||
import ace from '../assets/ace'
|
||||
|
||||
|
@ -12,13 +14,10 @@ import ace from '../assets/ace'
|
|||
*
|
||||
*/
|
||||
export default class Ace extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
aceEditor = null
|
||||
settingValue = false // Used to prevent Ace from emitting onChange event whilst we're setting a value programmatically
|
||||
|
||||
this.aceEditor = null
|
||||
}
|
||||
|
||||
render (props, state) {
|
||||
render () {
|
||||
return h('div', {ref: 'container', className: 'jsoneditor-code'})
|
||||
}
|
||||
|
||||
|
@ -64,35 +63,45 @@ export default class Ace extends Component {
|
|||
: aceEditor
|
||||
|
||||
// register onchange event
|
||||
this.aceEditor.on('change', this.handleChange)
|
||||
|
||||
// set value, the text contents for the editor
|
||||
this.aceEditor.setValue(this.props.value || '', -1)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.value !== this.aceEditor.getValue()) {
|
||||
this.aceEditor.setValue(nextProps.value, -1)
|
||||
if (this.aceEditor) {
|
||||
this.aceEditor.on('change', this.handleChange)
|
||||
}
|
||||
|
||||
if (nextProps.indentation != undefined) {
|
||||
// set value, the text contents for the editor
|
||||
if (this.aceEditor) {
|
||||
this.aceEditor.setValue(this.props.value || '', -1)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps: {value: string, indentation?: number}) {
|
||||
if (this.aceEditor && nextProps.value !== this.aceEditor.getValue()) {
|
||||
this.settingValue = true
|
||||
this.aceEditor.setValue(nextProps.value, -1)
|
||||
this.settingValue = false
|
||||
}
|
||||
|
||||
if (this.aceEditor && nextProps.indentation != undefined) {
|
||||
this.aceEditor.getSession().setTabSize(this.props.indentation)
|
||||
}
|
||||
|
||||
// TODO: only resize only when needed
|
||||
setTimeout(() => {
|
||||
this.aceEditor.resize(false);
|
||||
if (this.aceEditor) {
|
||||
this.aceEditor.resize(false);
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
// neatly destroy ace editor instance
|
||||
this.aceEditor.destroy()
|
||||
this.aceEditor = null
|
||||
if (this.aceEditor) {
|
||||
this.aceEditor.destroy()
|
||||
this.aceEditor = null
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = () => {
|
||||
if (this.props && this.props.onChange) {
|
||||
if (this.props && this.props.onChange && this.aceEditor && !this.settingValue) {
|
||||
// TODO: pass a diff
|
||||
this.props.onChange(this.aceEditor.getValue())
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// @flow
|
||||
|
||||
import { createElement as h, Component } from 'react'
|
||||
import TextMode from './TextMode'
|
||||
import Ace from './Ace'
|
||||
|
@ -28,11 +30,12 @@ import Ace from './Ace'
|
|||
*
|
||||
*/
|
||||
export default class CodeMode extends TextMode {
|
||||
constructor (props) {
|
||||
constructor (props: {options: {onLoadAce: Function, indentation: number}}) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
text: '{}'
|
||||
text: '{}',
|
||||
compiledSchema: null
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,22 +45,15 @@ export default class CodeMode extends TextMode {
|
|||
|
||||
h('div', {key: 'contents', className: 'jsoneditor-contents'}, h(Ace, {
|
||||
value: this.state.text,
|
||||
onChange: this.handleChange,
|
||||
onLoadAce: this.props.options.onLoadAce,
|
||||
indentation: this.props.options.indentation,
|
||||
ace: this.props.options.ace
|
||||
onChange: this.handleChangeText,
|
||||
onLoadAce: this.props.onLoadAce,
|
||||
indentation: this.props.indentation,
|
||||
ace: this.props.ace
|
||||
})),
|
||||
|
||||
this.renderSchemaErrors ()
|
||||
])
|
||||
}
|
||||
|
||||
handleChange = (text) => {
|
||||
this.setState({ text })
|
||||
|
||||
if (this.props.options && this.props.options.onChangeText) {
|
||||
// TODO: pass a diff
|
||||
this.props.options.onChangeText()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: define propTypes
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
// @flow
|
||||
|
||||
import { createElement as h, Component, PropTypes } from 'react'
|
||||
import { render, unmountComponentAtNode} from 'react-dom'
|
||||
import CodeMode from './CodeMode'
|
||||
import TextMode from './TextMode'
|
||||
import TreeMode from './TreeMode'
|
||||
|
||||
export default class JSONEditor extends Component {
|
||||
static modeConstructors = {
|
||||
code: CodeMode,
|
||||
form: TreeMode,
|
||||
text: TextMode,
|
||||
tree: TreeMode,
|
||||
view: TreeMode
|
||||
}
|
||||
|
||||
state = {
|
||||
mode: 'tree'
|
||||
}
|
||||
|
||||
render () {
|
||||
const mode = this.state.mode // We use mode from state, not from props!
|
||||
const ModeConstructor = JSONEditor.modeConstructors[mode]
|
||||
|
||||
if (!ModeConstructor) {
|
||||
// TODO: show an on screen error instead of throwing an error?
|
||||
throw new Error('Unknown mode "' + mode + '". ' +
|
||||
'Choose from: ' + Object.keys(this.props.modes).join(', '))
|
||||
}
|
||||
|
||||
return h(ModeConstructor, {
|
||||
...this.props,
|
||||
mode,
|
||||
onError: this.handleError,
|
||||
onChangeMode: this.handleChangeMode
|
||||
})
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
if (this.props.mode) {
|
||||
this.setState({ mode: this.props.mode })
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps: {mode: ?string}) {
|
||||
if (nextProps.mode !== this.props.mode) {
|
||||
this.setState({ mode: nextProps.mode })
|
||||
}
|
||||
}
|
||||
|
||||
handleError = (err: Error) => {
|
||||
if (this.props.onError) {
|
||||
this.props.onError(err)
|
||||
}
|
||||
else {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
handleChangeMode = (mode: string) => {
|
||||
const prevMode = this.state.mode
|
||||
|
||||
this.setState({ mode })
|
||||
|
||||
if (this.props.onChangeMode) {
|
||||
this.props.onChangeMode(mode, prevMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JSONEditor.propTypes = {
|
||||
mode: PropTypes.string
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
// @flow weak
|
||||
|
||||
import { createElement as h, Component } from 'react'
|
||||
|
||||
import ActionButton from './menu/ActionButton'
|
||||
|
@ -15,13 +17,9 @@ let activeContextMenu = null
|
|||
export default class JSONNode extends Component {
|
||||
static URL_TITLE = 'Ctrl+Click or Ctrl+Enter to open url'
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
menu: null, // context menu
|
||||
appendMenu: null, // append context menu (used in placeholder of empty object/array)
|
||||
}
|
||||
state = {
|
||||
menu: null, // context menu
|
||||
appendMenu: null, // append context menu (used in placeholder of empty object/array)
|
||||
}
|
||||
|
||||
render () {
|
||||
|
@ -484,7 +482,7 @@ export default class JSONNode extends Component {
|
|||
|
||||
/**
|
||||
* Singleton function to hide the currently visible context menu if any.
|
||||
* @private
|
||||
* @protected
|
||||
*/
|
||||
static hideActionMenu () {
|
||||
if (activeContextMenu) {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// @flow weak
|
||||
|
||||
import { createElement as h, Component } from 'react'
|
||||
import Ajv from 'ajv'
|
||||
import { parseJSON } from '../utils/jsonUtils'
|
||||
|
@ -18,7 +20,9 @@ const AJV_OPTIONS = {
|
|||
* Usage:
|
||||
*
|
||||
* <TextMode
|
||||
* options={Object}
|
||||
* text={string}
|
||||
* json={JSON}
|
||||
* ...options
|
||||
* onChange={function(text: string)}
|
||||
* onChangeMode={function(mode: string)}
|
||||
* onError={function(error: Error)}
|
||||
|
@ -37,6 +41,7 @@ const AJV_OPTIONS = {
|
|||
*
|
||||
*/
|
||||
export default class TextMode extends Component {
|
||||
state: Object
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
@ -47,6 +52,34 @@ export default class TextMode extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.applyProps(this.props, {})
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
this.applyProps(nextProps, this.props)
|
||||
}
|
||||
|
||||
// TODO: create some sort of watcher structure for these props? Is there a Reactpattern for that?
|
||||
applyProps (nextProps, currentProps) {
|
||||
// Apply text
|
||||
if (nextProps.text !== currentProps.text) {
|
||||
this.setText(nextProps.text)
|
||||
}
|
||||
|
||||
// Apply json
|
||||
if (nextProps.json !== currentProps.json) {
|
||||
this.set(nextProps.json)
|
||||
}
|
||||
|
||||
// Apply JSON Schema
|
||||
if (nextProps.schema !== currentProps.schema) {
|
||||
this.setSchema(nextProps.schema)
|
||||
}
|
||||
|
||||
// TODO: apply patchText
|
||||
}
|
||||
|
||||
render () {
|
||||
return h('div', {className: 'jsoneditor jsoneditor-mode-text'}, [
|
||||
this.renderMenu(),
|
||||
|
@ -88,9 +121,10 @@ export default class TextMode extends Component {
|
|||
className: 'jsoneditor-vertical-menu-separator'
|
||||
}),
|
||||
|
||||
this.props.options.modes && h(ModeButton, {
|
||||
this.props.modes && h(ModeButton, {
|
||||
key: 'mode',
|
||||
modes: this.props.options.modes,
|
||||
// TODO: simply pass all options?
|
||||
modes: this.props.modes,
|
||||
mode: this.props.mode,
|
||||
onChangeMode: this.props.onChangeMode,
|
||||
onError: this.props.onError
|
||||
|
@ -113,9 +147,7 @@ export default class TextMode extends Component {
|
|||
const allErrors = this.state.compiledSchema.errors.map(enrichSchemaError)
|
||||
const limitedErrors = limitErrors(allErrors)
|
||||
|
||||
console.log('errors', allErrors)
|
||||
|
||||
return h('div', { className: 'jsoneditor-errors'},
|
||||
return h('div', { key: 'errors', className: 'jsoneditor-errors'},
|
||||
h('table', {},
|
||||
h('tbody', {}, limitedErrors.map(TextMode.renderSchemaError))
|
||||
)
|
||||
|
@ -139,14 +171,15 @@ export default class TextMode extends Component {
|
|||
/**
|
||||
* Render a table row of a single JSON schema error
|
||||
* @param {Error | Object | string} error
|
||||
* @param {number} index
|
||||
* @return {JSX.Element}
|
||||
*/
|
||||
static renderSchemaError (error) {
|
||||
static renderSchemaError (error, index) {
|
||||
const icon = h('input', {type: 'button', className: 'jsoneditor-schema-error'})
|
||||
|
||||
if (error && error.schema && error.schemaPath) {
|
||||
// this is an ajv error message
|
||||
return h('tr', {}, [
|
||||
return h('tr', { key: index }, [
|
||||
h('td', {key: 'icon'}, icon),
|
||||
h('td', {key: 'path'}, error.dataPath),
|
||||
h('td', {key: 'message'}, error.message)
|
||||
|
@ -155,7 +188,7 @@ export default class TextMode extends Component {
|
|||
else {
|
||||
// any other error message
|
||||
console.log('error???', error)
|
||||
return h('tr', {},
|
||||
return h('tr', { key: index },
|
||||
h('td', {key: 'icon'}, icon),
|
||||
h('td', {key: 'message', colSpan: 2}, h('code', {}, String(error)))
|
||||
)
|
||||
|
@ -169,7 +202,7 @@ export default class TextMode extends Component {
|
|||
*/
|
||||
setSchema (schema) {
|
||||
if (schema) {
|
||||
const ajv = this.props.options.ajv || Ajv && Ajv(AJV_OPTIONS)
|
||||
const ajv = this.props.ajv || Ajv && Ajv(AJV_OPTIONS)
|
||||
|
||||
if (!ajv) {
|
||||
throw new Error('Cannot validate JSON: ajv not available. ' +
|
||||
|
@ -188,14 +221,24 @@ export default class TextMode extends Component {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get the configured indentation
|
||||
* @return {number}
|
||||
* @protected
|
||||
* Get the configured indentation. When not configured, returns the default value 2
|
||||
*/
|
||||
getIndentation () {
|
||||
return this.props.options && this.props.options.indentation || 2
|
||||
static getIndentation (props?: {indentation?: number}) : number {
|
||||
return props && props.indentation || 2
|
||||
}
|
||||
|
||||
static format (text, indentation) {
|
||||
const json = parseJSON(text)
|
||||
return JSON.stringify(json, null, indentation)
|
||||
}
|
||||
|
||||
static compact (text) {
|
||||
const json = parseJSON(text)
|
||||
return JSON.stringify(json)
|
||||
}
|
||||
|
||||
// TODO: move the static functions above into a separate util file
|
||||
|
||||
handleChange = (event) => {
|
||||
// do nothing...
|
||||
}
|
||||
|
@ -206,18 +249,14 @@ export default class TextMode extends Component {
|
|||
* @protected
|
||||
*/
|
||||
handleInput = (event) => {
|
||||
this.setText(event.target.value)
|
||||
|
||||
if (this.props.options && this.props.options.onChangeText) {
|
||||
// TODO: pass a diff
|
||||
this.props.options.onChangeText()
|
||||
}
|
||||
this.handleChangeText(event.target.value)
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
handleFormat = () => {
|
||||
try {
|
||||
this.format()
|
||||
const formatted = TextMode.format(this.getText(), TextMode.getIndentation(this.props))
|
||||
this.handleChangeText(formatted)
|
||||
}
|
||||
catch (err) {
|
||||
this.props.onError(err)
|
||||
|
@ -227,7 +266,8 @@ export default class TextMode extends Component {
|
|||
/** @protected */
|
||||
handleCompact = () => {
|
||||
try {
|
||||
this.compact()
|
||||
const compacted = TextMode.compact(this.getText())
|
||||
this.handleChangeText(compacted)
|
||||
}
|
||||
catch (err) {
|
||||
this.props.onError(err)
|
||||
|
@ -235,22 +275,22 @@ export default class TextMode extends Component {
|
|||
}
|
||||
|
||||
/**
|
||||
* Format the json
|
||||
* Apply new text to the state, and emit an onChangeText event if there is a change
|
||||
*/
|
||||
format () {
|
||||
const json = this.get()
|
||||
const text = JSON.stringify(json, null, this.getIndentation())
|
||||
this.setText(text)
|
||||
handleChangeText = (text: string) => {
|
||||
if (this.props.onChangeText && text !== this.state.text) {
|
||||
const appliedText = this.setText(text)
|
||||
this.props.onChangeText(appliedText)
|
||||
}
|
||||
else {
|
||||
this.setText(text)
|
||||
}
|
||||
|
||||
// TODO: also invoke a patch action
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact the json
|
||||
*/
|
||||
compact () {
|
||||
const json = this.get()
|
||||
const text = JSON.stringify(json)
|
||||
this.setText(text)
|
||||
}
|
||||
// TODO: implement method patchText
|
||||
// TODO: implement callback onPatchText
|
||||
|
||||
/**
|
||||
* Apply a JSONPatch to the current JSON document
|
||||
|
@ -279,7 +319,7 @@ export default class TextMode extends Component {
|
|||
* @param {Object | Array | string | number | boolean | null} json JSON data
|
||||
*/
|
||||
set (json) {
|
||||
this.setText(JSON.stringify(json, null, this.getIndentation()))
|
||||
this.setText(JSON.stringify(json, null, TextMode.getIndentation(this.props)))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -292,14 +332,15 @@ export default class TextMode extends Component {
|
|||
|
||||
/**
|
||||
* Set a string containing a JSON document
|
||||
* @param {string} text
|
||||
*/
|
||||
setText (text) {
|
||||
this.setState({
|
||||
text: this.props.options.escapeUnicode
|
||||
? escapeUnicodeChars(text)
|
||||
: text
|
||||
})
|
||||
setText (text: string) : string {
|
||||
const normalizedText = this.props.escapeUnicode
|
||||
? escapeUnicodeChars(text)
|
||||
: text
|
||||
|
||||
this.setState({ text: normalizedText })
|
||||
|
||||
return normalizedText
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -310,3 +351,6 @@ export default class TextMode extends Component {
|
|||
return this.state.text
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: define propTypes
|
||||
|
||||
|
|
|
@ -59,6 +59,43 @@ export default class TreeMode extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.applyProps(this.props, {})
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
this.applyProps(nextProps, this.props)
|
||||
}
|
||||
|
||||
// TODO: create some sort of watcher structure for these props? Is there a Reactpattern for that?
|
||||
applyProps (nextProps, currentProps) {
|
||||
// Apply text
|
||||
if (nextProps.text !== currentProps.text) {
|
||||
this.patch([{
|
||||
op: 'replace',
|
||||
path: '',
|
||||
value: parseJSON(nextProps.text) // FIXME: this can fail, handle error correctly
|
||||
}])
|
||||
}
|
||||
|
||||
// Apply json
|
||||
if (nextProps.json !== currentProps.json) {
|
||||
this.patch([{
|
||||
op: 'replace',
|
||||
path: '',
|
||||
value: nextProps.json
|
||||
}])
|
||||
}
|
||||
|
||||
// Apply JSON Schema
|
||||
if (nextProps.schema !== currentProps.schema) {
|
||||
this.setSchema(nextProps.schema)
|
||||
}
|
||||
|
||||
// TODO: apply patchText
|
||||
// TODO: apply patch
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props, state } = this
|
||||
|
||||
|
@ -95,7 +132,7 @@ export default class TreeMode extends Component {
|
|||
h(Node, {
|
||||
data,
|
||||
events: state.events,
|
||||
options: props.options,
|
||||
options: props,
|
||||
parent: null,
|
||||
prop: null
|
||||
})
|
||||
|
@ -120,7 +157,7 @@ export default class TreeMode extends Component {
|
|||
})
|
||||
]
|
||||
|
||||
if (this.props.mode !== 'view' && this.props.options.history != false) {
|
||||
if (this.props.mode !== 'view' && this.props.history != false) {
|
||||
items = items.concat([
|
||||
h('div', {key: 'history-separator', className: 'jsoneditor-vertical-menu-separator'}),
|
||||
|
||||
|
@ -141,13 +178,13 @@ export default class TreeMode extends Component {
|
|||
])
|
||||
}
|
||||
|
||||
if (this.props.options.modes ) {
|
||||
if (this.props.modes ) {
|
||||
items = items.concat([
|
||||
h('div', {key: 'mode-separator', className: 'jsoneditor-vertical-menu-separator'}),
|
||||
|
||||
h(ModeButton, {
|
||||
key: 'mode',
|
||||
modes: this.props.options.modes,
|
||||
modes: this.props.modes,
|
||||
mode: this.props.mode,
|
||||
onChangeMode: this.props.onChangeMode,
|
||||
onError: this.props.onError
|
||||
|
@ -155,7 +192,7 @@ export default class TreeMode extends Component {
|
|||
])
|
||||
}
|
||||
|
||||
if (this.props.options.search !== false) {
|
||||
if (this.props.search !== false) {
|
||||
// option search is true or undefined
|
||||
items = items.concat([
|
||||
h('div', {key: 'search', className: 'jsoneditor-menu-panel-right'},
|
||||
|
@ -283,18 +320,34 @@ export default class TreeMode extends Component {
|
|||
// apply changes
|
||||
const result = this.patch(actions)
|
||||
|
||||
this.emitOnChange (actions, result.revert)
|
||||
this.emitOnChange (actions, result.revert, result.data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an onChange event when there is a listener for it.
|
||||
* @param {JSONPatch} patch
|
||||
* @param {JSONPatch} revert
|
||||
* @param {JSONData} data
|
||||
* @private
|
||||
*/
|
||||
emitOnChange (patch, revert) {
|
||||
if (this.props.options.onChange) {
|
||||
this.props.options.onChange(patch, revert)
|
||||
emitOnChange (patch, revert, data) {
|
||||
if (this.props.onPatch) {
|
||||
this.props.onPatch(patch, revert)
|
||||
}
|
||||
|
||||
if (this.props.onChange || this.props.onChangeText) {
|
||||
const json = dataToJson(data)
|
||||
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(json)
|
||||
}
|
||||
|
||||
if (this.props.onChangeText) {
|
||||
const indentation = this.props.indentation || 2
|
||||
const text = JSON.stringify(json, null, indentation)
|
||||
|
||||
this.props.onChangeText(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -362,7 +415,7 @@ export default class TreeMode extends Component {
|
|||
const result = patchData(this.state.data, actions, expand)
|
||||
const data = result.data
|
||||
|
||||
if (this.props.options.history != false) {
|
||||
if (this.props.history != false) {
|
||||
// update data and store history
|
||||
const historyItem = {
|
||||
redo: actions,
|
||||
|
@ -387,19 +440,19 @@ export default class TreeMode extends Component {
|
|||
return {
|
||||
patch: actions,
|
||||
revert: result.revert,
|
||||
error: result.error
|
||||
error: result.error,
|
||||
data // FIXME: shouldn't pass data here
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set JSON object in editor
|
||||
* @param {Object | Array | string | number | boolean | null} json JSON data
|
||||
* @param {SetOptions} [options] If no expand function is provided,
|
||||
* The root will be expanded and all other nodes
|
||||
* will be collapsed.
|
||||
*/
|
||||
set (json, options = {}) {
|
||||
const expand = options.expand || TreeMode.expandRoot
|
||||
set (json) {
|
||||
// FIXME: when both json and expand are being changed via React, this.props must be updated before set(json) is called
|
||||
// TODO: document option expand
|
||||
const expand = this.props.expand || TreeMode.expandRoot
|
||||
|
||||
this.setState({
|
||||
data: jsonToData(json, expand, []),
|
||||
|
@ -431,7 +484,7 @@ export default class TreeMode extends Component {
|
|||
* @return {string} text
|
||||
*/
|
||||
getText () {
|
||||
const indentation = this.props.options.indentation || 2
|
||||
const indentation = this.props.indentation || 2
|
||||
return JSON.stringify(this.get(), null, indentation)
|
||||
}
|
||||
|
||||
|
@ -443,7 +496,7 @@ export default class TreeMode extends Component {
|
|||
// TODO: deduplicate this function, it's also implemented in TextMode
|
||||
setSchema (schema) {
|
||||
if (schema) {
|
||||
const ajv = this.props.options.ajv || Ajv && Ajv(AJV_OPTIONS)
|
||||
const ajv = this.props.ajv || Ajv && Ajv(AJV_OPTIONS)
|
||||
|
||||
if (!ajv) {
|
||||
throw new Error('Cannot validate JSON: ajv not available. ' +
|
||||
|
@ -534,3 +587,5 @@ export default class TreeMode extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// TODO: describe PropTypes
|
|
@ -77,7 +77,11 @@
|
|||
indentation: 4,
|
||||
escapeUnicode: true,
|
||||
history: true,
|
||||
search: true
|
||||
search: true,
|
||||
|
||||
expand: function (path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
const editor = jsoneditor(container, options)
|
||||
const json = {
|
||||
|
@ -92,11 +96,7 @@
|
|||
'unicode': 'A unicode character: \u260E',
|
||||
'url': 'http://jsoneditoronline.org'
|
||||
}
|
||||
editor.set(json, {
|
||||
expand: function (path) {
|
||||
return true
|
||||
}
|
||||
})
|
||||
editor.set(json)
|
||||
|
||||
const schema = {
|
||||
"title": "Example Schema",
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
// @flow
|
||||
declare export default string
|
41
src/index.js
41
src/index.js
|
@ -1,5 +1,6 @@
|
|||
import { createElement as h, Component } from 'react'
|
||||
import { render, unmountComponentAtNode} from 'react-dom'
|
||||
import React, { createElement as h, Component } from 'react'
|
||||
import ReactDOM, { render, unmountComponentAtNode} from 'react-dom'
|
||||
import JSONEditor from './components/JSONEditor'
|
||||
import CodeMode from './components/CodeMode'
|
||||
import TextMode from './components/TextMode'
|
||||
import TreeMode from './components/TreeMode'
|
||||
|
@ -45,6 +46,7 @@ function jsoneditor (container, options = {}) {
|
|||
* @param {SetOptions} [options]
|
||||
*/
|
||||
editor.set = function (json, options = {}) {
|
||||
// TODO: remove options from editor.set, move them to global options instead
|
||||
editor._component.set(json, options)
|
||||
}
|
||||
|
||||
|
@ -72,6 +74,30 @@ function jsoneditor (container, options = {}) {
|
|||
return editor._component.getText()
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the json.
|
||||
* Only applicable for mode 'text' and 'code' (in other modes nothing will
|
||||
* happen)
|
||||
*/
|
||||
editor.format = function () {
|
||||
const formatted = TextMode.format(editor._component.getText(), TextMode.getIndentation(this.props))
|
||||
editor._component.setText(formatted)
|
||||
|
||||
// TODO: test whether this doesn't destroy the current state
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact the json.
|
||||
* Only applicable for mode 'text' and 'code' (in other modes nothing will
|
||||
* happen)
|
||||
*/
|
||||
editor.compact = function () {
|
||||
const compacted = TextMode.compact(editor._component.getText())
|
||||
editor._component.setText(compacted)
|
||||
|
||||
// TODO: test whether this doesn't destroy the current state
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a JSON schema for validation of the JSON object.
|
||||
* To remove the schema, call JSONEditor.setSchema(null)
|
||||
|
@ -134,6 +160,8 @@ function jsoneditor (container, options = {}) {
|
|||
* @param {'tree' | 'text'} mode
|
||||
*/
|
||||
editor.setMode = function (mode) {
|
||||
// TODO: strongly simplify .setMode, no error handling or logic here
|
||||
|
||||
if (mode === editor._mode) {
|
||||
// mode stays the same. do nothing
|
||||
return
|
||||
|
@ -172,8 +200,8 @@ function jsoneditor (container, options = {}) {
|
|||
// create new component
|
||||
component = render(
|
||||
h(constructor, {
|
||||
...options,
|
||||
mode,
|
||||
options: editor._options,
|
||||
onChangeMode: handleChangeMode,
|
||||
onError: handleError
|
||||
}),
|
||||
|
@ -233,4 +261,11 @@ jsoneditor.utils = {
|
|||
parseJSONPointer
|
||||
}
|
||||
|
||||
// expose React component
|
||||
jsoneditor.JSONEditor = JSONEditor
|
||||
|
||||
// expose React itself
|
||||
jsoneditor.React = React
|
||||
jsoneditor.ReactDOM = ReactDOM
|
||||
|
||||
module.exports = jsoneditor
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
// @flow
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* type: 'Array',
|
||||
* expanded: boolean?,
|
||||
* props: Array.<{name: string, value: JSONData}>?
|
||||
* }} ObjectData
|
||||
*
|
||||
* @typedef {{
|
||||
* type: 'Object',
|
||||
* expanded: boolean?,
|
||||
* items: JSONData[]?
|
||||
* }} ArrayData
|
||||
*
|
||||
* @typedef {{
|
||||
* type: 'value' | 'string',
|
||||
* value: *?
|
||||
* }} ValueData
|
||||
*
|
||||
* @typedef {Array.<string>} Path
|
||||
*
|
||||
* @typedef {ObjectData | ArrayData | ValueData} JSONData
|
||||
*
|
||||
* @typedef {'Object' | 'Array' | 'value' | 'string'} JSONDataType
|
||||
|
||||
* @typedef {{
|
||||
* patch: JSONPatch,
|
||||
* revert: JSONPatch,
|
||||
* error: null | Error
|
||||
* }} JSONPatchResult
|
||||
*
|
||||
* @typedef {{
|
||||
* dataPath: string,
|
||||
* message: string
|
||||
* }} JSONSchemaError
|
||||
*
|
||||
* @typedef {{
|
||||
* name: string?,
|
||||
* mode: 'code' | 'form' | 'text' | 'tree' | 'view'?,
|
||||
* modes: string[]?,
|
||||
* history: boolean?,
|
||||
* indentation: number | string?,
|
||||
* onChange: function (patch: JSONPatch, revert: JSONPatch)?,
|
||||
* onChangeText: function ()?,
|
||||
* onChangeMode: function (mode: string, prevMode: string)?,
|
||||
* onError: function (err: Error)?,
|
||||
* isPropertyEditable: function (Path)?
|
||||
* isValueEditable: function (Path)?,
|
||||
* escapeUnicode: boolean?,
|
||||
* expand: function(path: Path) : boolean?,
|
||||
* ajv: Object?,
|
||||
* ace: Object?
|
||||
* }} Options
|
||||
*
|
||||
* @typedef {{
|
||||
* expand: function (path: Path)?
|
||||
* }} PatchOptions
|
||||
*
|
||||
* @typedef {{
|
||||
* dataPath: Path,
|
||||
* property: boolean?,
|
||||
* value: boolean?
|
||||
* }} SearchResult
|
||||
* // TODO: SearchResult.dataPath is an array, JSONSchemaError.dataPath is a string -> make this consistent
|
||||
*/
|
||||
|
||||
type JSONType = | string | number | boolean | null | JSONObjectType | JSONArrayType;
|
||||
type JSONObjectType = { [key:string]: JSON };
|
||||
type JSONArrayType = Array<JSON>;
|
||||
|
||||
export type Path = string[]
|
||||
|
||||
export type SetOptions = {
|
||||
expand?: (path: Path) => boolean
|
||||
}
|
||||
|
||||
export type JSONEditorMode = {
|
||||
setSchema: (schema?: Object) => void,
|
||||
set: (JSON) => void,
|
||||
setText: (text: string) => void,
|
||||
getText: () => string
|
||||
}
|
||||
|
||||
export type JSONPatchAction = {
|
||||
op: string, // TODO: define allowed ops
|
||||
path?: string,
|
||||
from?: string,
|
||||
value?: any
|
||||
}
|
||||
export type JSONPatch = JSONPatchAction[]
|
Loading…
Reference in New Issue