Refactored the internal data structure

This commit is contained in:
jos 2016-07-31 14:27:20 +02:00
parent 224e436828
commit 38250a38ba
3 changed files with 244 additions and 174 deletions

View File

@ -47,7 +47,7 @@ export default class JSONNode extends Component {
} }
renderJSONObject ({path, data, options, events}) { renderJSONObject ({path, data, options, events}) {
const childCount = data.childs.length const childCount = data.props.length
const contents = [ const contents = [
h('div', {class: 'jsoneditor-node jsoneditor-object'}, [ h('div', {class: 'jsoneditor-node jsoneditor-object'}, [
this.renderExpandButton(), this.renderExpandButton(),
@ -59,23 +59,23 @@ export default class JSONNode extends Component {
] ]
if (data.expanded) { if (data.expanded) {
const childs = data.childs.map(child => { const props = data.props.map(prop => {
return h(JSONNode, { return h(JSONNode, {
path: path.concat(child.prop), path: path.concat(prop.name),
data: child, data: prop.value,
options, options,
events events
}) })
}) })
contents.push(h('ul', {class: 'jsoneditor-list'}, childs)) contents.push(h('ul', {class: 'jsoneditor-list'}, props))
} }
return h('li', {}, contents) return h('li', {}, contents)
} }
renderJSONArray ({path, data, options, events}) { renderJSONArray ({path, data, options, events}) {
const childCount = data.childs.length const childCount = data.items.length
const contents = [ const contents = [
h('div', {class: 'jsoneditor-node jsoneditor-array'}, [ h('div', {class: 'jsoneditor-node jsoneditor-array'}, [
this.renderExpandButton(), this.renderExpandButton(),
@ -87,7 +87,7 @@ export default class JSONNode extends Component {
] ]
if (data.expanded) { if (data.expanded) {
const childs = data.childs.map((child, index) => { const items = data.items.map((child, index) => {
return h(JSONNode, { return h(JSONNode, {
path: path.concat(index), path: path.concat(index),
data: child, data: child,
@ -96,7 +96,7 @@ export default class JSONNode extends Component {
}) })
}) })
contents.push(h('ul', {class: 'jsoneditor-list'}, childs)) contents.push(h('ul', {class: 'jsoneditor-list'}, items))
} }
return h('li', {}, contents) return h('li', {}, contents)
@ -259,31 +259,31 @@ export default class JSONNode extends Component {
title: 'Insert a new item with type \'value\' after this item (Ctrl+Ins)', title: 'Insert a new item with type \'value\' after this item (Ctrl+Ins)',
submenuTitle: 'Select the type of the item to be inserted', submenuTitle: 'Select the type of the item to be inserted',
className: 'jsoneditor-insert', className: 'jsoneditor-insert',
click: () => events.onInsert(path, '', ''), click: () => events.onInsert(path, 'value'),
submenu: [ submenu: [
{ {
text: 'Value', text: 'Value',
className: 'jsoneditor-type-value', className: 'jsoneditor-type-value',
title: TYPE_TITLES.value, title: TYPE_TITLES.value,
click: () => events.onInsert(path, '', '') click: () => events.onInsert(path, 'value')
}, },
{ {
text: 'Array', text: 'Array',
className: 'jsoneditor-type-array', className: 'jsoneditor-type-array',
title: TYPE_TITLES.array, title: TYPE_TITLES.array,
click: () => events.onInsert(path, '', []) click: () => events.onInsert(path, 'array')
}, },
{ {
text: 'Object', text: 'Object',
className: 'jsoneditor-type-object', className: 'jsoneditor-type-object',
title: TYPE_TITLES.object, title: TYPE_TITLES.object,
click: () => events.onInsert(path, '', {}) click: () => events.onInsert(path, 'object')
}, },
{ {
text: 'String', text: 'String',
className: 'jsoneditor-type-string', className: 'jsoneditor-type-string',
title: TYPE_TITLES.string, title: TYPE_TITLES.string,
click: () => events.onInsert(path, '', '', 'string') click: () => events.onInsert(path, 'string')
} }
] ]
}); });
@ -326,7 +326,7 @@ export default class JSONNode extends Component {
} }
handleChangeProperty (event) { handleChangeProperty (event) {
const oldProp = this.props.data.prop const oldProp = last(this.props.path)
const newProp = unescapeHTML(getInnerText(event.target)) const newProp = unescapeHTML(getInnerText(event.target))
// remove last entry from the path to get the path of the parent object // remove last entry from the path to get the path of the parent object

View File

@ -23,7 +23,7 @@ export default class Main extends Component {
data: { data: {
type: 'object', type: 'object',
expanded: true, expanded: true,
childs: [] props: []
}, },
events: { events: {
@ -73,8 +73,7 @@ export default class Main extends Component {
const index = this._findIndex(path, oldProp) const index = this._findIndex(path, oldProp)
const newPath = path.concat(newProp) const newPath = path.concat(newProp)
this._setIn(path, ['childs', index, 'path'], newPath) this._setIn(path, ['props', index, 'name'], newProp)
this._setIn(path, ['childs', index, 'prop'], newProp)
} }
handleChangeType (path, type) { handleChangeType (path, type) {
@ -83,8 +82,8 @@ export default class Main extends Component {
this._setIn(path, ['type'], type) this._setIn(path, ['type'], type)
} }
handleInsert (path, prop, value, type) { handleInsert (path, type) {
console.log('handleInsert', path, prop, value, type) console.log('handleInsert', path, type)
this.handleHideContextMenu() // TODO: should be handled by the contextmenu itself this.handleHideContextMenu() // TODO: should be handled by the contextmenu itself
@ -94,25 +93,29 @@ export default class Main extends Component {
const parentPath = path.slice(0, path.length - 1) const parentPath = path.slice(0, path.length - 1)
const parent = this._getIn(parentPath) const parent = this._getIn(parentPath)
const index = parent.type === 'array' if (parent.type === 'array') {
? parseInt(afterProp) this._updateIn(parentPath, ['items'], (items) => {
: this._findIndex(parentPath, afterProp) const index = parseInt(afterProp)
const updated = items.slice(0)
this._updateIn(parentPath, ['childs'], function (childs) { updated.splice(index + 1, 0, createDataEntry(type))
const updated = childs.slice(0)
const type = isObject(value) ? 'object' : Array.isArray(value) ? 'array' : (type || 'value')
const newEntry = {
expanded: true,
type,
prop,
value,
childs: []
}
updated.splice(index + 1, 0, newEntry) return updated
})
}
else { // parent.type === 'object'
this._updateIn(parentPath, ['props'], (props) => {
const index = this._findIndex(parentPath, afterProp)
const updated = props.slice(0)
return updated updated.splice(index + 1, 0, {
}) name: '',
value: createDataEntry(type)
})
return updated
})
}
} }
handleDuplicate (path) { handleDuplicate (path) {
@ -126,19 +129,30 @@ export default class Main extends Component {
const parentPath = path.slice(0, path.length - 1) const parentPath = path.slice(0, path.length - 1)
const parent = this._getIn(parentPath) const parent = this._getIn(parentPath)
const index = parent.type === 'array' if (parent.type === 'array') {
? parseInt(prop) this._updateIn(parentPath, ['items'], (items) => {
: this._findIndex(parentPath, prop) const index = parseInt(prop)
const updated = items.slice(0)
const original = items[index]
const duplicate = cloneDeep(original)
this._updateIn(parentPath, ['childs'], function (childs) { updated.splice(index + 1, 0, duplicate)
const updated = childs.slice(0)
const original = childs[index]
const duplicate = cloneDeep(original)
updated.splice(index + 1, 0, duplicate) return updated
})
}
else { // parent.type === 'object'
this._updateIn(parentPath, ['props'], (props) => {
const index = this._findIndex(parentPath, prop)
const updated = props.slice(0)
const original = props[index]
const duplicate = cloneDeep(original)
return updated updated.splice(index + 1, 0, duplicate)
})
return updated
})
}
} }
handleRemove (path) { handleRemove (path) {
@ -146,11 +160,29 @@ export default class Main extends Component {
this.handleHideContextMenu() // TODO: should be handled by the contextmenu itself this.handleHideContextMenu() // TODO: should be handled by the contextmenu itself
this._deleteIn(path) const parentPath = path.slice(0, path.length - 1)
const parent = this._getIn(parentPath)
if (parent.type === 'array') {
const dataPath = toDataPath(this.state.data, path)
this.setState({
data: deleteIn(this.state.data, dataPath)
})
}
else { // parent.type === 'object'
const dataPath = toDataPath(this.state.data, path)
dataPath.pop() // remove the 'value' property, we want to remove the whole object property
this.setState({
data: deleteIn(this.state.data, dataPath)
})
}
} }
/** /**
* Order the childs of an array in ascending or descending order * Order the items of an array or the properties of an object in ascending
* or descending order
* @param {Array.<string | number>} path * @param {Array.<string | number>} path
* @param {'asc' | 'desc' | null} [order=null] If not provided, will toggle current ordering * @param {'asc' | 'desc' | null} [order=null] If not provided, will toggle current ordering
*/ */
@ -159,7 +191,7 @@ export default class Main extends Component {
this.handleHideContextMenu() // TODO: should be handled by the contextmenu itself this.handleHideContextMenu() // TODO: should be handled by the contextmenu itself
const entry = this._getIn(path) const object = this._getIn(path)
let _order let _order
if (order === 'asc' || order === 'desc') { if (order === 'asc' || order === 'desc') {
@ -167,23 +199,30 @@ export default class Main extends Component {
} }
else { else {
// toggle previous order // toggle previous order
_order = entry.order !== 'asc' ? 'asc' : 'desc' _order = object.order !== 'asc' ? 'asc' : 'desc'
this._setIn(path, ['order'], _order) this._setIn(path, ['order'], _order)
} }
this._updateIn(path, ['childs'], function (childs) { if (object.type === 'array') {
const ordered = childs.slice(0) this._updateIn(path, ['items'], function (items) {
const compare = _order === 'desc' ? compareDesc : compareAsc const ordered = items.slice(0)
const compare = _order === 'desc' ? compareDesc : compareAsc
if (entry.type === 'array') {
ordered.sort((a, b) => compare(a.value, b.value)) ordered.sort((a, b) => compare(a.value, b.value))
}
else { // entry.type === 'object'
ordered.sort((a, b) => compare(a.prop, b.prop))
}
return ordered return ordered
}) })
}
else { // object.type === 'object'
this._updateIn(path, ['props'], function (props) {
const ordered = props.slice(0)
const compare = _order === 'desc' ? compareDesc : compareAsc
ordered.sort((a, b) => compare(a.name, b.name))
return ordered
})
}
} }
handleExpand(path, expand) { handleExpand(path, expand) {
@ -223,50 +262,42 @@ export default class Main extends Component {
this.handleShowContextMenu({}) this.handleShowContextMenu({})
} }
_getIn (path, modelProps = []) { _getIn (path, dataProps = []) {
const modelPath = Main._pathToModelPath(this.state.data, path) const dataPath = toDataPath(this.state.data, path)
return getIn(this.state.data, modelPath.concat(modelProps)) return getIn(this.state.data, dataPath.concat(dataProps))
} }
_setIn (path, modelProps = [], value) { _setIn (path, dataProps = [], value) {
const modelPath = Main._pathToModelPath(this.state.data, path) const dataPath = toDataPath(this.state.data, path)
this.setState({ this.setState({
data: setIn(this.state.data, modelPath.concat(modelProps), value) data: setIn(this.state.data, dataPath.concat(dataProps), value)
}) })
} }
_updateIn (path, modelProps = [], callback) { _updateIn (path, dataProps = [], callback) {
const modelPath = Main._pathToModelPath(this.state.data, path) const dataPath = toDataPath(this.state.data, path)
this.setState({ this.setState({
data: updateIn(this.state.data, modelPath.concat(modelProps), callback) data: updateIn(this.state.data, dataPath.concat(dataProps), callback)
})
}
_deleteIn (path, modelProps = []) {
const modelPath = Main._pathToModelPath(this.state.data, path)
this.setState({
data: deleteIn(this.state.data, modelPath.concat(modelProps))
}) })
} }
_findIndex(path, prop) { _findIndex(path, prop) {
const object = this._getIn(path) const object = this._getIn(path)
return object.childs.findIndex(child => child.prop === prop) return object.props.findIndex(p => p.name === prop)
} }
// TODO: comment // TODO: comment
get () { get () {
return Main._modelToJson(this.state.data) return dataToJson(this.state.data)
} }
// TODO: comment // TODO: comment
set (json) { set (json) {
this.setState({ this.setState({
data: Main._jsonToModel([], null, json, this.state.options.expand) data: jsonToData([], json, this.state.options.expand)
}) })
} }
@ -282,92 +313,119 @@ export default class Main extends Component {
return path.length === 0 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 => child.prop === 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 {string | null} prop
* @param {Object | Array | string | number | boolean | null} value
* @param {function(path: Array.<string | number>)} expand
* @return {Model}
* @private
*/
static _jsonToModel (path, prop, value, expand) {
if (Array.isArray(value)) {
return {
type: 'array',
expanded: expand(path),
prop,
childs: value.map((child, index) => Main._jsonToModel(path.concat(index), null, child, expand))
}
}
else if (isObject(value)) {
return {
type: 'object',
expanded: expand(path),
prop,
childs: Object.keys(value).map(prop => {
return Main._jsonToModel(path.concat(prop), prop, value[prop], expand)
})
}
}
else {
return {
type: 'value',
prop,
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 => {
object[child.prop] = Main._modelToJson(child)
})
return object
}
else {
// type 'value' or 'string'
return model.value
}
}
} }
/**
* Convert a path of a JSON object into a path in the corresponding data model
* @param {Data} data
* @param {Array.<string | number>} path
* @return {Array.<string | number>} dataPath
* @private
*/
function toDataPath (data, path) {
if (path.length === 0) {
return []
}
let index
if (data.type === 'array') {
// index of an array
index = path[0]
return ['items', index].concat(toDataPath(data.items[index], path.slice(1)))
}
else {
// object property. find the index of this property
index = data.props.findIndex(prop => prop.name === path[0])
return ['props', index, 'value'].concat(toDataPath(data.props[index].value, 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} json
* @param {function(path: Array.<string | number>)} expand
* @return {Data}
*/
function jsonToData (path, json, expand) {
if (Array.isArray(json)) {
return {
type: 'array',
expanded: expand(path),
items: json.map((child, index) => jsonToData(path.concat(index), child, expand))
}
}
else if (isObject(json)) {
return {
type: 'object',
expanded: expand(path),
props: Object.keys(json).map(name => {
return {
name,
value: jsonToData(path.concat(name), json[name], expand)
}
})
}
}
else {
return {
type: 'json',
value: json
}
}
}
/**
* Convert the internal data model to a regular JSON object
* @param {Data} data
* @return {Object | Array | string | number | boolean | null} json
*/
function dataToJson (data) {
if (data.type === 'array') {
return data.items.map(dataToJson)
}
else if (data.type === 'object') {
const object = {}
data.props.forEach(prop => {
object[prop.name] = dataToJson(prop.value)
})
return object
}
else {
// type 'value' or 'string'
return data.value
}
}
/**
* Create a new data entry
* @param {'object' | 'array' | 'value' | 'string'} [type]
* @return {*}
*/
function createDataEntry (type) {
if (type === 'array') {
return {
type,
expanded: true,
items: []
}
}
else if (type === 'object') {
return {
type,
expanded: true,
props: []
}
}
else {
return {
type,
value: ''
}
}
}

View File

@ -4,13 +4,25 @@
* type: string, * type: string,
* expanded: boolean?, * expanded: boolean?,
* menu: boolean?, * menu: boolean?,
* prop: string?, * props: Array.<{name: string, value: Data}>?
* value: *?, * }} ObjectData
* childs: Model[]? *
* }} Model * @typedef {{
*/ * type: string,
* expanded: boolean?,
/** * menu: boolean?,
* items: Data[]?
* }} ArrayData
*
* @typedef {{
* type: string,
* expanded: boolean?,
* menu: boolean?,
* value: *?
* }} ValueData
*
* @typedef {ObjectData | ArrayData | ValueData} Data
*
* @typedef {{ * @typedef {{
* name: string? * name: string?
* expand: function? * expand: function?