Implemented JSON Schema support for mode `text` and `code`
This commit is contained in:
parent
18d63fcd9a
commit
68a5b1476f
|
@ -4,4 +4,3 @@ downloads
|
||||||
node_modules
|
node_modules
|
||||||
*.zip
|
*.zip
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
yarn.lock
|
|
||||||
|
|
|
@ -59,12 +59,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
var options = {
|
var options = {
|
||||||
schema: schema
|
modes: ['code', 'tree']
|
||||||
}
|
}
|
||||||
|
|
||||||
// create the editor
|
// create the editor
|
||||||
var container = document.getElementById('jsoneditor')
|
var container = document.getElementById('jsoneditor')
|
||||||
var editor = jsoneditor(container, options, json)
|
var editor = jsoneditor(container, options)
|
||||||
|
editor.setSchema(schema)
|
||||||
|
editor.set(json)
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -35,7 +35,7 @@
|
||||||
var container = document.getElementById('jsoneditor')
|
var container = document.getElementById('jsoneditor')
|
||||||
var options = {
|
var options = {
|
||||||
mode: 'code',
|
mode: 'code',
|
||||||
onLoadAce: function (aceEditor, container, options) {
|
onLoadAce: function (aceEditor, container) {
|
||||||
// we can adjust configuration of the ace editor,
|
// we can adjust configuration of the ace editor,
|
||||||
// or create a completely new instance of ace editor.
|
// or create a completely new instance of ace editor.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { h, Component } from 'preact'
|
||||||
|
import ace from '../assets/ace'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Usage:
|
||||||
|
*
|
||||||
|
* <Ace value={"{}"}
|
||||||
|
* ace={Object}
|
||||||
|
* indentation={2}
|
||||||
|
* onChange={function(value: String)}
|
||||||
|
* onLoadAce={function(aceEditor, container)} />
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export default class Ace extends Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.aceEditor = null
|
||||||
|
}
|
||||||
|
|
||||||
|
render (props, state) {
|
||||||
|
return h('div', {id: this.id, class: 'jsoneditor-code'})
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate () {
|
||||||
|
// always prevent rerendering, that would destroy the DOM of the Ace editor
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const container = this.base
|
||||||
|
|
||||||
|
// use ace from bundle, and if not available
|
||||||
|
// try to use from options or else from global
|
||||||
|
const _ace = ace || this.props.ace || window['ace']
|
||||||
|
|
||||||
|
let aceEditor = null
|
||||||
|
if (_ace && _ace.edit) {
|
||||||
|
// create ace editor
|
||||||
|
aceEditor = _ace.edit(container)
|
||||||
|
|
||||||
|
// bundle and load jsoneditor theme for ace editor
|
||||||
|
require('../assets/ace/theme-jsoneditor')
|
||||||
|
|
||||||
|
// configure ace editor
|
||||||
|
aceEditor.$blockScrolling = Infinity
|
||||||
|
aceEditor.setTheme('ace/theme/jsoneditor')
|
||||||
|
aceEditor.setShowPrintMargin(false)
|
||||||
|
aceEditor.setFontSize(13)
|
||||||
|
aceEditor.getSession().setMode('ace/mode/json')
|
||||||
|
aceEditor.getSession().setTabSize(this.props.indentation || 2)
|
||||||
|
aceEditor.getSession().setUseSoftTabs(true)
|
||||||
|
aceEditor.getSession().setUseWrapMode(true)
|
||||||
|
aceEditor.commands.bindKey('Ctrl-L', null) // disable Ctrl+L (is used by the browser to select the address bar)
|
||||||
|
aceEditor.commands.bindKey('Command-L', null) // disable Ctrl+L (is used by the browser to select the address bar)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// ace is excluded from the bundle.
|
||||||
|
}
|
||||||
|
|
||||||
|
// allow changing the config or completely replacing aceEditor
|
||||||
|
this.aceEditor = this.props.onLoadAce
|
||||||
|
? this.props.onLoadAce(aceEditor, container) || aceEditor
|
||||||
|
: 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 (nextProps.indentation != undefined) {
|
||||||
|
this.aceEditor.getSession().setTabSize(this.props.indentation)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: only resize only when needed
|
||||||
|
setTimeout(() => {
|
||||||
|
this.aceEditor.resize(false);
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
// neatly destroy ace editor, it has created a worker for validation
|
||||||
|
this.aceEditor.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange = () => {
|
||||||
|
if (this.props && this.props.onChange) {
|
||||||
|
// TODO: pass a diff
|
||||||
|
this.props.onChange(this.aceEditor.getValue())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { h } from 'preact'
|
import { h } from 'preact'
|
||||||
import TextMode from './TextMode'
|
import TextMode from './TextMode'
|
||||||
import ace from '../assets/ace'
|
import Ace from './Ace'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CodeMode (powered by Ace editor)
|
* CodeMode (powered by Ace editor)
|
||||||
|
@ -11,7 +11,8 @@ import ace from '../assets/ace'
|
||||||
* options={Object}
|
* options={Object}
|
||||||
* onChange={function(text: string)}
|
* onChange={function(text: string)}
|
||||||
* onChangeMode={function(mode: string)}
|
* onChangeMode={function(mode: string)}
|
||||||
* onLoadAce={function(aceEditor: Object, container: Element, options: Object) : Object}
|
* onError={function(error: Error)}
|
||||||
|
* onLoadAce={function(aceEditor: Object, container: Element) : Object}
|
||||||
* />
|
* />
|
||||||
*
|
*
|
||||||
* Methods:
|
* Methods:
|
||||||
|
@ -30,81 +31,30 @@ export default class CodeMode extends TextMode {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this.state = {}
|
this.state = {
|
||||||
|
text: '{}'
|
||||||
this.id = 'id' + Math.round(Math.random() * 1e6) // unique enough id within the JSONEditor
|
}
|
||||||
this.aceEditor = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render (props, state) {
|
render (props, state) {
|
||||||
return h('div', {class: 'jsoneditor jsoneditor-mode-code'}, [
|
return h('div', {class: 'jsoneditor jsoneditor-mode-code'}, [
|
||||||
this.renderMenu(),
|
this.renderMenu(),
|
||||||
|
|
||||||
h('div', {class: 'jsoneditor-contents', id: this.id})
|
h('div', {class: '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
|
||||||
|
})),
|
||||||
|
|
||||||
|
this.renderSchemaErrors ()
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
handleChange = (text) => {
|
||||||
const options = this.props.options || {}
|
this.setState({ text })
|
||||||
|
|
||||||
const container = this.base.querySelector('#' + this.id)
|
|
||||||
|
|
||||||
// use ace from bundle, and if not available try to use from global
|
|
||||||
const _ace = ace || window['ace']
|
|
||||||
|
|
||||||
let aceEditor = null
|
|
||||||
if (_ace && _ace.edit) {
|
|
||||||
// create ace editor
|
|
||||||
aceEditor = _ace.edit(container)
|
|
||||||
|
|
||||||
// bundle and load jsoneditor theme for ace editor
|
|
||||||
require('../assets/ace/theme-jsoneditor')
|
|
||||||
|
|
||||||
// configure ace editor
|
|
||||||
aceEditor.$blockScrolling = Infinity
|
|
||||||
aceEditor.setTheme('ace/theme/jsoneditor')
|
|
||||||
aceEditor.setShowPrintMargin(false)
|
|
||||||
aceEditor.setFontSize(13)
|
|
||||||
aceEditor.getSession().setMode('ace/mode/json')
|
|
||||||
aceEditor.getSession().setTabSize(options.indentation || 2)
|
|
||||||
aceEditor.getSession().setUseSoftTabs(true)
|
|
||||||
aceEditor.getSession().setUseWrapMode(true)
|
|
||||||
aceEditor.commands.bindKey('Ctrl-L', null) // disable Ctrl+L (is used by the browser to select the address bar)
|
|
||||||
aceEditor.commands.bindKey('Command-L', null) // disable Ctrl+L (is used by the browser to select the address bar)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// ace is excluded from the bundle.
|
|
||||||
}
|
|
||||||
|
|
||||||
// allow changing the config or completely replacing aceEditor
|
|
||||||
this.aceEditor = options.onLoadAce
|
|
||||||
? options.onLoadAce(aceEditor, container, options) || aceEditor
|
|
||||||
: aceEditor
|
|
||||||
|
|
||||||
// register onchange event
|
|
||||||
this.aceEditor.on('change', this.handleChange)
|
|
||||||
|
|
||||||
// set initial text
|
|
||||||
this.setText('{}')
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
this.destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroy the editor
|
|
||||||
*/
|
|
||||||
destroy () {
|
|
||||||
// neatly destroy ace editor
|
|
||||||
this.aceEditor.destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate () {
|
|
||||||
// TODO: handle changes in props
|
|
||||||
}
|
|
||||||
|
|
||||||
handleChange = () => {
|
|
||||||
if (this.props.options && this.props.options.onChangeText) {
|
if (this.props.options && this.props.options.onChangeText) {
|
||||||
// TODO: pass a diff
|
// TODO: pass a diff
|
||||||
this.props.options.onChangeText()
|
this.props.options.onChangeText()
|
||||||
|
@ -116,7 +66,7 @@ export default class CodeMode extends TextMode {
|
||||||
* @param {string} text
|
* @param {string} text
|
||||||
*/
|
*/
|
||||||
setText (text) {
|
setText (text) {
|
||||||
this.aceEditor.setValue(text, -1)
|
this.setState({text})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -124,6 +74,6 @@ export default class CodeMode extends TextMode {
|
||||||
* @return {string} text
|
* @return {string} text
|
||||||
*/
|
*/
|
||||||
getText () {
|
getText () {
|
||||||
return this.aceEditor.getValue()
|
return this.state.text
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
import { h, Component } from 'preact'
|
import { h, Component } from 'preact'
|
||||||
|
import Ajv from 'ajv'
|
||||||
import { parseJSON } from '../utils/jsonUtils'
|
import { parseJSON } from '../utils/jsonUtils'
|
||||||
import { escapeUnicodeChars } from '../utils/stringUtils'
|
import { escapeUnicodeChars } from '../utils/stringUtils'
|
||||||
|
import { enrichSchemaError, limitErrors } from '../utils/schemaUtils'
|
||||||
import { jsonToData, dataToJson, patchData } from '../jsonData'
|
import { jsonToData, dataToJson, patchData } from '../jsonData'
|
||||||
import ModeButton from './menu/ModeButton'
|
import ModeButton from './menu/ModeButton'
|
||||||
|
|
||||||
|
@ -13,6 +15,7 @@ import ModeButton from './menu/ModeButton'
|
||||||
* options={Object}
|
* options={Object}
|
||||||
* onChange={function(text: string)}
|
* onChange={function(text: string)}
|
||||||
* onChangeMode={function(mode: string)}
|
* onChangeMode={function(mode: string)}
|
||||||
|
* onError={function(error: Error)}
|
||||||
* />
|
* />
|
||||||
*
|
*
|
||||||
* Methods:
|
* Methods:
|
||||||
|
@ -33,7 +36,8 @@ export default class TextMode extends Component {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
text: '{}'
|
text: '{}',
|
||||||
|
compiledSchema: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,7 +51,9 @@ export default class TextMode extends Component {
|
||||||
value: this.state.text,
|
value: this.state.text,
|
||||||
onInput: this.handleChange
|
onInput: this.handleChange
|
||||||
})
|
})
|
||||||
])
|
]),
|
||||||
|
|
||||||
|
this.renderSchemaErrors ()
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,11 +79,86 @@ export default class TextMode extends Component {
|
||||||
modes: this.props.options.modes,
|
modes: this.props.options.modes,
|
||||||
mode: this.props.mode,
|
mode: this.props.mode,
|
||||||
onChangeMode: this.props.onChangeMode,
|
onChangeMode: this.props.onChangeMode,
|
||||||
onError: this.handleError
|
onError: this.props.onError
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @protected */
|
||||||
|
renderSchemaErrors () {
|
||||||
|
// TODO: move the JSON Schema stuff into a separate Component?
|
||||||
|
if (!this.state.compiledSchema) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// TODO: only validate again when json is changed since last validation
|
||||||
|
const json = this.get(); // this can fail when there is no valid json
|
||||||
|
const valid = this.state.compiledSchema(json)
|
||||||
|
if (!valid) {
|
||||||
|
const allErrors = this.state.compiledSchema.errors.map(enrichSchemaError)
|
||||||
|
const limitedErrors = limitErrors(allErrors)
|
||||||
|
|
||||||
|
return h('table', {class: 'jsoneditor-text-errors'},
|
||||||
|
h('tbody', {}, limitedErrors.map(TextMode.renderSchemaError))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
// no valid JSON, don't validate
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a table row of a single JSON schema error
|
||||||
|
* @param {Error | string} error
|
||||||
|
* @return {JSX.Element}
|
||||||
|
*/
|
||||||
|
static renderSchemaError (error) {
|
||||||
|
const icon = h('input', {type: 'button', class: 'jsoneditor-schema-error'})
|
||||||
|
|
||||||
|
if (typeof error === 'string') {
|
||||||
|
return h('tr', {},
|
||||||
|
h('td', {}, icon),
|
||||||
|
h('td', {colSpan: 2}, h('pre', {}, error))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return h('tr', {}, [
|
||||||
|
h('td', {}, icon),
|
||||||
|
h('td', {}, error.dataPath),
|
||||||
|
h('td', {}, error.message)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a JSON schema for validation of the JSON object.
|
||||||
|
* To remove the schema, call JSONEditor.setSchema(null)
|
||||||
|
* @param {Object | null} schema
|
||||||
|
*/
|
||||||
|
setSchema (schema) {
|
||||||
|
if (schema) {
|
||||||
|
const ajv = this.props.options.ajv ||
|
||||||
|
Ajv && Ajv({ allErrors: true, verbose: true })
|
||||||
|
|
||||||
|
if (!ajv) {
|
||||||
|
throw new Error('Cannot validate JSON: ajv not available. ' +
|
||||||
|
'Provide ajv via options or use a JSONEditor bundle including ajv.')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
compiledSchema: ajv.compile(schema)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.setState({
|
||||||
|
compiledSchema: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the configured indentation
|
* Get the configured indentation
|
||||||
* @return {number}
|
* @return {number}
|
||||||
|
@ -106,7 +187,7 @@ export default class TextMode extends Component {
|
||||||
this.format()
|
this.format()
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
this.handleError(err)
|
this.props.onError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,17 +197,7 @@ export default class TextMode extends Component {
|
||||||
this.compact()
|
this.compact()
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
this.handleError(err)
|
this.props.onError(err)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @protected */
|
|
||||||
handleError = (err) => {
|
|
||||||
if (this.props.options && this.props.options.onError) {
|
|
||||||
this.props.options.onError(err)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.error(err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,11 +276,4 @@ export default class TextMode extends Component {
|
||||||
getText () {
|
getText () {
|
||||||
return this.state.text
|
return this.state.text
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroy the editor
|
|
||||||
*/
|
|
||||||
destroy () {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -112,7 +112,7 @@ export default class TreeMode extends Component {
|
||||||
modes: this.props.options.modes,
|
modes: this.props.options.modes,
|
||||||
mode: this.props.mode,
|
mode: this.props.mode,
|
||||||
onChangeMode: this.props.onChangeMode,
|
onChangeMode: this.props.onChangeMode,
|
||||||
onError: this.handleError
|
onError: this.props.onError
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
@ -213,16 +213,6 @@ export default class TreeMode extends Component {
|
||||||
this.emitOnChange (actions, result.revert)
|
this.emitOnChange (actions, result.revert)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @private */
|
|
||||||
handleError = (err) => {
|
|
||||||
if (this.props.options && this.props.options.onError) {
|
|
||||||
this.props.options.onError(err)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emit an onChange event when there is a listener for it.
|
* Emit an onChange event when there is a listener for it.
|
||||||
* @param {JSONPatch} patch
|
* @param {JSONPatch} patch
|
||||||
|
@ -372,6 +362,16 @@ export default class TreeMode extends Component {
|
||||||
return JSON.stringify(this.get(), null, indentation)
|
return JSON.stringify(this.get(), null, indentation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a JSON schema for validation of the JSON object.
|
||||||
|
* To remove the schema, call JSONEditor.setSchema(null)
|
||||||
|
* @param {Object | null} schema
|
||||||
|
*/
|
||||||
|
setSchema (schema) {
|
||||||
|
// TODO: implement setSchema for TreeMode
|
||||||
|
console.error('setSchema not yet implemented for TreeMode')
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expand one or multiple objects or arrays
|
* Expand one or multiple objects or arrays
|
||||||
* @param {Path | function (path: Path) : boolean} callback
|
* @param {Path | function (path: Path) : boolean} callback
|
||||||
|
@ -422,13 +422,6 @@ export default class TreeMode extends Component {
|
||||||
: TreeMode.expandAll(path)
|
: TreeMode.expandAll(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroy the editor
|
|
||||||
*/
|
|
||||||
destroy () {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default function to determine whether or not to expand a node initially
|
* Default function to determine whether or not to expand a node initially
|
||||||
*
|
*
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#container {
|
#container {
|
||||||
height: 500px;
|
height: 400px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width : 800px;
|
max-width : 800px;
|
||||||
}
|
}
|
||||||
|
@ -24,11 +24,13 @@
|
||||||
<button id="setJSON">Set JSON</button>
|
<button id="setJSON">Set JSON</button>
|
||||||
<button id="getJSON">Get JSON</button>
|
<button id="getJSON">Get JSON</button>
|
||||||
|
|
||||||
|
<button id="toggleSchema">Toggle JSON Schema</button>
|
||||||
|
|
||||||
<label for="mode">mode:
|
<label for="mode">mode:
|
||||||
<select id="mode">
|
<select id="mode">
|
||||||
<option value="text">text</option>
|
<option value="text">text</option>
|
||||||
<option value="code">code</option>
|
<option value="code" selected>code</option>
|
||||||
<option value="tree" selected>tree</option>
|
<option value="tree">tree</option>
|
||||||
<option value="form">form</option>
|
<option value="form">form</option>
|
||||||
<option value="view">view</option>
|
<option value="view">view</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -83,6 +85,28 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
"title": "Example Schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"firstName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"lastName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"gender": {
|
||||||
|
"enum": ["male", "female"]
|
||||||
|
},
|
||||||
|
"age": {
|
||||||
|
"description": "Age in years",
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["firstName", "lastName"]
|
||||||
|
}
|
||||||
|
|
||||||
// set json
|
// set json
|
||||||
document.getElementById('setJSON').onclick = function () {
|
document.getElementById('setJSON').onclick = function () {
|
||||||
editor.set(largeJson, {
|
editor.set(largeJson, {
|
||||||
|
@ -92,6 +116,13 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set schema
|
||||||
|
let hasSchema = false
|
||||||
|
document.getElementById('toggleSchema').onclick = function () {
|
||||||
|
editor.setSchema(hasSchema ? null : schema)
|
||||||
|
hasSchema = !hasSchema
|
||||||
|
}
|
||||||
|
|
||||||
// get json
|
// get json
|
||||||
document.getElementById('getJSON').onclick = function () {
|
document.getElementById('getJSON').onclick = function () {
|
||||||
const json = editor.get()
|
const json = editor.get()
|
||||||
|
|
66
src/index.js
66
src/index.js
|
@ -1,4 +1,4 @@
|
||||||
import { h, render } from 'preact'
|
import { h, render, } from 'preact'
|
||||||
import CodeMode from './components/CodeMode'
|
import CodeMode from './components/CodeMode'
|
||||||
import TextMode from './components/TextMode'
|
import TextMode from './components/TextMode'
|
||||||
import TreeMode from './components/TreeMode'
|
import TreeMode from './components/TreeMode'
|
||||||
|
@ -30,13 +30,9 @@ function jsoneditor (container, options = {}) {
|
||||||
const editor = {
|
const editor = {
|
||||||
isJSONEditor: true,
|
isJSONEditor: true,
|
||||||
|
|
||||||
utils: {
|
|
||||||
compileJSONPointer,
|
|
||||||
parseJSONPointer
|
|
||||||
},
|
|
||||||
|
|
||||||
_container: container,
|
_container: container,
|
||||||
_options: options,
|
_options: options,
|
||||||
|
_schema: null,
|
||||||
_modes: modes,
|
_modes: modes,
|
||||||
_mode: null,
|
_mode: null,
|
||||||
_element: null,
|
_element: null,
|
||||||
|
@ -76,6 +72,16 @@ function jsoneditor (container, options = {}) {
|
||||||
return editor._component.getText()
|
return editor._component.getText()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a JSON schema for validation of the JSON object.
|
||||||
|
* To remove the schema, call JSONEditor.setSchema(null)
|
||||||
|
* @param {Object | null} schema
|
||||||
|
*/
|
||||||
|
editor.setSchema = function (schema) {
|
||||||
|
editor._schema = schema || null
|
||||||
|
editor._component.setSchema(schema)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expand one or multiple objects or arrays.
|
* Expand one or multiple objects or arrays.
|
||||||
*
|
*
|
||||||
|
@ -154,15 +160,33 @@ function jsoneditor (container, options = {}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleError (err) {
|
||||||
|
if (editor._options && editor._options.onError) {
|
||||||
|
editor._options.onError(err)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// create new component
|
// create new component
|
||||||
element = render(
|
element = render(
|
||||||
h(constructor, {
|
h(constructor, {
|
||||||
mode,
|
mode,
|
||||||
options: editor._options,
|
options: editor._options,
|
||||||
onChangeMode: handleChangeMode
|
onChangeMode: handleChangeMode,
|
||||||
|
onError: handleError
|
||||||
}),
|
}),
|
||||||
editor._container)
|
editor._container)
|
||||||
|
|
||||||
|
// apply JSON schema (if any)
|
||||||
|
try {
|
||||||
|
element._component.setSchema(editor._schema)
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
handleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
// set JSON (this can throw an error)
|
// set JSON (this can throw an error)
|
||||||
const text = editor._component ? editor._component.getText() : '{}'
|
const text = editor._component ? editor._component.getText() : '{}'
|
||||||
element._component.setText(text)
|
element._component.setText(text)
|
||||||
|
@ -174,8 +198,7 @@ function jsoneditor (container, options = {}) {
|
||||||
if (success) {
|
if (success) {
|
||||||
// destroy previous component
|
// destroy previous component
|
||||||
if (editor._element) {
|
if (editor._element) {
|
||||||
editor._element._component.destroy()
|
unrender(container, editor._element)
|
||||||
editor._element.parentNode.removeChild(editor._element)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
editor._mode = mode
|
editor._mode = mode
|
||||||
|
@ -185,7 +208,8 @@ function jsoneditor (container, options = {}) {
|
||||||
else {
|
else {
|
||||||
// TODO: fall back to text mode when loading code mode failed?
|
// TODO: fall back to text mode when loading code mode failed?
|
||||||
|
|
||||||
// remove the just created component if any (where construction or setText failed)
|
// remove the just created component if an error occurred during construction
|
||||||
|
// (for example when construction or setText failed)
|
||||||
const childCount = editor._container.children.length
|
const childCount = editor._container.children.length
|
||||||
if (childCount !== initialChildCount) {
|
if (childCount !== initialChildCount) {
|
||||||
editor._container.removeChild(editor._container.lastChild)
|
editor._container.removeChild(editor._container.lastChild)
|
||||||
|
@ -194,11 +218,31 @@ function jsoneditor (container, options = {}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: implement destroy
|
/**
|
||||||
|
* Remove the editor from the DOM and clean up workers
|
||||||
|
*/
|
||||||
|
editor.destroy = function () {
|
||||||
|
unrender(container, editor._element)
|
||||||
|
}
|
||||||
|
|
||||||
editor.setMode(options && options.mode || 'tree')
|
editor.setMode(options && options.mode || 'tree')
|
||||||
|
|
||||||
return editor
|
return editor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// expose util functions
|
||||||
|
jsoneditor.utils = {
|
||||||
|
compileJSONPointer,
|
||||||
|
parseJSONPointer
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy a rendered preact component
|
||||||
|
* @param container
|
||||||
|
* @param root
|
||||||
|
*/
|
||||||
|
function unrender (container, root) {
|
||||||
|
render('', container, root);
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = jsoneditor
|
module.exports = jsoneditor
|
||||||
|
|
|
@ -567,3 +567,37 @@ textarea.jsoneditor-text {
|
||||||
font-size: @fontSize;
|
font-size: @fontSize;
|
||||||
color: @black;
|
color: @black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.jsoneditor-code {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: @contentsMinHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* JSON schema errors displayed at the bottom of the editor in mode text and code */
|
||||||
|
|
||||||
|
.jsoneditor-text-errors {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background-color: #ffef8b;
|
||||||
|
border-top: 1px solid #ffd700;
|
||||||
|
|
||||||
|
font-family: @fontFamily;
|
||||||
|
font-size: @fontSize;
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 3px 6px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jsoneditor-schema-error {
|
||||||
|
user-select: none;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 4px 0 0;
|
||||||
|
background: url('img/jsoneditor-icons.svg') -168px -46px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -32,17 +32,19 @@
|
||||||
*
|
*
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
* name: string?,
|
* name: string?,
|
||||||
* mode?: 'code' | 'form' | 'text' | 'tree' | 'view',
|
* mode: 'code' | 'form' | 'text' | 'tree' | 'view'?,
|
||||||
* modes?: string[],
|
* modes: string[]?,
|
||||||
* history?: boolean,
|
* history: boolean?,
|
||||||
* indentation?: number | string,
|
* indentation: number | string?,
|
||||||
* onChange?: function (patch: JSONPatch, revert: JSONPatch),
|
* onChange: function (patch: JSONPatch, revert: JSONPatch)?,
|
||||||
* onChangeText?: function (),
|
* onChangeText: function ()?,
|
||||||
* onChangeMode?: function (mode: string, prevMode: string),
|
* onChangeMode: function (mode: string, prevMode: string)?,
|
||||||
* onError?: function (err: Error),
|
* onError: function (err: Error)?,
|
||||||
* isPropertyEditable?: function (Path) : boolean
|
* isPropertyEditable: function (Path)?
|
||||||
* isValueEditable?: function (Path) : boolean,
|
* isValueEditable: function (Path)?,
|
||||||
* escapeUnicode:? boolean
|
* escapeUnicode: boolean?,
|
||||||
|
* ajv: Object?
|
||||||
|
* ace: Object?
|
||||||
* }} Options
|
* }} Options
|
||||||
*
|
*
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
|
||||||
|
// TODO: make MAX_ERRORS configurable
|
||||||
|
export const MAX_ERRORS = 3; // maximum number of displayed errors at the bottom
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enrich the error message of a JSON schema error
|
||||||
|
* @param {Object} error
|
||||||
|
* @return {Object} The improved error
|
||||||
|
*/
|
||||||
|
export function enrichSchemaError(error) {
|
||||||
|
if (error.keyword === 'enum' && Array.isArray(error.schema)) {
|
||||||
|
let enums = error.schema
|
||||||
|
if (enums) {
|
||||||
|
enums = enums.map(JSON.stringify)
|
||||||
|
|
||||||
|
if (enums.length > 5) {
|
||||||
|
const more = ['(' + (enums.length - 5) + ' more...)']
|
||||||
|
enums = enums.slice(0, 5)
|
||||||
|
enums.push(more)
|
||||||
|
}
|
||||||
|
error.message = 'should be equal to one of: ' + enums.join(', ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.keyword === 'additionalProperties') {
|
||||||
|
error.message = 'should NOT have additional property: ' + error.params.additionalProperty
|
||||||
|
}
|
||||||
|
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limit the number of errors.
|
||||||
|
* If the number of errors exceeds the maximum, the tail is removed and
|
||||||
|
* a message that there are more errors is added
|
||||||
|
* @param {Array} errors
|
||||||
|
* @return {Array} Returns limited items
|
||||||
|
*/
|
||||||
|
export function limitErrors (errors) {
|
||||||
|
if (errors.length > MAX_ERRORS) {
|
||||||
|
const hidden = errors.length - MAX_ERRORS
|
||||||
|
let limitedErrors = errors.slice(0, MAX_ERRORS)
|
||||||
|
limitedErrors.push('(' + hidden + ' more errors...)')
|
||||||
|
return limitedErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
Loading…
Reference in New Issue