Implemented a data model, fixed ordering of object properties, implemented options `name` and `expand`
This commit is contained in:
parent
fd631cd34c
commit
fda4f94655
138
src/JSONNode.js
138
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,25 +27,25 @@ 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 => {
|
||||
if (data.expanded) {
|
||||
const childs = data.childs.map(child => {
|
||||
return h(JSONNode, {
|
||||
parent: this,
|
||||
field: f,
|
||||
value: value[f],
|
||||
data: child,
|
||||
options,
|
||||
onChangeValue,
|
||||
onChangeField
|
||||
onChangeProperty,
|
||||
onExpand
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -60,25 +55,26 @@ 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) => {
|
||||
if (data.expanded) {
|
||||
const childs = data.childs.map((child, index) => {
|
||||
return h(JSONNode, {
|
||||
parent: this,
|
||||
index: i,
|
||||
value: v,
|
||||
data: child,
|
||||
index,
|
||||
options,
|
||||
onChangeValue,
|
||||
onChangeField
|
||||
onChangeProperty,
|
||||
onExpand
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -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
|
||||
: 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
|
||||
}
|
||||
}
|
||||
|
|
167
src/Main.js
167
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 oldObject = getIn(this.state.json, path)
|
||||
const newObject = renameField(oldObject, oldField, newField)
|
||||
const modelPath = Main._pathToModelPath(this.state.data, oldPath).concat('path')
|
||||
|
||||
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.<string | number>} 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.<string | number>} path
|
||||
* @return {Array.<string | number>} 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.<string | number>} path
|
||||
* @param {Object | Array | string | number | boolean | null} value
|
||||
* @param {function(path: Array.<string>)} 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,7 +16,9 @@
|
|||
<script>
|
||||
// create the editor
|
||||
const container = document.getElementById('container');
|
||||
const options = {};
|
||||
const options = {
|
||||
name: 'myObject'
|
||||
};
|
||||
const editor = jsoneditor(container, options);
|
||||
const json = {
|
||||
'array': [1, 2, 3],
|
||||
|
|
|
@ -4,11 +4,12 @@ import Main from './Main'
|
|||
/**
|
||||
* Factory function to create a new JSONEditor
|
||||
* @param container
|
||||
* @param {Options} options
|
||||
* @return {*}
|
||||
* @constructor
|
||||
*/
|
||||
export default function jsoneditor (container) {
|
||||
const elem = render(h(Main), container)
|
||||
export default function jsoneditor (container, options) {
|
||||
const elem = render(h(Main, {options}), container)
|
||||
return elem._component
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ ul.jsoneditor-list {
|
|||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.jsoneditor-field,
|
||||
.jsoneditor-property,
|
||||
.jsoneditor-value,
|
||||
.jsoneditor-readonly,
|
||||
.jsoneditor-separator {
|
||||
|
@ -42,7 +42,7 @@ ul.jsoneditor-list {
|
|||
font-size: 10pt;
|
||||
}
|
||||
|
||||
.jsoneditor-field,
|
||||
.jsoneditor-property,
|
||||
.jsoneditor-value,
|
||||
.jsoneditor-readonly {
|
||||
min-width: 24px;
|
||||
|
@ -58,17 +58,17 @@ ul.jsoneditor-list {
|
|||
font-size: 0;
|
||||
}
|
||||
|
||||
.jsoneditor-field,
|
||||
.jsoneditor-property,
|
||||
.jsoneditor-value {
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.jsoneditor-field:focus,
|
||||
.jsoneditor-property:focus,
|
||||
.jsoneditor-value:focus {
|
||||
box-shadow: 0 0 3px 1px #008fd5;
|
||||
}
|
||||
|
||||
.jsoneditor-field:hover,
|
||||
.jsoneditor-property:hover,
|
||||
.jsoneditor-value:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
/**
|
||||
* @typedef {{
|
||||
* type: string,
|
||||
* expanded: boolean?,
|
||||
* path: Array.<string | number>,
|
||||
* value: *?,
|
||||
* childs: Model[]?
|
||||
* }} Model
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* name: string?
|
||||
* expand: function?
|
||||
* }} Options
|
||||
*/
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* Returns the last item of an array
|
||||
* @param {Array} array
|
||||
* @return {*}
|
||||
*/
|
||||
export function last (array) {
|
||||
return array[array.length - 1]
|
||||
}
|
|
@ -79,30 +79,3 @@ export function setIn (object, path, value) {
|
|||
|
||||
return updated
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a field in an object without mutating the object itself.
|
||||
* The order of the fields in the object is maintained.
|
||||
* @param {Object} object
|
||||
* @param {string} oldField
|
||||
* @param {string} newField
|
||||
* @return {Object} Returns a clone of the object where property `oldField` is
|
||||
* renamed to `newField`
|
||||
*/
|
||||
export function renameField(object, oldField, newField) {
|
||||
const renamed = {}
|
||||
|
||||
// important: maintain the order in which we add the properties to newValue,
|
||||
// else the field will "jump" to another place
|
||||
Object.keys(object).forEach(field => {
|
||||
if (field === oldField) {
|
||||
renamed[newField] = object[oldField]
|
||||
}
|
||||
else {
|
||||
renamed[field] = object[field]
|
||||
}
|
||||
})
|
||||
|
||||
return renamed
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue