Moved the logic for manipulating JSONData object into a separate file

This commit is contained in:
jos 2016-08-20 11:14:28 +02:00
parent c8a5614511
commit 069d35ace4
6 changed files with 484 additions and 380 deletions

View File

@ -1,3 +1,3 @@
{
"presets": ["es2015"]
"presets": ["es2015", "stage-3", "stage-2"]
}

View File

@ -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');

View File

@ -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": [

View File

@ -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,8 +16,6 @@ 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
@ -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') {
handleInsert = (path, afterProp, type) => {
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
})
data: insert(this.state.data, path, afterProp, type)
})
}
else { // parent.type === 'object'
handleAppend = (path, type) => {
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
})
data: append(this.state.data, path, type)
})
}
}
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') {
handleDuplicate = (path, type) => {
this.setState({
data: updateIn(this.state.data, dataPath.concat(['items']), (items) => {
const updatedItems = items.slice(0)
updatedItems.push(createDataEntry(type))
return updatedItems
})
data: duplicate(this.state.data, path, type)
})
}
else { // object.type === 'object'
handleRemove = (path, prop) => {
this.setState({
data: updateIn(this.state.data, dataPath.concat(['props']), (props) => {
const updatedProps = props.slice(0)
updatedProps.push({
name: '',
value: createDataEntry(type)
})
return updatedProps
})
data: remove(this.state.data, path, prop)
})
}
}
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') {
handleSort = (path, order = null) => {
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
})
data: sort(this.state.data, path, order)
})
}
else { // object.type === 'object'
handleExpand = (path, doExpand) => {
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)
this.setState({
data: setIn(this.state.data, dataPath.concat(['expanded']), expand)
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
}

410
src/jsonData.js Normal file
View File

@ -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
}

View File

@ -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
*/