Moved the logic for manipulating JSONData object into a separate file
This commit is contained in:
parent
c8a5614511
commit
069d35ace4
2
.babelrc
2
.babelrc
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
"presets": ["es2015"]
|
"presets": ["es2015", "stage-3", "stage-2"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ var gutil = require('gulp-util');
|
||||||
var shell = require('gulp-shell');
|
var shell = require('gulp-shell');
|
||||||
var mkdirp = require('mkdirp');
|
var mkdirp = require('mkdirp');
|
||||||
var webpack = require('webpack');
|
var webpack = require('webpack');
|
||||||
|
var WebpackDevServer = require('webpack-dev-server');
|
||||||
|
|
||||||
var NAME = 'jsoneditor';
|
var NAME = 'jsoneditor';
|
||||||
var NAME_MINIMALIST = 'jsoneditor-minimalist';
|
var NAME_MINIMALIST = 'jsoneditor-minimalist';
|
||||||
|
@ -37,6 +38,7 @@ var loaders = [
|
||||||
var compiler = webpack({
|
var compiler = webpack({
|
||||||
entry: ENTRY,
|
entry: ENTRY,
|
||||||
devtool: 'source-map',
|
devtool: 'source-map',
|
||||||
|
debug: true,
|
||||||
output: {
|
output: {
|
||||||
library: 'jsoneditor',
|
library: 'jsoneditor',
|
||||||
libraryTarget: 'umd',
|
libraryTarget: 'umd',
|
||||||
|
@ -57,6 +59,7 @@ var compiler = webpack({
|
||||||
var compilerMinimalist = webpack({
|
var compilerMinimalist = webpack({
|
||||||
entry: ENTRY,
|
entry: ENTRY,
|
||||||
devtool: 'source-map',
|
devtool: 'source-map',
|
||||||
|
debug: true,
|
||||||
output: {
|
output: {
|
||||||
library: 'jsoneditor',
|
library: 'jsoneditor',
|
||||||
libraryTarget: 'umd',
|
libraryTarget: 'umd',
|
||||||
|
@ -87,7 +90,7 @@ gulp.task('bundle', ['mkdir'], function (done) {
|
||||||
|
|
||||||
compiler.run(function (err, stats) {
|
compiler.run(function (err, stats) {
|
||||||
if (err) {
|
if (err) {
|
||||||
gutil.log(err);
|
throw new gutil.PluginError('webpack', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
gutil.log('bundled ' + NAME + '.js');
|
gutil.log('bundled ' + NAME + '.js');
|
||||||
|
@ -103,7 +106,7 @@ gulp.task('bundle-minimalist', ['mkdir'], function (done) {
|
||||||
|
|
||||||
compilerMinimalist.run(function (err, stats) {
|
compilerMinimalist.run(function (err, stats) {
|
||||||
if (err) {
|
if (err) {
|
||||||
gutil.log(err);
|
throw new gutil.PluginError('webpack', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
gutil.log('bundled ' + NAME_MINIMALIST + '.js');
|
gutil.log('bundled ' + NAME_MINIMALIST + '.js');
|
||||||
|
|
10
package.json
10
package.json
|
@ -23,15 +23,17 @@
|
||||||
"test": "ava test/*.test.js test/**/*.test.js --verbose"
|
"test": "ava test/*.test.js test/**/*.test.js --verbose"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "4.4.0",
|
"ajv": "4.5.0",
|
||||||
"brace": "0.8.0",
|
"brace": "0.8.0",
|
||||||
"javascript-natural-sort": "0.7.1",
|
"javascript-natural-sort": "0.7.1",
|
||||||
"preact": "5.6.0"
|
"preact": "5.7.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"ava": "0.16.0",
|
"ava": "0.16.0",
|
||||||
"babel-core": "6.13.2",
|
"babel-core": "6.13.2",
|
||||||
"babel-loader": "6.2.4",
|
"babel-loader": "6.2.5",
|
||||||
|
"babel-preset-stage-2": "6.13.0",
|
||||||
|
"babel-preset-stage-3": "6.11.0",
|
||||||
"css-loader": "0.23.1",
|
"css-loader": "0.23.1",
|
||||||
"gulp": "3.9.1",
|
"gulp": "3.9.1",
|
||||||
"gulp-shell": "0.5.2",
|
"gulp-shell": "0.5.2",
|
||||||
|
@ -42,7 +44,7 @@
|
||||||
"mkdirp": "0.5.1",
|
"mkdirp": "0.5.1",
|
||||||
"style-loader": "0.13.1",
|
"style-loader": "0.13.1",
|
||||||
"svg-url-loader": "1.1.0",
|
"svg-url-loader": "1.1.0",
|
||||||
"webpack": "1.13.1"
|
"webpack": "1.13.2"
|
||||||
},
|
},
|
||||||
"ava": {
|
"ava": {
|
||||||
"require": [
|
"require": [
|
||||||
|
|
381
src/TreeMode.js
381
src/TreeMode.js
|
@ -1,11 +1,13 @@
|
||||||
import { h, Component } from 'preact'
|
import { h, Component } from 'preact'
|
||||||
|
|
||||||
import { cloneDeep } from './utils/objectUtils'
|
import { setIn } from './utils/immutabilityHelpers'
|
||||||
import { setIn, updateIn, getIn, deleteIn } from './utils/immutabilityHelpers'
|
import {
|
||||||
import { compareAsc, compareDesc } from './utils/arrayUtils'
|
changeValue, changeProperty, changeType,
|
||||||
import { stringConvert } from './utils/typeUtils'
|
insert, append, duplicate, remove,
|
||||||
import { isObject } from './utils/objectUtils'
|
sort,
|
||||||
import bindMethods from './utils/bindMethods'
|
expand,
|
||||||
|
jsonToData, dataToJson
|
||||||
|
} from './jsonData'
|
||||||
import JSONNode from './JSONNode'
|
import JSONNode from './JSONNode'
|
||||||
|
|
||||||
export default class TreeMode extends Component {
|
export default class TreeMode extends Component {
|
||||||
|
@ -14,8 +16,6 @@ export default class TreeMode extends Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
bindMethods(this)
|
|
||||||
|
|
||||||
const name = this.props.options && this.props.options.name || null
|
const name = this.props.options && this.props.options.name || null
|
||||||
const expand = this.props.options && this.props.options.expand || TreeMode.expand
|
const expand = this.props.options && this.props.options.expand || TreeMode.expand
|
||||||
|
|
||||||
|
@ -43,13 +43,13 @@ export default class TreeMode extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render (props, state) {
|
||||||
return h('div', {class: 'jsoneditor', contentEditable: 'false', onClick: JSONNode.hideContextMenu}, [
|
return h('div', {class: 'jsoneditor', contentEditable: 'false', onClick: JSONNode.hideContextMenu}, [
|
||||||
h('ul', {class: 'jsoneditor-list', contentEditable: 'false'}, [
|
h('ul', {class: 'jsoneditor-list', contentEditable: 'false'}, [
|
||||||
h(JSONNode, {
|
h(JSONNode, {
|
||||||
data: this.state.data,
|
data: state.data,
|
||||||
events: this.state.events,
|
events: state.events,
|
||||||
options: this.state.options,
|
options: state.options,
|
||||||
parent: null,
|
parent: null,
|
||||||
prop: null
|
prop: null
|
||||||
})
|
})
|
||||||
|
@ -57,224 +57,57 @@ export default class TreeMode extends Component {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChangeValue (path, value) {
|
handleChangeValue = (path, value) => {
|
||||||
console.log('handleChangeValue', path, value)
|
|
||||||
|
|
||||||
const dataPath = toDataPath(this.state.data, path)
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
data: setIn(this.state.data, dataPath.concat(['value']), value)
|
data: changeValue(this.state.data, path, value)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChangeProperty (path, oldProp, newProp) {
|
handleChangeProperty = (path, oldProp, newProp) => {
|
||||||
console.log('handleChangeProperty', path, oldProp, newProp)
|
|
||||||
|
|
||||||
const dataPath = toDataPath(this.state.data, path)
|
|
||||||
const object = getIn(this.state.data, dataPath)
|
|
||||||
const index = object.props.findIndex(p => p.name === oldProp)
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
data: setIn(this.state.data, dataPath.concat(['props', index, 'name']), newProp)
|
data: changeProperty(this.state.data, path, oldProp, newProp)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChangeType (path, type) {
|
handleChangeType = (path, type) => {
|
||||||
console.log('handleChangeType', path, type)
|
|
||||||
|
|
||||||
const dataPath = toDataPath(this.state.data, path)
|
|
||||||
const oldEntry = getIn(this.state.data, dataPath)
|
|
||||||
const newEntry = convertDataEntry(oldEntry, type)
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
data: setIn(this.state.data, dataPath, newEntry)
|
data: changeType(this.state.data, path, type)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInsert (path, afterProp, type) {
|
handleInsert = (path, afterProp, type) => {
|
||||||
console.log('handleInsert', path, afterProp, type)
|
|
||||||
|
|
||||||
const dataPath = toDataPath(this.state.data, path)
|
|
||||||
const parent = getIn(this.state.data, dataPath)
|
|
||||||
|
|
||||||
if (parent.type === 'array') {
|
|
||||||
this.setState({
|
this.setState({
|
||||||
data: updateIn(this.state.data, dataPath.concat(['items']), (items) => {
|
data: insert(this.state.data, path, afterProp, type)
|
||||||
const index = parseInt(afterProp)
|
|
||||||
const updatedItems = items.slice(0)
|
|
||||||
|
|
||||||
updatedItems.splice(index + 1, 0, createDataEntry(type))
|
|
||||||
|
|
||||||
return updatedItems
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
else { // parent.type === 'object'
|
|
||||||
|
handleAppend = (path, type) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
data: updateIn(this.state.data, dataPath.concat(['props']), (props) => {
|
data: append(this.state.data, path, type)
|
||||||
const index = props.findIndex(p => p.name === afterProp)
|
|
||||||
const updatedProps = props.slice(0)
|
|
||||||
|
|
||||||
updatedProps.splice(index + 1, 0, {
|
|
||||||
name: '',
|
|
||||||
value: createDataEntry(type)
|
|
||||||
})
|
|
||||||
|
|
||||||
return updatedProps
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
handleAppend (path, type) {
|
handleDuplicate = (path, type) => {
|
||||||
console.log('handleAppend', path, type)
|
|
||||||
|
|
||||||
const dataPath = toDataPath(this.state.data, path)
|
|
||||||
const object = getIn(this.state.data, dataPath)
|
|
||||||
|
|
||||||
if (object.type === 'array') {
|
|
||||||
this.setState({
|
this.setState({
|
||||||
data: updateIn(this.state.data, dataPath.concat(['items']), (items) => {
|
data: duplicate(this.state.data, path, type)
|
||||||
const updatedItems = items.slice(0)
|
|
||||||
|
|
||||||
updatedItems.push(createDataEntry(type))
|
|
||||||
|
|
||||||
return updatedItems
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
else { // object.type === 'object'
|
|
||||||
|
handleRemove = (path, prop) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
data: updateIn(this.state.data, dataPath.concat(['props']), (props) => {
|
data: remove(this.state.data, path, prop)
|
||||||
const updatedProps = props.slice(0)
|
|
||||||
|
|
||||||
updatedProps.push({
|
|
||||||
name: '',
|
|
||||||
value: createDataEntry(type)
|
|
||||||
})
|
|
||||||
|
|
||||||
return updatedProps
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
handleDuplicate (path, prop) {
|
handleSort = (path, order = null) => {
|
||||||
console.log('handleDuplicate', path)
|
|
||||||
|
|
||||||
const dataPath = toDataPath(this.state.data, path)
|
|
||||||
const object = getIn(this.state.data, dataPath)
|
|
||||||
|
|
||||||
if (object.type === 'array') {
|
|
||||||
this.setState({
|
this.setState({
|
||||||
data: updateIn(this.state.data, dataPath.concat(['items']), (items) => {
|
data: sort(this.state.data, path, order)
|
||||||
const index = parseInt(prop)
|
|
||||||
const updatedItems = items.slice(0)
|
|
||||||
const original = items[index]
|
|
||||||
const duplicate = cloneDeep(original)
|
|
||||||
|
|
||||||
updatedItems.splice(index + 1, 0, duplicate)
|
|
||||||
|
|
||||||
return updatedItems
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
else { // object.type === 'object'
|
|
||||||
|
handleExpand = (path, doExpand) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
data: updateIn(this.state.data, dataPath.concat(['props']), (props) => {
|
data: expand(this.state.data, path, doExpand)
|
||||||
const index = props.findIndex(p => p.name === prop)
|
|
||||||
const updated = props.slice(0)
|
|
||||||
const original = props[index]
|
|
||||||
const duplicate = cloneDeep(original)
|
|
||||||
|
|
||||||
updated.splice(index + 1, 0, duplicate)
|
|
||||||
|
|
||||||
return updated
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleRemove (path, prop) {
|
|
||||||
console.log('handleRemove', path)
|
|
||||||
|
|
||||||
const object = getIn(this.state.data, toDataPath(this.state.data, path))
|
|
||||||
|
|
||||||
if (object.type === 'array') {
|
|
||||||
const dataPath = toDataPath(this.state.data, path.concat(prop))
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
data: deleteIn(this.state.data, dataPath)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else { // object.type === 'object'
|
|
||||||
const dataPath = toDataPath(this.state.data, path.concat(prop))
|
|
||||||
|
|
||||||
dataPath.pop() // remove the 'value' property, we want to remove the whole object property
|
|
||||||
this.setState({
|
|
||||||
data: deleteIn(this.state.data, dataPath)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
handleSort (path, order = null) {
|
|
||||||
console.log('handleSort', path, order)
|
|
||||||
|
|
||||||
const dataPath = toDataPath(this.state.data, path)
|
|
||||||
const object = getIn(this.state.data, dataPath)
|
|
||||||
|
|
||||||
let _order
|
|
||||||
if (order === 'asc' || order === 'desc') {
|
|
||||||
_order = order
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// toggle previous order
|
|
||||||
_order = object.order !== 'asc' ? 'asc' : 'desc'
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
data: setIn(this.state.data, dataPath.concat(['order']), _order)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (object.type === 'array') {
|
|
||||||
this.setState({
|
|
||||||
data: updateIn(this.state.data, dataPath.concat(['items']), (items) =>{
|
|
||||||
const ordered = items.slice(0)
|
|
||||||
const compare = _order === 'desc' ? compareDesc : compareAsc
|
|
||||||
|
|
||||||
ordered.sort((a, b) => compare(a.value, b.value))
|
|
||||||
|
|
||||||
return ordered
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else { // object.type === 'object'
|
|
||||||
this.setState({
|
|
||||||
data: updateIn(this.state.data, dataPath.concat(['props']), (props) => {
|
|
||||||
const orderedProps = props.slice(0)
|
|
||||||
const compare = _order === 'desc' ? compareDesc : compareAsc
|
|
||||||
|
|
||||||
orderedProps.sort((a, b) => compare(a.name, b.name))
|
|
||||||
|
|
||||||
return orderedProps
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleExpand(path, expand) {
|
|
||||||
console.log('handleExpand', path, expand)
|
|
||||||
|
|
||||||
const dataPath = toDataPath(this.state.data, path)
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
data: setIn(this.state.data, dataPath.concat(['expanded']), expand)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -315,151 +148,3 @@ export default class TreeMode extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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: 'value',
|
|
||||||
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) {
|
|
||||||
switch (data.type) {
|
|
||||||
case 'array':
|
|
||||||
return data.items.map(dataToJson)
|
|
||||||
|
|
||||||
case 'object':
|
|
||||||
const object = {}
|
|
||||||
|
|
||||||
data.props.forEach(prop => {
|
|
||||||
object[prop.name] = dataToJson(prop.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
return object
|
|
||||||
|
|
||||||
default: // type 'string' or 'value'
|
|
||||||
return data.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new data entry
|
|
||||||
* @param {'object' | 'array' | 'value' | 'string'} [type='value']
|
|
||||||
* @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: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert an entry into a different type. When possible, data is retained
|
|
||||||
* @param {Data} entry
|
|
||||||
* @param {'object' | 'array' | 'value' | 'string'} type
|
|
||||||
*/
|
|
||||||
function convertDataEntry (entry, type) {
|
|
||||||
const convertedEntry = createDataEntry(type)
|
|
||||||
|
|
||||||
// convert contents from old value to new value where possible
|
|
||||||
if (type === 'value' && entry.type === 'string') {
|
|
||||||
convertedEntry.value = stringConvert(entry.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'string' && entry.type === 'value') {
|
|
||||||
convertedEntry.value = entry.value + ''
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'object' && entry.type === 'array') {
|
|
||||||
convertedEntry.props = entry.items.map((item, index) => {
|
|
||||||
return {
|
|
||||||
name: index + '',
|
|
||||||
value: item
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'array' && entry.type === 'object') {
|
|
||||||
convertedEntry.items = entry.props.map(prop => prop.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return convertedEntry
|
|
||||||
}
|
|
|
@ -0,0 +1,410 @@
|
||||||
|
/**
|
||||||
|
* This file contains functions to act on a JSONData object.
|
||||||
|
* All functions are pure and don't mutate the JSONData.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { cloneDeep } from './utils/objectUtils'
|
||||||
|
import { setIn, updateIn, getIn, deleteIn } from './utils/immutabilityHelpers'
|
||||||
|
import { compareAsc, compareDesc } from './utils/arrayUtils'
|
||||||
|
import { stringConvert } from './utils/typeUtils'
|
||||||
|
import { isObject } from './utils/objectUtils'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the value of a property or item
|
||||||
|
* @param {JSONData} data
|
||||||
|
* @param {Path} path
|
||||||
|
* @param {*} value
|
||||||
|
* @return {JSONData}
|
||||||
|
*/
|
||||||
|
export function changeValue (data, path, value) {
|
||||||
|
console.log('changeValue', data, value)
|
||||||
|
|
||||||
|
const dataPath = toDataPath(data, path)
|
||||||
|
|
||||||
|
return setIn(data, dataPath.concat(['value']), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change a property name
|
||||||
|
* @param {JSONData} data
|
||||||
|
* @param {Path} path
|
||||||
|
* @param {string} oldProp
|
||||||
|
* @param {string} newProp
|
||||||
|
* @return {JSONData}
|
||||||
|
*/
|
||||||
|
export function changeProperty (data, path, oldProp, newProp) {
|
||||||
|
console.log('changeProperty', path, oldProp, newProp)
|
||||||
|
|
||||||
|
const dataPath = toDataPath(data, path)
|
||||||
|
const object = getIn(data, dataPath)
|
||||||
|
const index = object.props.findIndex(p => p.name === oldProp)
|
||||||
|
|
||||||
|
return setIn(data, dataPath.concat(['props', index, 'name']), newProp)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the type of a property or item
|
||||||
|
* @param {JSONData} data
|
||||||
|
* @param {Path} path
|
||||||
|
* @param {JSONDataType} type
|
||||||
|
* @return {JSONData}
|
||||||
|
*/
|
||||||
|
export function changeType (data, path, type) {
|
||||||
|
console.log('changeType', path, type)
|
||||||
|
|
||||||
|
const dataPath = toDataPath(data, path)
|
||||||
|
const oldEntry = getIn(data, dataPath)
|
||||||
|
const newEntry = convertDataEntry(oldEntry, type)
|
||||||
|
|
||||||
|
return setIn(data, dataPath, newEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert a new item after specified property or item
|
||||||
|
* @param {JSONData} data
|
||||||
|
* @param {Path} path
|
||||||
|
* @param {string | number} afterProp
|
||||||
|
* @param {JSONDataType} type
|
||||||
|
* @return {JSONData}
|
||||||
|
*/
|
||||||
|
export function insert (data, path, afterProp, type) {
|
||||||
|
console.log('insert', path, afterProp, type)
|
||||||
|
|
||||||
|
const dataPath = toDataPath(data, path)
|
||||||
|
const parent = getIn(data, dataPath)
|
||||||
|
|
||||||
|
if (parent.type === 'array') {
|
||||||
|
return updateIn(data, dataPath.concat(['items']), (items) => {
|
||||||
|
const index = parseInt(afterProp)
|
||||||
|
const updatedItems = items.slice(0)
|
||||||
|
|
||||||
|
updatedItems.splice(index + 1, 0, createDataEntry(type))
|
||||||
|
|
||||||
|
return updatedItems
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else { // parent.type === 'object'
|
||||||
|
return updateIn(data, dataPath.concat(['props']), (props) => {
|
||||||
|
const index = props.findIndex(p => p.name === afterProp)
|
||||||
|
const updatedProps = props.slice(0)
|
||||||
|
|
||||||
|
updatedProps.splice(index + 1, 0, {
|
||||||
|
name: '',
|
||||||
|
value: createDataEntry(type)
|
||||||
|
})
|
||||||
|
|
||||||
|
return updatedProps
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append a new item at the end of an object or array
|
||||||
|
* @param {JSONData} data
|
||||||
|
* @param {Path} path
|
||||||
|
* @param {JSONDataType} type
|
||||||
|
* @return {JSONData}
|
||||||
|
*/
|
||||||
|
export function append (data, path, type) {
|
||||||
|
console.log('append', path, type)
|
||||||
|
|
||||||
|
const dataPath = toDataPath(data, path)
|
||||||
|
const object = getIn(data, dataPath)
|
||||||
|
|
||||||
|
if (object.type === 'array') {
|
||||||
|
return updateIn(data, dataPath.concat(['items']), (items) => {
|
||||||
|
const updatedItems = items.slice(0)
|
||||||
|
|
||||||
|
updatedItems.push(createDataEntry(type))
|
||||||
|
|
||||||
|
return updatedItems
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else { // object.type === 'object'
|
||||||
|
return updateIn(data, dataPath.concat(['props']), (props) => {
|
||||||
|
const updatedProps = props.slice(0)
|
||||||
|
|
||||||
|
updatedProps.push({
|
||||||
|
name: '',
|
||||||
|
value: createDataEntry(type)
|
||||||
|
})
|
||||||
|
|
||||||
|
return updatedProps
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicate a property or item
|
||||||
|
* @param {JSONData} data
|
||||||
|
* @param {Path} path
|
||||||
|
* @param {string | number} prop
|
||||||
|
* @return {JSONData}
|
||||||
|
*/
|
||||||
|
export function duplicate (data, path, prop) {
|
||||||
|
console.log('duplicate', path)
|
||||||
|
|
||||||
|
const dataPath = toDataPath(data, path)
|
||||||
|
const object = getIn(data, dataPath)
|
||||||
|
|
||||||
|
if (object.type === 'array') {
|
||||||
|
return updateIn(data, dataPath.concat(['items']), (items) => {
|
||||||
|
const index = parseInt(prop)
|
||||||
|
const updatedItems = items.slice(0)
|
||||||
|
const original = items[index]
|
||||||
|
const duplicate = cloneDeep(original)
|
||||||
|
|
||||||
|
updatedItems.splice(index + 1, 0, duplicate)
|
||||||
|
|
||||||
|
return updatedItems
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else { // object.type === 'object'
|
||||||
|
return updateIn(data, dataPath.concat(['props']), (props) => {
|
||||||
|
const index = props.findIndex(p => p.name === prop)
|
||||||
|
const updated = props.slice(0)
|
||||||
|
const original = props[index]
|
||||||
|
const duplicate = cloneDeep(original)
|
||||||
|
|
||||||
|
updated.splice(index + 1, 0, duplicate)
|
||||||
|
|
||||||
|
return updated
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an item or property
|
||||||
|
* @param {JSONData} data
|
||||||
|
* @param {Path} path
|
||||||
|
* @param {string | number} prop
|
||||||
|
* @return {JSONData}
|
||||||
|
*/
|
||||||
|
export function remove (data, path, prop) {
|
||||||
|
console.log('remove', path)
|
||||||
|
|
||||||
|
const object = getIn(data, toDataPath(data, path))
|
||||||
|
|
||||||
|
if (object.type === 'array') {
|
||||||
|
const dataPath = toDataPath(data, path.concat(prop))
|
||||||
|
|
||||||
|
return deleteIn(data, dataPath)
|
||||||
|
}
|
||||||
|
else { // object.type === 'object'
|
||||||
|
const dataPath = toDataPath(data, path.concat(prop))
|
||||||
|
|
||||||
|
dataPath.pop() // remove the 'value' property, we want to remove the whole object property
|
||||||
|
return deleteIn(data, dataPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Order the items of an array or the properties of an object in ascending
|
||||||
|
* or descending order
|
||||||
|
* @param {JSONData} data
|
||||||
|
* @param {Path} path
|
||||||
|
* @param {'asc' | 'desc' | null} [order=null] If not provided, will toggle current ordering
|
||||||
|
* @return {JSONData}
|
||||||
|
*/
|
||||||
|
export function sort (data, path, order = null) {
|
||||||
|
console.log('sort', path, order)
|
||||||
|
|
||||||
|
const dataPath = toDataPath(data, path)
|
||||||
|
const object = getIn(data, dataPath)
|
||||||
|
|
||||||
|
let _order
|
||||||
|
if (order === 'asc' || order === 'desc') {
|
||||||
|
_order = order
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// toggle previous order
|
||||||
|
_order = object.order !== 'asc' ? 'asc' : 'desc'
|
||||||
|
|
||||||
|
data = setIn(data, dataPath.concat(['order']), _order)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (object.type === 'array') {
|
||||||
|
return updateIn(data, dataPath.concat(['items']), (items) =>{
|
||||||
|
const ordered = items.slice(0)
|
||||||
|
const compare = _order === 'desc' ? compareDesc : compareAsc
|
||||||
|
|
||||||
|
ordered.sort((a, b) => compare(a.value, b.value))
|
||||||
|
|
||||||
|
return ordered
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else { // object.type === 'object'
|
||||||
|
return updateIn(data, dataPath.concat(['props']), (props) => {
|
||||||
|
const orderedProps = props.slice(0)
|
||||||
|
const compare = _order === 'desc' ? compareDesc : compareAsc
|
||||||
|
|
||||||
|
orderedProps.sort((a, b) => compare(a.name, b.name))
|
||||||
|
|
||||||
|
return orderedProps
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand or collapse an item or property
|
||||||
|
* @param {JSONData} data
|
||||||
|
* @param {Path} path
|
||||||
|
* @param {boolean} expand
|
||||||
|
* @return {JSONData}
|
||||||
|
*/
|
||||||
|
export function expand (data, path, expand) {
|
||||||
|
console.log('expand', path, expand)
|
||||||
|
|
||||||
|
const dataPath = toDataPath(data, path)
|
||||||
|
|
||||||
|
return setIn(data, dataPath.concat(['expanded']), expand)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a path of a JSON object into a path in the corresponding data model
|
||||||
|
* @param {JSONData} data
|
||||||
|
* @param {Path} path
|
||||||
|
* @return {Path} dataPath
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export 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 {Path} path
|
||||||
|
* @param {Object | Array | string | number | boolean | null} json
|
||||||
|
* @param {function(path: Path)} expand
|
||||||
|
* @return {JSONData}
|
||||||
|
*/
|
||||||
|
export 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: 'value',
|
||||||
|
value: json
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the internal data model to a regular JSON object
|
||||||
|
* @param {JSONData} data
|
||||||
|
* @return {Object | Array | string | number | boolean | null} json
|
||||||
|
*/
|
||||||
|
export function dataToJson (data) {
|
||||||
|
switch (data.type) {
|
||||||
|
case 'array':
|
||||||
|
return data.items.map(dataToJson)
|
||||||
|
|
||||||
|
case 'object':
|
||||||
|
const object = {}
|
||||||
|
|
||||||
|
data.props.forEach(prop => {
|
||||||
|
object[prop.name] = dataToJson(prop.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
return object
|
||||||
|
|
||||||
|
default: // type 'string' or 'value'
|
||||||
|
return data.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new data entry
|
||||||
|
* @param {JSONDataType} [type='value']
|
||||||
|
* @return {JSONData}
|
||||||
|
*/
|
||||||
|
export function createDataEntry (type) {
|
||||||
|
if (type === 'array') {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
expanded: true,
|
||||||
|
items: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (type === 'object') {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
expanded: true,
|
||||||
|
props: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
value: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an entry into a different type. When possible, data is retained
|
||||||
|
* @param {JSONData} entry
|
||||||
|
* @param {JSONDataType} type
|
||||||
|
* @return {JSONData}
|
||||||
|
*/
|
||||||
|
export function convertDataEntry (entry, type) {
|
||||||
|
const convertedEntry = createDataEntry(type)
|
||||||
|
|
||||||
|
// convert contents from old value to new value where possible
|
||||||
|
if (type === 'value' && entry.type === 'string') {
|
||||||
|
convertedEntry.value = stringConvert(entry.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'string' && entry.type === 'value') {
|
||||||
|
convertedEntry.value = entry.value + ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'object' && entry.type === 'array') {
|
||||||
|
convertedEntry.props = entry.items.map((item, index) => {
|
||||||
|
return {
|
||||||
|
name: index + '',
|
||||||
|
value: item
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'array' && entry.type === 'object') {
|
||||||
|
convertedEntry.items = entry.props.map(prop => prop.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return convertedEntry
|
||||||
|
}
|
|
@ -1,27 +1,31 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
* type: string,
|
* type: 'array',
|
||||||
* expanded: boolean?,
|
* expanded: boolean?,
|
||||||
* menu: boolean?,
|
* menu: boolean?,
|
||||||
* props: Array.<{name: string, value: Data}>?
|
* props: Array.<{name: string, value: JSONData}>?
|
||||||
* }} ObjectData
|
* }} ObjectData
|
||||||
*
|
*
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
* type: string,
|
* type: 'object',
|
||||||
* expanded: boolean?,
|
* expanded: boolean?,
|
||||||
* menu: boolean?,
|
* menu: boolean?,
|
||||||
* items: Data[]?
|
* items: JSONData[]?
|
||||||
* }} ArrayData
|
* }} ArrayData
|
||||||
*
|
*
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
* type: string,
|
* type: 'value' | 'string',
|
||||||
* expanded: boolean?,
|
* expanded: boolean?,
|
||||||
* menu: boolean?,
|
* menu: boolean?,
|
||||||
* value: *?
|
* value: *?
|
||||||
* }} ValueData
|
* }} ValueData
|
||||||
*
|
*
|
||||||
* @typedef {ObjectData | ArrayData | ValueData} Data
|
* @typedef {Array.<string | number>} Path
|
||||||
|
*
|
||||||
|
* @typedef {ObjectData | ArrayData | ValueData} JSONData
|
||||||
|
*
|
||||||
|
* @typedef {'object' | 'array' | 'value' | 'string'} JSONDataType
|
||||||
*
|
*
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
*
|
*
|
||||||
|
@ -29,6 +33,6 @@
|
||||||
*
|
*
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
* name: string?,
|
* name: string?,
|
||||||
* expand: function (path: Array.<string | number>)?
|
* expand: function (path: Path)?
|
||||||
* }} SetOptions
|
* }} SetOptions
|
||||||
*/
|
*/
|
Loading…
Reference in New Issue