diff --git a/src/JSONNode.js b/src/JSONNode.js index 52c4335..ffa252f 100644 --- a/src/JSONNode.js +++ b/src/JSONNode.js @@ -1,19 +1,14 @@ import { h, Component } from 'preact' import { escapeHTML, unescapeHTML } from './utils/stringUtils' import { getInnerText } from './utils/domUtils' -import {stringConvert, valueType, isUrl, isObject} from './utils/typeUtils' +import {stringConvert, valueType, isUrl} from './utils/typeUtils' +import { last } from './utils/arrayUtils' export default class JSONNode extends Component { constructor (props) { super(props) - this.state = { - expanded: false, - value: props.value - } - - this.handleChangeField = this.handleChangeField.bind(this) - this.handleBlurValue = this.handleBlurValue.bind(this) + this.handleChangeProperty = this.handleChangeProperty.bind(this) this.handleChangeValue = this.handleChangeValue.bind(this) this.handleClickValue = this.handleClickValue.bind(this) this.handleKeyDownValue = this.handleKeyDownValue.bind(this) @@ -21,10 +16,10 @@ export default class JSONNode extends Component { } render (props) { - if (Array.isArray(props.value)) { + if (props.data.type === 'array') { return this.renderJSONArray(props) } - else if (isObject(props.value)) { + else if (props.data.type === 'object') { return this.renderJSONObject(props) } else { @@ -32,27 +27,27 @@ export default class JSONNode extends Component { } } - renderJSONObject ({parent, index, field, value, onChangeValue, onChangeField}) { - const childCount = Object.keys(value).length + renderJSONObject ({data, index, options, onChangeValue, onChangeProperty, onExpand}) { + const childCount = data.childs.length const contents = [ h('div', {class: 'jsoneditor-node jsoneditor-object'}, [ this.renderExpandButton(), - this.renderField(parent, index, field, value), + this.renderProperty(data, index, options), this.renderSeparator(), this.renderReadonly(`{${childCount}}`, `Array containing ${childCount} items`) ]) ] - if (this.state.expanded) { - const childs = this.state.expanded && Object.keys(value).map(f => { - return h(JSONNode, { - parent: this, - field: f, - value: value[f], - onChangeValue, - onChangeField - }) - }) + if (data.expanded) { + const childs = data.childs.map(child => { + return h(JSONNode, { + data: child, + options, + onChangeValue, + onChangeProperty, + onExpand + }) + }) contents.push(h('ul', {class: 'jsoneditor-list'}, childs)) } @@ -60,27 +55,28 @@ export default class JSONNode extends Component { return h('li', {}, contents) } - renderJSONArray ({parent, index, field, value, onChangeValue, onChangeField}) { - const childCount = value.length + renderJSONArray ({data, index, options, onChangeValue, onChangeProperty, onExpand}) { + const childCount = data.childs.length const contents = [ h('div', {class: 'jsoneditor-node jsoneditor-array'}, [ this.renderExpandButton(), - this.renderField(parent, index, field, value), + this.renderProperty(data, index, options), this.renderSeparator(), this.renderReadonly(`[${childCount}]`, `Array containing ${childCount} items`) ]) ] - if (this.state.expanded) { - const childs = this.state.expanded && value.map((v, i) => { - return h(JSONNode, { - parent: this, - index: i, - value: v, - onChangeValue, - onChangeField - }) - }) + if (data.expanded) { + const childs = data.childs.map((child, index) => { + return h(JSONNode, { + data: child, + index, + options, + onChangeValue, + onChangeProperty, + onExpand + }) + }) contents.push(h('ul', {class: 'jsoneditor-list'}, childs)) } @@ -88,13 +84,13 @@ export default class JSONNode extends Component { return h('li', {}, contents) } - renderJSONValue ({parent, index, field, value}) { + renderJSONValue ({data, index, options}) { return h('li', {}, [ h('div', {class: 'jsoneditor-node'}, [ h('div', {class: 'jsoneditor-button-placeholder'}), - this.renderField(parent, index, field, value), + this.renderProperty(data, index, options), this.renderSeparator(), - this.renderValue(this.state.value) + this.renderValue(data.value) ]) ]) } @@ -103,22 +99,31 @@ export default class JSONNode extends Component { return h('div', {class: 'jsoneditor-readonly', contentEditable: false, title}, text) } - renderField (parent, index, field, value) { - const readonly = !parent || index !== undefined - const content = !parent - ? valueType(value) // render 'object' or 'array', or 'number' as field + renderProperty (data, index, options) { + const property = last(data.path) + const isProperty = typeof property === 'string' + const content = isProperty + ? escapeHTML(property) // render the property name : index !== undefined - ? index // render the array index of the item - : escapeHTML(field) // render the property name + ? index // render the array index of the item + : JSONNode._rootName(data, options) return h('div', { - class: 'jsoneditor-field' + (readonly ? ' jsoneditor-readonly' : ''), - contentEditable: !readonly, + class: 'jsoneditor-property' + (isProperty ? '' : ' jsoneditor-readonly'), + contentEditable: isProperty, spellCheck: 'false', - onBlur: this.handleChangeField + onInput: this.handleChangeProperty }, content) } + static _rootName (data, options) { + return typeof options.name === 'string' + ? options.name + : (data.type === 'object' || data.type === 'array') + ? data.type + : valueType(data.value) + } + renderSeparator() { return h('div', {class: 'jsoneditor-separator'}, ':') } @@ -133,7 +138,6 @@ export default class JSONNode extends Component { contentEditable: true, spellCheck: 'false', onInput: this.handleChangeValue, - onBlur: this.handleBlurValue, onClick: this.handleClickValue, onKeyDown: this.handleKeyDownValue, title: _isUrl ? 'Ctrl+Click or ctrl+Enter to open url' : null @@ -141,43 +145,28 @@ export default class JSONNode extends Component { } renderExpandButton () { - const className = `jsoneditor-button jsoneditor-${this.state.expanded ? 'expanded' : 'collapsed'}` + const className = `jsoneditor-button jsoneditor-${this.props.data.expanded ? 'expanded' : 'collapsed'}` return h('div', {class: 'jsoneditor-button-container'}, h('button', {class: className, onClick: this.handleExpand}) ) } shouldComponentUpdate(nextProps, nextState) { - return Object.keys(nextProps).some(prop => this.props[prop] !== nextProps[prop]) || - (this.state && Object.keys(nextState).some(prop => this.state[prop] !== nextState[prop])) + return Object.keys(nextProps).some(prop => this.props[prop] !== nextProps[prop]) } - componentWillReceiveProps(nextProps) { - this.setState({ - value: nextProps.value - }) - } + handleChangeProperty (event) { + const property = unescapeHTML(getInnerText(event.target)) + const oldPath = this.props.data.path + const newPath = oldPath.slice(0, oldPath.length - 1).concat(property) - handleChangeField (event) { - const path = this.props.parent.getPath() - const newField = unescapeHTML(getInnerText(event.target)) - const oldField = this.props.field - if (newField !== oldField) { - this.props.onChangeField(path, oldField, newField) - } - } - - handleBlurValue (event) { - const path = this.getPath() - if (this.state.value !== this.props.value) { - this.props.onChangeValue(path, this.state.value) - } + this.props.onChangeProperty(oldPath, newPath) } handleChangeValue (event) { - this.setState({ - value: this._getValueFromEvent(event) - }) + const value = this._getValueFromEvent(event) + + this.props.onChangeValue(this.props.data.path, value) } handleClickValue (event) { @@ -193,9 +182,7 @@ export default class JSONNode extends Component { } handleExpand (event) { - this.setState({ - expanded: !this.state.expanded - }) + this.props.onExpand(this.props.data.path, !this.props.data.expanded) } _openLinkIfUrl (event) { @@ -212,19 +199,4 @@ export default class JSONNode extends Component { _getValueFromEvent (event) { return stringConvert(unescapeHTML(getInnerText(event.target))) } - - getPath () { - const path = [] - - let node = this - while (node) { - path.unshift(node.props.field || node.props.index) - - node = node.props.parent - } - - path.shift() // remove the root node again (null) - - return path - } } diff --git a/src/Main.js b/src/Main.js index cbd9d07..972f022 100644 --- a/src/Main.js +++ b/src/Main.js @@ -1,6 +1,8 @@ import { h, Component } from 'preact' -import { getIn, setIn, renameField } from './utils/objectUtils' +import { setIn } from './utils/objectUtils' +import { last } from './utils/arrayUtils' +import { isObject } from './utils/typeUtils' import JSONNode from './JSONNode' export default class Main extends Component { @@ -8,50 +10,173 @@ export default class Main extends Component { super(props) this.state = { - json: props.json || {} + options: Object.assign({ + name: null, + expand: Main.expand + }, props.options || {}), + + data: { + type: 'object', + expanded: true, + path: [], + childs: [] + } } - this.onChangeValue = this.onChangeValue.bind(this) - this.onChangeField = this.onChangeField.bind(this) + this._onExpand = this._onExpand.bind(this) + this._onChangeValue = this._onChangeValue.bind(this) + this._onChangeProperty = this._onChangeProperty.bind(this) } - render(props, state) { - return h('div', {class: 'jsoneditor', onInput: this.onInput}, [ + render() { + return h('div', {class: 'jsoneditor'}, [ h('ul', {class: 'jsoneditor-list'}, [ h(JSONNode, { - parent: null, - field: null, - value: state.json, - onChangeField: this.onChangeField, - onChangeValue: this.onChangeValue + data: this.state.data, + options: this.state.options, + onChangeProperty: this._onChangeProperty, + onChangeValue: this._onChangeValue, + onExpand: this._onExpand }) ]) ]) } - onChangeValue (path, value) { - console.log('onChangeValue', path, value) + _onChangeValue (path, value) { + console.log('_onChangeValue', path, value) + + const modelPath = Main._pathToModelPath(this.state.data, path).concat('value') + this.setState({ - json: setIn(this.state.json, path, value) + data: setIn(this.state.data, modelPath, value) }) } - onChangeField (path, oldField, newField) { - console.log('onChangeField', path, newField, oldField) + _onChangeProperty (oldPath, newPath) { + console.log('_onChangeProperty', oldPath, newPath) + + const modelPath = Main._pathToModelPath(this.state.data, oldPath).concat('path') - const oldObject = getIn(this.state.json, path) - const newObject = renameField(oldObject, oldField, newField) - this.setState({ - json: setIn(this.state.json, path, newObject) + data: setIn(this.state.data, modelPath, newPath) }) } + _onExpand(path, expand) { + const modelPath = Main._pathToModelPath(this.state.data, path).concat('expanded') + + this.setState({ + data: setIn(this.state.data, modelPath, expand) + }) + } + + // TODO: comment get () { - return this.state.json + return Main._modelToJson(this.state.data) } + // TODO: comment set (json) { - this.setState({json}) + this.setState({ + data: Main._jsonToModel([], json, this.state.options.expand) + }) } + + /** + * Default function to determine whether or not to expand a node initially + * @param {Array.} path + * @return {boolean} + */ + static expand (path) { + return path.length === 0 + } + + /** + * Convert a path of a JSON object into a path in the corresponding data model + * @param {Model} model + * @param {Array.} path + * @return {Array.} modelPath + * @private + */ + static _pathToModelPath (model, path) { + if (path.length === 0) { + return [] + } + + let index + if (typeof path[0] === 'number') { + // index of an array + index = path[0] + } + else { + // object property. find the index of this property + index = model.childs.findIndex(child => last(child.path) === path[0]) + } + + return ['childs', index] + .concat(Main._pathToModelPath(model.childs[index], path.slice(1))) + } + + /** + * Convert a JSON object into the internally used data model + * @param {Array.} path + * @param {Object | Array | string | number | boolean | null} value + * @param {function(path: Array.)} expand + * @return {Model} + * @private + */ + static _jsonToModel (path, value, expand) { + if (Array.isArray(value)) { + return { + type: 'array', + expanded: expand(path), + path, + childs: value.map((child, index) => Main._jsonToModel(path.concat(index), child, expand)) + } + } + else if (isObject(value)) { + return { + type: 'object', + expanded: expand(path), + path, + childs: Object.keys(value).map(prop => { + return Main._jsonToModel(path.concat(prop), value[prop], expand) + }) + } + } + else { + return { + type: 'auto', + path, + value + } + } + } + + /** + * Convert the internal data model to a regular JSON object + * @param {Model} model + * @return {Object | Array | string | number | boolean | null} json + * @private + */ + static _modelToJson (model) { + if (model.type === 'array') { + return model.childs.map(Main._modelToJson) + } + else if (model.type === 'object') { + const object = {} + + model.childs.forEach(child => { + const prop = last(child.path) + object[prop] = Main._modelToJson(child) + }) + + return object + } + else { + // type 'auto' or 'string' + return model.value + } + } + } diff --git a/src/develop.html b/src/develop.html index 43f2442..4a2a404 100644 --- a/src/develop.html +++ b/src/develop.html @@ -16,7 +16,9 @@