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}) {
const childCount = data.childs.length
const childCount = data.props.length
const contents = [
h('div', {class: 'jsoneditor-node jsoneditor-object'}, [
this.renderExpandButton(),
@ -59,23 +59,23 @@ export default class JSONNode extends Component {
]
if (data.expanded) {
const childs = data.childs.map(child => {
const props = data.props.map(prop => {
return h(JSONNode, {
path: path.concat(child.prop),
data: child,
path: path.concat(prop.name),
data: prop.value,
options,
events
})
})
contents.push(h('ul', {class: 'jsoneditor-list'}, childs))
contents.push(h('ul', {class: 'jsoneditor-list'}, props))
}
return h('li', {}, contents)
}
renderJSONArray ({path, data, options, events}) {
const childCount = data.childs.length
const childCount = data.items.length
const contents = [
h('div', {class: 'jsoneditor-node jsoneditor-array'}, [
this.renderExpandButton(),
@ -87,7 +87,7 @@ export default class JSONNode extends Component {
]
if (data.expanded) {
const childs = data.childs.map((child, index) => {
const items = data.items.map((child, index) => {
return h(JSONNode, {
path: path.concat(index),
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)
@ -259,31 +259,31 @@ export default class JSONNode extends Component {
title: 'Insert a new item with type \'value\' after this item (Ctrl+Ins)',
submenuTitle: 'Select the type of the item to be inserted',
className: 'jsoneditor-insert',
click: () => events.onInsert(path, '', ''),
click: () => events.onInsert(path, 'value'),
submenu: [
{
text: 'Value',
className: 'jsoneditor-type-value',
title: TYPE_TITLES.value,
click: () => events.onInsert(path, '', '')
click: () => events.onInsert(path, 'value')
},
{
text: 'Array',
className: 'jsoneditor-type-array',
title: TYPE_TITLES.array,
click: () => events.onInsert(path, '', [])
click: () => events.onInsert(path, 'array')
},
{
text: 'Object',
className: 'jsoneditor-type-object',
title: TYPE_TITLES.object,
click: () => events.onInsert(path, '', {})
click: () => events.onInsert(path, 'object')
},
{
text: 'String',
className: 'jsoneditor-type-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) {
const oldProp = this.props.data.prop
const oldProp = last(this.props.path)
const newProp = unescapeHTML(getInnerText(event.target))
// 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: {
type: 'object',
expanded: true,
childs: []
props: []
},
events: {
@ -73,8 +73,7 @@ export default class Main extends Component {
const index = this._findIndex(path, oldProp)
const newPath = path.concat(newProp)
this._setIn(path, ['childs', index, 'path'], newPath)
this._setIn(path, ['childs', index, 'prop'], newProp)
this._setIn(path, ['props', index, 'name'], newProp)
}
handleChangeType (path, type) {
@ -83,8 +82,8 @@ export default class Main extends Component {
this._setIn(path, ['type'], type)
}
handleInsert (path, prop, value, type) {
console.log('handleInsert', path, prop, value, type)
handleInsert (path, type) {
console.log('handleInsert', path, type)
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 parent = this._getIn(parentPath)
const index = parent.type === 'array'
? parseInt(afterProp)
: this._findIndex(parentPath, afterProp)
if (parent.type === 'array') {
this._updateIn(parentPath, ['items'], (items) => {
const index = parseInt(afterProp)
const updated = items.slice(0)
this._updateIn(parentPath, ['childs'], function (childs) {
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, createDataEntry(type))
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) {
@ -126,19 +129,30 @@ export default class Main extends Component {
const parentPath = path.slice(0, path.length - 1)
const parent = this._getIn(parentPath)
const index = parent.type === 'array'
? parseInt(prop)
: this._findIndex(parentPath, prop)
if (parent.type === 'array') {
this._updateIn(parentPath, ['items'], (items) => {
const index = parseInt(prop)
const updated = items.slice(0)
const original = items[index]
const duplicate = cloneDeep(original)
this._updateIn(parentPath, ['childs'], function (childs) {
const updated = childs.slice(0)
const original = childs[index]
const duplicate = cloneDeep(original)
updated.splice(index + 1, 0, duplicate)
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) {
@ -146,11 +160,29 @@ export default class Main extends Component {
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 {'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
const entry = this._getIn(path)
const object = this._getIn(path)
let _order
if (order === 'asc' || order === 'desc') {
@ -167,23 +199,30 @@ export default class Main extends Component {
}
else {
// toggle previous order
_order = entry.order !== 'asc' ? 'asc' : 'desc'
_order = object.order !== 'asc' ? 'asc' : 'desc'
this._setIn(path, ['order'], _order)
}
this._updateIn(path, ['childs'], function (childs) {
const ordered = childs.slice(0)
const compare = _order === 'desc' ? compareDesc : compareAsc
if (object.type === 'array') {
this._updateIn(path, ['items'], function (items) {
const ordered = items.slice(0)
const compare = _order === 'desc' ? compareDesc : compareAsc
if (entry.type === 'array') {
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) {
@ -223,50 +262,42 @@ export default class Main extends Component {
this.handleShowContextMenu({})
}
_getIn (path, modelProps = []) {
const modelPath = Main._pathToModelPath(this.state.data, path)
_getIn (path, dataProps = []) {
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) {
const modelPath = Main._pathToModelPath(this.state.data, path)
_setIn (path, dataProps = [], value) {
const dataPath = toDataPath(this.state.data, path)
this.setState({
data: setIn(this.state.data, modelPath.concat(modelProps), value)
data: setIn(this.state.data, dataPath.concat(dataProps), value)
})
}
_updateIn (path, modelProps = [], callback) {
const modelPath = Main._pathToModelPath(this.state.data, path)
_updateIn (path, dataProps = [], callback) {
const dataPath = toDataPath(this.state.data, path)
this.setState({
data: updateIn(this.state.data, modelPath.concat(modelProps), callback)
})
}
_deleteIn (path, modelProps = []) {
const modelPath = Main._pathToModelPath(this.state.data, path)
this.setState({
data: deleteIn(this.state.data, modelPath.concat(modelProps))
data: updateIn(this.state.data, dataPath.concat(dataProps), callback)
})
}
_findIndex(path, prop) {
const object = this._getIn(path)
return object.childs.findIndex(child => child.prop === prop)
return object.props.findIndex(p => p.name === prop)
}
// TODO: comment
get () {
return Main._modelToJson(this.state.data)
return dataToJson(this.state.data)
}
// TODO: comment
set (json) {
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
}
/**
* 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,
* expanded: boolean?,
* menu: boolean?,
* prop: string?,
* value: *?,
* childs: Model[]?
* }} Model
*/
/**
* props: Array.<{name: string, value: Data}>?
* }} ObjectData
*
* @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 {{
* name: string?
* expand: function?