diff --git a/src/Editor.jsx b/src/Editor.jsx index 9a1cc83..83d0a15 100644 --- a/src/Editor.jsx +++ b/src/Editor.jsx @@ -1,257 +1,282 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import JSONEditor from 'jsoneditor/dist/jsoneditor-minimalist'; -import 'jsoneditor/dist/jsoneditor.css'; -import './fixAce.css'; - -/** - * @typedef {{ - * tree: string, - * view: string, - * form: string, - * code: string, - * text: string, - * allValues: Array - * }} TJsonEditorModes - */ -const modes = { - tree: 'tree', - view: 'view', - form: 'form', - code: 'code', - text: 'text' -}; - -const values = Object.values(modes); - -modes.allValues = values; - -/** - * @type {object} - * @property {object} [value] - * @property {string} [mode='tree'] - Set the editor mode. - * @property {string} [name=undefined] - Initial field name for the root node - * @property {object} [schema] - Validate the JSON object against a JSON schema. - * @property {object} [schemaRefs] - Schemas that are referenced using - * the $ref property - * @property {Function} [onChange] - Set a callback function - * triggered when the contents of the JSONEditor change. - * Called without parameters. Will only be triggered on changes made by the user. - * Return new json. - * @property {Function} [onError] - Set a callback function triggered when an error occurs. - * Invoked with the error as first argument. - * The callback is only invoked for errors triggered by a users action, - * like switching from code mode to tree mode or clicking - * the Format button whilst the editor doesn't contain valid JSON. - * @property {Function} [onModeChange] - Set a callback function - * triggered right after the mode is changed by the user. - * @property {object} [ace] - Provide a version of the Ace editor. - * Only applicable when mode is code - * @property {object} [ajv] - Provide a instance of ajv, - * the library used for JSON schema validation. - * @property {string} [theme] - Set the Ace editor theme, - * uses included 'ace/theme/jsoneditor' by default. - * @property {boolean} [history=false] - Enables history, - * adds a button Undo and Redo to the menu of the JSONEditor. Only applicable when - * mode is 'tree' or 'form' - * @property {boolean} [navigationBar=true] - Adds navigation bar to the menu - * the navigation bar visualize the current position on the - * tree structure as well as allows breadcrumbs navigation. - * @property {boolean} [statusBar=true] - Adds status bar to the buttom of the editor - * the status bar shows the cursor position and a count of the selected characters. - * Only applicable when mode is 'code' or 'text'. - * @property {boolean} [search=true] - Enables a search box in - * the upper right corner of the JSONEditor. - * @property {Array} [allowedModes] - Create a box in the editor menu where - * the user can switch between the specified modes. - * @property {(string|PropTypes.elementType)} [tag='div'] - Html element, or react element to render - * @property {object} [htmlElementProps] - html element custom props - * @property {Function} [innerRef] - callback to get html element reference - */ -export default class Editor extends Component { - constructor(props) { - super(props); - - this.htmlElementRef = null; - this.jsonEditor = null; - - this.handleChange = this.handleChange.bind(this); - this.setRef = this.setRef.bind(this); - this.collapseAll = this.collapseAll.bind(this); - this.expandAll = this.expandAll.bind(this); - this.focus = this.focus.bind(this); - } - - componentDidMount() { - const { - allowedModes, - innerRef, - htmlElementProps, - tag, - onChange, - ...rest - } = this.props; - - this.createEditor({ - ...rest, - modes: allowedModes - }); - } - - // eslint-disable-next-line react/sort-comp - componentDidUpdate({ - allowedModes, - schema, - name, - theme, - schemaRefs, - innerRef, - htmlElementProps, - tag, - onChange, - ...rest - }) { - if (this.jsonEditor) { - if (theme !== this.props.theme) { - this.createEditor({ - ...rest, - theme, - modes: allowedModes - }); - } else { - if (schema !== this.props.schema - || schemaRefs !== this.props.schemaRefs - ) { - this.jsonEditor.setSchema(schema, schemaRefs); - } - - if (name !== this.jsonEditor.getName()) { - this.jsonEditor.setName(name); - } - } - } - } - - shouldComponentUpdate({ htmlElementProps }) { - return htmlElementProps !== this.props.htmlElementProps; - } - - componentWillUnmount() { - if (this.jsonEditor) { - this.jsonEditor.destroy(); - this.jsonEditor = null; - } - } - - setRef(element) { - this.htmlElementRef = element; - if (this.props.innerRef) { - this.props.innerRef(element); - } - } - - createEditor({ value, ...rest }) { - if (this.jsonEditor) { - this.jsonEditor.destroy(); - } - - this.jsonEditor = new JSONEditor(this.htmlElementRef, { - onChange: this.handleChange, - ...rest - }); - - this.jsonEditor.set(value); - } - - handleChange() { - if (this.props.onChange) { - try { - const text = this.jsonEditor.getText(); - if (text === '') { - this.props.onChange(null); - } - - const currentJson = this.jsonEditor.get(); - if (this.props.value !== currentJson) { - this.props.onChange(currentJson); - } - } catch (err) { - this.err = err; - } - } - } - - collapseAll() { - if (this.jsonEditor) { - this.jsonEditor.collapseAll(); - } - } - - expandAll() { - if (this.jsonEditor) { - this.jsonEditor.expandAll(); - } - } - - focus() { - if (this.jsonEditor) { - this.jsonEditor.focus(); - } - } - - render() { - const { - htmlElementProps, - tag - } = this.props; - - return React.createElement( - tag, - { - ...htmlElementProps, - ref: this.setRef - } - ); - } -} - -Editor.propTypes = { - // jsoneditor props - value: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), - mode: PropTypes.oneOf(values), - name: PropTypes.string, - schema: PropTypes.object, - schemaRefs: PropTypes.object, - - onChange: PropTypes.func, - onError: PropTypes.func, - onModeChange: PropTypes.func, - - ace: PropTypes.object, - ajv: PropTypes.object, - theme: PropTypes.string, - history: PropTypes.bool, - navigationBar: PropTypes.bool, - statusBar: PropTypes.bool, - search: PropTypes.bool, - allowedModes: PropTypes.arrayOf(PropTypes.oneOf(values)), - - // custom props - tag: PropTypes.oneOfType([PropTypes.string, PropTypes.elementType]), - htmlElementProps: PropTypes.object, - innerRef: PropTypes.func, -}; - -Editor.defaultProps = { - tag: 'div', - mode: modes.tree, - history: false, - search: true, - navigationBar: true, - statusBar: true, -}; - -/** - * @type TJsonEditorModes - */ -Editor.modes = modes; +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import JSONEditor from 'jsoneditor/dist/jsoneditor-minimalist'; +import 'jsoneditor/dist/jsoneditor.css'; +import './fixAce.css'; + +/** + * @typedef {{ + * tree: string, + * view: string, + * form: string, + * code: string, + * text: string, + * allValues: Array + * }} TJsonEditorModes + */ +const modes = { + tree: 'tree', + view: 'view', + form: 'form', + code: 'code', + text: 'text' +}; + +const values = Object.values(modes); + +modes.allValues = values; + +/** + * @type {object} + * @property {object} [value] + * @property {string} [mode='tree'] - Set the editor mode. + * @property {string} [name=undefined] - Initial field name for the root node + * @property {object} [schema] - Validate the JSON object against a JSON schema. + * @property {object} [schemaRefs] - Schemas that are referenced using + * the $ref property + * @property {Function} [onChange] - Set a callback function + * triggered when the contents of the JSONEditor change. + * Called without parameters. Will only be triggered on changes made by the user. + * Return new json. + * @property {Function} [onError] - Set a callback function triggered when an error occurs. + * Invoked with the error as first argument. + * The callback is only invoked for errors triggered by a users action, + * like switching from code mode to tree mode or clicking + * the Format button whilst the editor doesn't contain valid JSON. + * @property {Function} [onModeChange] - Set a callback function + * triggered right after the mode is changed by the user. + * @property {object} [ace] - Provide a version of the Ace editor. + * Only applicable when mode is code + * @property {object} [ajv] - Provide a instance of ajv, + * the library used for JSON schema validation. + * @property {string} [theme] - Set the Ace editor theme, + * uses included 'ace/theme/jsoneditor' by default. + * @property {boolean} [history=false] - Enables history, + * adds a button Undo and Redo to the menu of the JSONEditor. Only applicable when + * mode is 'tree' or 'form' + * @property {boolean} [navigationBar=true] - Adds navigation bar to the menu + * the navigation bar visualize the current position on the + * tree structure as well as allows breadcrumbs navigation. + * @property {boolean} [statusBar=true] - Adds status bar to the buttom of the editor + * the status bar shows the cursor position and a count of the selected characters. + * Only applicable when mode is 'code' or 'text'. + * @property {boolean} [search=true] - Enables a search box in + * the upper right corner of the JSONEditor. + * @property {Array} [allowedModes] - Create a box in the editor menu where + * the user can switch between the specified modes. + * @property {(string|PropTypes.elementType)} [tag='div'] - Html element, or react element to render + * @property {object} [htmlElementProps] - html element custom props + * @property {Function} [innerRef] - callback to get html element reference + */ +export default class Editor extends Component { + constructor(props) { + super(props); + + this.htmlElementRef = null; + this.jsonEditor = null; + + this.handleChange = this.handleChange.bind(this); + this.handleError = this.handleError.bind(this); + this.setRef = this.setRef.bind(this); + this.collapseAll = this.collapseAll.bind(this); + this.expandAll = this.expandAll.bind(this); + this.focus = this.focus.bind(this); + this.state = { + previousJson: this.props.value, + }; + } + + componentDidMount() { + const { + allowedModes, + innerRef, + htmlElementProps, + tag, + onChange, + onError, + ...rest + } = this.props; + + this.createEditor({ + ...rest, + modes: allowedModes + }); + } + + // eslint-disable-next-line react/sort-comp + componentDidUpdate({ + allowedModes, + schema, + name, + theme, + schemaRefs, + innerRef, + htmlElementProps, + tag, + onChange, + onError, + ...rest + }) { + if (this.jsonEditor) { + if (theme !== this.props.theme) { + this.createEditor({ + ...rest, + theme, + modes: allowedModes + }); + } else { + if (schema !== this.props.schema + || schemaRefs !== this.props.schemaRefs + ) { + this.jsonEditor.setSchema(schema, schemaRefs); + } + + if (name !== this.jsonEditor.getName()) { + this.jsonEditor.setName(name); + } + } + } + } + + shouldComponentUpdate({ htmlElementProps }) { + return htmlElementProps !== this.props.htmlElementProps; + } + + componentWillUnmount() { + if (this.jsonEditor) { + this.jsonEditor.destroy(); + this.jsonEditor = null; + } + } + + setRef(element) { + this.htmlElementRef = element; + if (this.props.innerRef) { + this.props.innerRef(element); + } + } + + createEditor({ value, ...rest }) { + if (this.jsonEditor) { + this.jsonEditor.destroy(); + } + + this.jsonEditor = new JSONEditor(this.htmlElementRef, { + onChange: this.handleChange, + onError: this.handleError, + ...rest + }); + + this.jsonEditor.set(value); + } + + handleChange() { + if (this.props.onChange) { + try { + const text = this.jsonEditor.getText(); + if (text === '') { + this.props.onChange(null); + } + + const currentJson = this.jsonEditor.get(); + if (this.props.value !== currentJson) { + this.props.onChange(currentJson); + } + } catch (err) { + this.err = err; + } + } + if (this.props.onError) { + this.handleError(); + } + } + + handleError() { + if (this.props.onError) { + try { + const text = this.jsonEditor.getText(); + const pj = JSON.parse(text); + this.setState({ + previousJson: pj + }); + } catch (err) { + this.props.onError(this.state.previousJson, err); + this.err = err; + } + } + } + + collapseAll() { + if (this.jsonEditor) { + this.jsonEditor.collapseAll(); + } + } + + expandAll() { + if (this.jsonEditor) { + this.jsonEditor.expandAll(); + } + } + + focus() { + if (this.jsonEditor) { + this.jsonEditor.focus(); + } + } + + render() { + const { + htmlElementProps, + tag + } = this.props; + + return React.createElement( + tag, + { + ...htmlElementProps, + ref: this.setRef + } + ); + } +} + +Editor.propTypes = { + // jsoneditor props + value: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), + mode: PropTypes.oneOf(values), + name: PropTypes.string, + schema: PropTypes.object, + schemaRefs: PropTypes.object, + + onChange: PropTypes.func, + onError: PropTypes.func, + onModeChange: PropTypes.func, + + ace: PropTypes.object, + ajv: PropTypes.object, + theme: PropTypes.string, + history: PropTypes.bool, + navigationBar: PropTypes.bool, + statusBar: PropTypes.bool, + search: PropTypes.bool, + allowedModes: PropTypes.arrayOf(PropTypes.oneOf(values)), + + // custom props + tag: PropTypes.oneOfType([PropTypes.string, PropTypes.elementType]), + htmlElementProps: PropTypes.object, + innerRef: PropTypes.func, +}; + +Editor.defaultProps = { + tag: 'div', + mode: modes.tree, + history: false, + search: true, + navigationBar: true, + statusBar: true, +}; + +/** + * @type TJsonEditorModes + */ +Editor.modes = modes; diff --git a/stories/Editor.jsx b/stories/Editor.jsx index ab7953a..034344c 100644 --- a/stories/Editor.jsx +++ b/stories/Editor.jsx @@ -1,167 +1,168 @@ -import React from 'react'; -import ace from 'brace'; -import 'brace/mode/json'; -import 'brace/theme/tomorrow_night_blue'; -import { Form, Field, reduxForm } from 'redux-form'; -import { storiesOf } from '@storybook/react'; -import { action } from '@storybook/addon-actions'; -import Ajv from 'ajv'; -import Editor from '../src/Editor'; -import Decorator from './decorator'; -import { reduxDecorator } from './reduxDecorator'; -import { FieldComponent } from './FieldComponent'; - -import '../src/fixAce.css'; -import DynamicTheme from './DynamicTheme'; - -const onChangeAction = action('onChange'); -const onErrorAction = action('onError'); - -let value = { - this: 'this', - is: 'is', - 'JSON!!!111!!': 'JSON!!!111!!', - 1: 1, - 2: 1, - 3: 1, - 4: 1, - 5: 1, - 6: 1, -}; - -function handleChange(json) { - onChangeAction(JSON.stringify(json)); - value = json; -} - -function handleError(error) { - onErrorAction(JSON.stringify(error)); -} - -const schema = { - type: 'object', - properties: { - some: { - type: 'integer' - } - }, - required: ['some'] -}; - -storiesOf('JsonEditor/modes/code', module) - .addDecorator(Decorator) - .add('onChange', () => ( - - )) - .add('onError', () => ( - - )) - .add('customization', () => ( - - )); - -const onEventAction = action('onEvent'); -function handleEvent(node, event) { - onEventAction(JSON.stringify({ node, eventType: event.type })); -} - -storiesOf('JsonEditor/modes/form', module) - .addDecorator(Decorator) - .add('with onEvent handler', () => ( - - )) - .add('with history enabled', () => ( - - )); - -const submitAction = action('onSubmit'); -const form = reduxForm({ - form: 'form', - initialValues: { field: value } -})(() => ( -
{ - submitAction(JSON.stringify(formData)); - }} - > - - -)); - -storiesOf('JsonEditor/redux-form', module) - .addDecorator(reduxDecorator) - .add('controlling by redux-form', () => React.createElement(form)); - -const aceStory = storiesOf('JsonEditor/ace', module).addDecorator(Decorator); - -const aceThemes = require.context('brace/theme/', false, /.js$/); - -aceThemes.keys().forEach((key) => { - const themeName = key.replace('.js', '').replace(/^\./, ''); - aceStory.add(themeName, () => { - aceThemes(key); - return ( - - ); - }); -}); - -aceStory.add('Changing theme dynamically', () => ( - -)); - -storiesOf('JsonEditor/ajv', module) - .addDecorator(Decorator) - .add('validate', () => [ -
- Schema: - -
, -
- JSON: - -
- ]); +import React from 'react'; +import ace from 'brace'; +import 'brace/mode/json'; +import 'brace/theme/tomorrow_night_blue'; +import { Form, Field, reduxForm } from 'redux-form'; +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import Ajv from 'ajv'; +import Editor from '../src/Editor'; +import Decorator from './decorator'; +import { reduxDecorator } from './reduxDecorator'; +import { FieldComponent } from './FieldComponent'; + +import '../src/fixAce.css'; +import DynamicTheme from './DynamicTheme'; + +const onChangeAction = action('onChange'); +const onErrorAction = action('onError'); + +let value = { + this: 'this', + is: 'is', + 'JSON!!!111!!': 'JSON!!!111!!', + 1: 1, + 2: 1, + 3: 1, + 4: 1, + 5: 1, + 6: 1, +}; + +function handleChange(json) { + onChangeAction(JSON.stringify(json)); + value = json; +} + +function handleError(preJson, error) { + onErrorAction(error); + value = preJson; +} + +const schema = { + type: 'object', + properties: { + some: { + type: 'integer' + } + }, + required: ['some'] +}; + +storiesOf('JsonEditor/modes/code', module) + .addDecorator(Decorator) + .add('onChange', () => ( + + )) + .add('onError', () => ( + + )) + .add('customization', () => ( + + )); + +const onEventAction = action('onEvent'); +function handleEvent(node, event) { + onEventAction(JSON.stringify({ node, eventType: event.type })); +} + +storiesOf('JsonEditor/modes/form', module) + .addDecorator(Decorator) + .add('with onEvent handler', () => ( + + )) + .add('with history enabled', () => ( + + )); + +const submitAction = action('onSubmit'); +const form = reduxForm({ + form: 'form', + initialValues: { field: value } +})(() => ( +
{ + submitAction(JSON.stringify(formData)); + }} + > + + +)); + +storiesOf('JsonEditor/redux-form', module) + .addDecorator(reduxDecorator) + .add('controlling by redux-form', () => React.createElement(form)); + +const aceStory = storiesOf('JsonEditor/ace', module).addDecorator(Decorator); + +const aceThemes = require.context('brace/theme/', false, /.js$/); + +aceThemes.keys().forEach((key) => { + const themeName = key.replace('.js', '').replace(/^\./, ''); + aceStory.add(themeName, () => { + aceThemes(key); + return ( + + ); + }); +}); + +aceStory.add('Changing theme dynamically', () => ( + +)); + +storiesOf('JsonEditor/ajv', module) + .addDecorator(Decorator) + .add('validate', () => [ +
+ Schema: + +
, +
+ JSON: + +
+ ]);