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 mkdirp = require('mkdirp');
|
||||
var webpack = require('webpack');
|
||||
var WebpackDevServer = require('webpack-dev-server');
|
||||
|
||||
var NAME = 'jsoneditor';
|
||||
var NAME_MINIMALIST = 'jsoneditor-minimalist';
|
||||
|
@ -37,6 +38,7 @@ var loaders = [
|
|||
var compiler = webpack({
|
||||
entry: ENTRY,
|
||||
devtool: 'source-map',
|
||||
debug: true,
|
||||
output: {
|
||||
library: 'jsoneditor',
|
||||
libraryTarget: 'umd',
|
||||
|
@ -57,6 +59,7 @@ var compiler = webpack({
|
|||
var compilerMinimalist = webpack({
|
||||
entry: ENTRY,
|
||||
devtool: 'source-map',
|
||||
debug: true,
|
||||
output: {
|
||||
library: 'jsoneditor',
|
||||
libraryTarget: 'umd',
|
||||
|
@ -87,7 +90,7 @@ gulp.task('bundle', ['mkdir'], function (done) {
|
|||
|
||||
compiler.run(function (err, stats) {
|
||||
if (err) {
|
||||
gutil.log(err);
|
||||
throw new gutil.PluginError('webpack', err);
|
||||
}
|
||||
|
||||
gutil.log('bundled ' + NAME + '.js');
|
||||
|
@ -103,7 +106,7 @@ gulp.task('bundle-minimalist', ['mkdir'], function (done) {
|
|||
|
||||
compilerMinimalist.run(function (err, stats) {
|
||||
if (err) {
|
||||
gutil.log(err);
|
||||
throw new gutil.PluginError('webpack', err);
|
||||
}
|
||||
|
||||
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"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": "4.4.0",
|
||||
"ajv": "4.5.0",
|
||||
"brace": "0.8.0",
|
||||
"javascript-natural-sort": "0.7.1",
|
||||
"preact": "5.6.0"
|
||||
"preact": "5.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ava": "0.16.0",
|
||||
"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",
|
||||
"gulp": "3.9.1",
|
||||
"gulp-shell": "0.5.2",
|
||||
|
@ -42,7 +44,7 @@
|
|||
"mkdirp": "0.5.1",
|
||||
"style-loader": "0.13.1",
|
||||
"svg-url-loader": "1.1.0",
|
||||
"webpack": "1.13.1"
|
||||
"webpack": "1.13.2"
|
||||
},
|
||||
"ava": {
|
||||
"require": [
|
||||
|
|
417
src/TreeMode.js
417
src/TreeMode.js
|
@ -1,11 +1,13 @@
|
|||
import { h, Component } from 'preact'
|
||||
|
||||
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'
|
||||
import bindMethods from './utils/bindMethods'
|
||||
import { setIn } from './utils/immutabilityHelpers'
|
||||
import {
|
||||
changeValue, changeProperty, changeType,
|
||||
insert, append, duplicate, remove,
|
||||
sort,
|
||||
expand,
|
||||
jsonToData, dataToJson
|
||||
} from './jsonData'
|
||||
import JSONNode from './JSONNode'
|
||||
|
||||
export default class TreeMode extends Component {
|
||||
|
@ -14,11 +16,9 @@ export default class TreeMode extends Component {
|
|||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
bindMethods(this)
|
||||
|
||||
const name = this.props.options && this.props.options.name || null
|
||||
const expand = this.props.options && this.props.options.expand || TreeMode.expand
|
||||
|
||||
|
||||
this.state = {
|
||||
options: {
|
||||
name
|
||||
|
@ -43,13 +43,13 @@ export default class TreeMode extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
render (props, state) {
|
||||
return h('div', {class: 'jsoneditor', contentEditable: 'false', onClick: JSONNode.hideContextMenu}, [
|
||||
h('ul', {class: 'jsoneditor-list', contentEditable: 'false'}, [
|
||||
h(JSONNode, {
|
||||
data: this.state.data,
|
||||
events: this.state.events,
|
||||
options: this.state.options,
|
||||
data: state.data,
|
||||
events: state.events,
|
||||
options: state.options,
|
||||
parent: null,
|
||||
prop: null
|
||||
})
|
||||
|
@ -57,224 +57,57 @@ export default class TreeMode extends Component {
|
|||
])
|
||||
}
|
||||
|
||||
handleChangeValue (path, value) {
|
||||
console.log('handleChangeValue', path, value)
|
||||
|
||||
const dataPath = toDataPath(this.state.data, path)
|
||||
|
||||
handleChangeValue = (path, value) => {
|
||||
this.setState({
|
||||
data: setIn(this.state.data, dataPath.concat(['value']), value)
|
||||
data: changeValue(this.state.data, path, value)
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
handleChangeProperty = (path, oldProp, newProp) => {
|
||||
this.setState({
|
||||
data: setIn(this.state.data, dataPath.concat(['props', index, 'name']), newProp)
|
||||
data: changeProperty(this.state.data, path, oldProp, newProp)
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
handleChangeType = (path, type) => {
|
||||
this.setState({
|
||||
data: setIn(this.state.data, dataPath, newEntry)
|
||||
data: changeType(this.state.data, path, 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({
|
||||
data: updateIn(this.state.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'
|
||||
this.setState({
|
||||
data: updateIn(this.state.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
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleAppend (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({
|
||||
data: updateIn(this.state.data, dataPath.concat(['items']), (items) => {
|
||||
const updatedItems = items.slice(0)
|
||||
|
||||
updatedItems.push(createDataEntry(type))
|
||||
|
||||
return updatedItems
|
||||
})
|
||||
})
|
||||
}
|
||||
else { // object.type === 'object'
|
||||
this.setState({
|
||||
data: updateIn(this.state.data, dataPath.concat(['props']), (props) => {
|
||||
const updatedProps = props.slice(0)
|
||||
|
||||
updatedProps.push({
|
||||
name: '',
|
||||
value: createDataEntry(type)
|
||||
})
|
||||
|
||||
return updatedProps
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleDuplicate (path, prop) {
|
||||
console.log('handleDuplicate', path)
|
||||
|
||||
const dataPath = toDataPath(this.state.data, path)
|
||||
const object = getIn(this.state.data, dataPath)
|
||||
|
||||
if (object.type === 'array') {
|
||||
this.setState({
|
||||
data: updateIn(this.state.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'
|
||||
this.setState({
|
||||
data: updateIn(this.state.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
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
handleInsert = (path, afterProp, type) => {
|
||||
this.setState({
|
||||
data: setIn(this.state.data, dataPath.concat(['expanded']), expand)
|
||||
data: insert(this.state.data, path, afterProp, type)
|
||||
})
|
||||
}
|
||||
|
||||
handleAppend = (path, type) => {
|
||||
this.setState({
|
||||
data: append(this.state.data, path, type)
|
||||
})
|
||||
}
|
||||
|
||||
handleDuplicate = (path, type) => {
|
||||
this.setState({
|
||||
data: duplicate(this.state.data, path, type)
|
||||
})
|
||||
}
|
||||
|
||||
handleRemove = (path, prop) => {
|
||||
this.setState({
|
||||
data: remove(this.state.data, path, prop)
|
||||
})
|
||||
}
|
||||
|
||||
handleSort = (path, order = null) => {
|
||||
this.setState({
|
||||
data: sort(this.state.data, path, order)
|
||||
})
|
||||
}
|
||||
|
||||
handleExpand = (path, doExpand) => {
|
||||
this.setState({
|
||||
data: expand(this.state.data, path, doExpand)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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 {{
|
||||
* type: string,
|
||||
* type: 'array',
|
||||
* expanded: boolean?,
|
||||
* menu: boolean?,
|
||||
* props: Array.<{name: string, value: Data}>?
|
||||
* props: Array.<{name: string, value: JSONData}>?
|
||||
* }} ObjectData
|
||||
*
|
||||
* @typedef {{
|
||||
* type: string,
|
||||
* type: 'object',
|
||||
* expanded: boolean?,
|
||||
* menu: boolean?,
|
||||
* items: Data[]?
|
||||
* items: JSONData[]?
|
||||
* }} ArrayData
|
||||
*
|
||||
* @typedef {{
|
||||
* type: string,
|
||||
* type: 'value' | 'string',
|
||||
* expanded: boolean?,
|
||||
* menu: boolean?,
|
||||
* value: *?
|
||||
* }} ValueData
|
||||
*
|
||||
* @typedef {ObjectData | ArrayData | ValueData} Data
|
||||
* @typedef {Array.<string | number>} Path
|
||||
*
|
||||
* @typedef {ObjectData | ArrayData | ValueData} JSONData
|
||||
*
|
||||
* @typedef {'object' | 'array' | 'value' | 'string'} JSONDataType
|
||||
*
|
||||
* @typedef {{
|
||||
*
|
||||
|
@ -29,6 +33,6 @@
|
|||
*
|
||||
* @typedef {{
|
||||
* name: string?,
|
||||
* expand: function (path: Array.<string | number>)?
|
||||
* expand: function (path: Path)?
|
||||
* }} SetOptions
|
||||
*/
|
Loading…
Reference in New Issue