Use JSONPatch actions internally

This commit is contained in:
jos 2016-09-16 14:41:51 +02:00
parent 44183e01bd
commit 678db6bced
8 changed files with 503 additions and 355 deletions

View File

@ -3,7 +3,7 @@ import { h, Component } from 'preact'
import ContextMenu from './ContextMenu'
import { escapeHTML, unescapeHTML } from './utils/stringUtils'
import { getInnerText } from './utils/domUtils'
import {stringConvert, valueType, isUrl} from './utils/typeUtils'
import { stringConvert, valueType, isUrl } from './utils/typeUtils'
// TYPE_TITLES with explanation for the different types
const TYPE_TITLES = {
@ -166,7 +166,7 @@ export default class JSONNode extends Component {
renderProperty (prop, data, options) {
if (prop !== null) {
const isIndex = typeof prop === 'number'
const isIndex = typeof prop === 'number' // FIXME: pass an explicit prop isIndex
if (isIndex) { // array item
return h('div', {
@ -375,9 +375,6 @@ export default class JSONNode extends Component {
}
if (hasParent) {
const parentPath = this.props.parent.getPath()
const prop = this.props.prop
if (items.length) {
// create a separator
items.push({
@ -391,31 +388,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(parentPath, prop, 'value'),
click: () => events.onInsert(path, 'value'),
submenu: [
{
text: 'Value',
className: 'jsoneditor-type-value',
title: TYPE_TITLES.value,
click: () => events.onInsert(parentPath, prop,'value')
click: () => events.onInsert(path, 'value')
},
{
text: 'Array',
className: 'jsoneditor-type-array',
title: TYPE_TITLES.array,
click: () => events.onInsert(parentPath, prop, 'array')
click: () => events.onInsert(path, 'array')
},
{
text: 'Object',
className: 'jsoneditor-type-object',
title: TYPE_TITLES.object,
click: () => events.onInsert(parentPath, prop, 'object')
click: () => events.onInsert(path, 'object')
},
{
text: 'String',
className: 'jsoneditor-type-string',
title: TYPE_TITLES.string,
click: () => events.onInsert(parentPath, prop, 'string')
click: () => events.onInsert(path, 'string')
}
]
})
@ -425,7 +422,7 @@ export default class JSONNode extends Component {
text: 'Duplicate',
title: 'Duplicate this item (Ctrl+D)',
className: 'jsoneditor-duplicate',
click: () => events.onDuplicate(parentPath, prop)
click: () => events.onDuplicate(path)
})
// create remove button
@ -433,7 +430,7 @@ export default class JSONNode extends Component {
text: 'Remove',
title: 'Remove this item (Ctrl+Del)',
className: 'jsoneditor-remove',
click: () => events.onRemove(parentPath, prop)
click: () => events.onRemove(path)
})
}
@ -522,16 +519,19 @@ export default class JSONNode extends Component {
const parentPath = this.props.parent.getPath()
const oldProp = this.props.prop
const newProp = unescapeHTML(getInnerText(event.target))
console.log('newProp', newProp)
this.props.events.onChangeProperty(parentPath, oldProp, newProp)
if (newProp !== oldProp) {
this.props.events.onChangeProperty(parentPath, oldProp, newProp)
}
}
handleChangeValue (event) {
const value = this._getValueFromEvent(event)
console.log('value', value)
this.props.events.onChangeValue(this.getPath(), value)
if (value !== this.props.data.value) {
console.log('oldValue', this.props.data.value, value)
this.props.events.onChangeValue(this.getPath(), value)
}
}
handleClickValue (event) {
@ -628,7 +628,7 @@ export default class JSONNode extends Component {
/**
* Get the path of this JSONNode
* @return {Array.<string | number>}
* @return {Path}
*/
getPath () {
const path = this.props.parent

View File

@ -2,13 +2,12 @@ import { h, Component } from 'preact'
import { setIn, updateIn } from './utils/immutabilityHelpers'
import {
changeValue, changeProperty, changeType,
insertAfter, append, duplicate, remove,
sort,
expand,
jsonToData, dataToJson, toDataPath,
createDataEntry
expand,
jsonToData, dataToJson, toDataPath, patchData, compileJSONPointer
} from './jsonData'
import {
duplicate, insert, append, changeType, changeValue, changeProperty, sort
} from './actions'
import JSONNode from './JSONNode'
export default class TreeMode extends Component {
@ -93,62 +92,73 @@ export default class TreeMode extends Component {
}
handleChangeValue = (path, value) => {
this.setData(changeValue(this.state.data, path, value))
this.handlePatch(changeValue(this.state.data, path, value))
}
handleChangeProperty = (path, oldProp, newProp) => {
this.setData(changeProperty(this.state.data, path, oldProp, newProp))
handleChangeProperty = (parentPath, oldProp, newProp) => {
this.handlePatch(changeProperty(this.state.data, parentPath, oldProp, newProp))
}
handleChangeType = (path, type) => {
this.setData(changeType(this.state.data, path, type))
this.handlePatch(changeType(this.state.data, path, type))
}
handleInsert = (path, afterProp, type) => {
this.setData(insertAfter(this.state.data, path, afterProp, type))
handleInsert = (path, type) => {
this.handlePatch(insert(this.state.data, path, type))
}
handleAppend = (path, type) => {
this.setData(append(this.state.data, path, type))
handleAppend = (parentPath, type) => {
this.handlePatch(append(this.state.data, parentPath, type))
}
handleDuplicate = (path, type) => {
this.setData(duplicate(this.state.data, path, type))
handleDuplicate = (path) => {
this.handlePatch(duplicate(this.state.data, path))
}
handleRemove = (path, prop) => {
this.setData(remove(this.state.data, path.concat(prop)))
handleRemove = (path) => {
const patch = [{
op: 'remove',
path: compileJSONPointer(path)
}]
this.handlePatch(patch)
}
handleSort = (path, order = null) => {
this.setData(sort(this.state.data, path, order))
this.handlePatch(sort(this.state.data, path, order))
}
handleExpand = (path, expanded, recurse) => {
if (recurse) {
const dataPath = toDataPath(this.state.data, path)
this.setData(updateIn (this.state.data, dataPath, function (child) {
return expand(child, (path) => true, expanded)
}))
this.setState({
data: updateIn(this.state.data, dataPath, function (child) {
return expand(child, (path) => true, expanded)
})
})
}
else {
this.setData(expand(this.state.data, path, expanded))
this.setState({
data: expand(this.state.data, path, expanded)
})
}
}
handleExpandAll = () => {
const all = (path) => true
const expanded = true
this.setData(expand(this.state.data, all, expanded))
this.setState({
data: expand(this.state.data, expandAll, expanded)
})
}
handleCollapseAll = () => {
const all = (path) => true
const expanded = false
this.setData(expand(this.state.data, all, expanded))
this.setState({
data: expand(this.state.data, expandAll, expanded)
})
}
canUndo = () => {
@ -177,7 +187,30 @@ export default class TreeMode extends Component {
}
}
setData (data) {
/**
* Apply a JSONPatch to the current JSON document and emit a change event
* @param {Array} actions
*/
handlePatch = (actions) => {
// apply changes
const revert = this.patch(actions)
// emit change event
if (this.props.options && this.props.options.onChange) {
this.props.options.onChange(actions, revert)
}
}
/**
* Apply a JSONPatch to the current JSON document
* @param {Array} actions JSONPatch actions
* @return {Array} Returns a JSONPatch to revert the applied patch
*/
patch (actions) {
const result = patchData(this.state.data, actions)
const data = result.data
// TODO: store patch and revert in history
const history = [data]
.concat(this.state.history.slice(this.state.historyIndex))
.slice(0, 1000)
@ -187,6 +220,8 @@ export default class TreeMode extends Component {
history,
historyIndex: 0
})
return result.revert
}
/**
@ -250,3 +285,8 @@ export default class TreeMode extends Component {
}
}
function expandAll (path) {
return true
}

331
src/actions.js Normal file
View File

@ -0,0 +1,331 @@
import { compileJSONPointer, toDataPath, dataToJson } from './jsonData'
import { findUniqueName } from './utils/stringUtils'
import { getIn } from './utils/immutabilityHelpers'
import { isObject, stringConvert } from './utils/typeUtils'
import { compareAsc, compareDesc, strictShallowEqual } from './utils/arrayUtils'
/**
* Create a JSONPatch to change the value of a property or item
* @param {JSONData} data
* @param {Path} path
* @param {*} value
* @return {Array}
*/
export function changeValue (data, path, value) {
// console.log('changeValue', data, value)
const dataPath = toDataPath(data, path)
const oldDataValue = getIn(data, dataPath)
return [{
op: 'replace',
path: compileJSONPointer(path),
value: value,
jsoneditor: { type: oldDataValue.type } // TODO: send type only in case of 'string'
// TODO: send some information to ensure the correct order of fields?
}]
}
/**
* Create a JSONPatch to change a property name
* @param {JSONData} data
* @param {Path} parentPath
* @param {string} oldProp
* @param {string} newProp
* @return {Array}
*/
export function changeProperty (data, parentPath, oldProp, newProp) {
console.log('changeProperty', parentPath, oldProp, newProp)
const dataPath = toDataPath(data, parentPath)
const parent = getIn(data, dataPath)
// find property after this one
const index = parent.props.findIndex(p => p.name === oldProp)
const next = parent.props[index + 1]
const nextProp = next && next.name
// prevent duplicate property names
const uniqueNewProp = findUniqueName(newProp, parent.props.map(p => p.name))
return [{
op: 'move',
from: compileJSONPointer(parentPath.concat(oldProp)),
path: compileJSONPointer(parentPath.concat(uniqueNewProp)),
jsoneditor: {
before: nextProp
}
}]
}
/**
* Create a JSONPatch to change the type of a property or item
* @param {JSONData} data
* @param {Path} path
* @param {JSONDataType} type
* @return {Array}
*/
export function changeType (data, path, type) {
const dataPath = toDataPath(data, path)
const oldEntry = dataToJson(getIn(data, dataPath))
const newEntry = convertType(oldEntry, type)
console.log('changeType', path, type, oldEntry, newEntry)
return [{
op: 'replace',
path: compileJSONPointer(path),
value: newEntry,
jsoneditor: { type } // TODO: send type only in case of 'string'
// TODO: send some information to ensure the correct order of fields?
}]
}
/**
* Create a JSONPatch for a duplicate action.
*
* This function needs the current data in order to be able to determine
* a unique property name for the duplicated node in case of duplicating
* and object property
*
* @param {JSONData} data
* @param {Path} path
* @return {Array}
*/
export function duplicate (data, path) {
// console.log('duplicate', path)
const parentPath = path.slice(0, path.length - 1)
const dataPath = toDataPath(data, parentPath)
const parent = getIn(data, dataPath)
if (parent.type === 'array') {
const index = parseInt(path[path.length - 1]) + 1
return [{
op: 'copy',
from: compileJSONPointer(path),
path: compileJSONPointer(parentPath.concat(index))
}]
}
else { // object.type === 'object'
const afterProp = path[path.length - 1]
const newProp = findUniqueName(afterProp, parent.props.map(p => p.name))
return [{
op: 'copy',
from: compileJSONPointer(path),
path: compileJSONPointer(parentPath.concat(newProp)),
jsoneditor: { afterProp }
}]
}
}
/**
* Create a JSONPatch for an insert action.
*
* This function needs the current data in order to be able to determine
* a unique property name for the inserted node in case of duplicating
* and object property
*
* @param {JSONData} data
* @param {Path} path
* @param {JSONDataType} type
* @return {Array}
*/
export function insert (data, path, type) {
// console.log('insert', path, type)
const parentPath = path.slice(0, path.length - 1)
const dataPath = toDataPath(data, parentPath)
const parent = getIn(data, dataPath)
const value = createEntry(type)
if (parent.type === 'array') {
const index = parseInt(path[path.length - 1]) + 1
return [{
op: 'add',
path: compileJSONPointer(parentPath.concat(index + '')),
value
}]
}
else { // object.type === 'object'
const afterProp = path[path.length - 1]
const newProp = findUniqueName('', parent.props.map(p => p.name))
return [{
op: 'add',
path: compileJSONPointer(parentPath.concat(newProp)),
value,
jsoneditor: { afterProp }
}]
}
}
/**
* Create a JSONPatch for an append action.
*
* This function needs the current data in order to be able to determine
* a unique property name for the inserted node in case of duplicating
* and object property
*
* @param {JSONData} data
* @param {Path} parentPath
* @param {JSONDataType} type
* @return {Array}
*/
export function append (data, parentPath, type) {
// console.log('append', parentPath, value)
const dataPath = toDataPath(data, parentPath)
const parent = getIn(data, dataPath)
const value = createEntry(type)
if (parent.type === 'array') {
return [{
op: 'add',
path: compileJSONPointer(parentPath.concat('-')),
value
}]
}
else { // object.type === 'object'
const newProp = findUniqueName('', parent.props.map(p => p.name))
return [{
op: 'add',
path: compileJSONPointer(parentPath.concat(newProp)),
value
}]
}
}
/**
* Create a JSONPatch to 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 {Array}
*/
export function sort (data, path, order = null) {
// console.log('sort', path, order)
const compare = order === 'desc' ? compareDesc : compareAsc
const dataPath = toDataPath(data, path)
const object = getIn(data, dataPath)
if (object.type === 'array') {
const orderedItems = object.items.slice(0)
// order the items by value
orderedItems.sort((a, b) => compare(a.value, b.value))
// when no order is provided, test whether ordering ascending
// changed anything. If not, sort descending
if (!order && strictShallowEqual(object.items, orderedItems)) {
orderedItems.reverse()
}
return [{
op: 'replace',
path: compileJSONPointer(path),
value: dataToJson({
type: 'array',
items: orderedItems
})
}]
}
else { // object.type === 'object'
const orderedProps = object.props.slice(0)
// order the properties by key
orderedProps.sort((a, b) => compare(a.name, b.name))
// when no order is provided, test whether ordering ascending
// changed anything. If not, sort descending
if (!order && strictShallowEqual(object.props, orderedProps)) {
orderedProps.reverse()
}
return [{
op: 'replace',
path: compileJSONPointer(path),
value: dataToJson({
type: 'object',
props: orderedProps
}),
jsoneditor: {
order: orderedProps.map(prop => prop.name)
}
}]
}
}
/**
* Create a JSON entry
* @param {JSONDataType} type
* @return {Array | Object | string}
*/
export function createEntry (type) {
if (type === 'array') {
return []
}
else if (type === 'object') {
return {}
}
else {
return ''
}
}
/**
* Convert a JSON object into a different type. When possible, data is retained
* @param {*} value
* @param {JSONDataType} type
* @return {*}
*/
export function convertType (value, type) {
// convert contents from old value to new value where possible
if (type === 'value') {
if (typeof value === 'string') {
return stringConvert(value)
}
else {
return ''
}
}
if (type === 'string') {
if (!isObject(value) && !Array.isArray(value)) {
return value + ''
}
else {
return ''
}
}
if (type === 'object') {
let object = {}
if (Array.isArray(value)) {
value.forEach((item, index) => object[index] = item)
}
return object
}
if (type === 'array') {
let array = []
if (isObject(value)) {
Object.keys(value).forEach(key => {
array.push(value[key])
})
}
return array
}
throw new Error(`Unknown type '${type}'`)
}

View File

@ -24,7 +24,13 @@
<script>
// create the editor
const container = document.getElementById('container');
const options = {};
const options = {
onChange: function (patch, revert) {
console.log('onChange patch=', patch, ', revert=', revert)
window.patch = patch
window.revert = revert
}
};
const editor = jsoneditor(container, options);
const json = {
'array': [1, 2, 3],

View File

@ -82,6 +82,15 @@ function jsoneditor (container, options) {
component.collapse(callback)
},
/**
* Apply a JSONPatch to the current JSON document
* @param {Array} actions JSONPatch actions
* @return {Array} Returns a JSONPatch to revert the applied patch
*/
patch (actions) {
return component.patch(actions)
}
// TODO: implement destroy
}

View File

@ -4,11 +4,8 @@
*/
import { setIn, updateIn, getIn, deleteIn } from './utils/immutabilityHelpers'
import { compareAsc, compareDesc } from './utils/arrayUtils'
import { isObject, stringConvert } from './utils/typeUtils'
import { findUniqueName } from './utils/stringUtils'
import { isObject } from './utils/typeUtils'
import isEqual from 'lodash/isEqual'
import cloneDeep from 'lodash/isEqual'
// TODO: rewrite the functions into jsonpatch functions, including a function `patch`
@ -16,140 +13,6 @@ const expandNever = function (path) {
return false
}
/**
* 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)
if (oldProp === newProp) {
return data
}
const dataPath = toDataPath(data, path)
const object = getIn(data, dataPath)
const index = object.props.findIndex(p => p.name === oldProp)
// prevent duplicate property names
const uniqueNewProp = findUniqueName(newProp, object.props.map(p => p.name))
return setIn(data, dataPath.concat(['props', index, 'name']), uniqueNewProp)
}
/**
* 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 = convertDataType(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}
*/
// TODO: remove function insertAfter, create insert(data, path, value, afterProp) instead
export function insertAfter (data, path, afterProp, type) {
// console.log('insertAfter', 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}
*/
// TODO: remove append, use add instead
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
})
}
}
/**
* Replace an existing item
* @param {JSONData} data
@ -171,49 +34,6 @@ export function replace (data, path, value) {
}
}
/**
* Duplicate a property or item
* @param {JSONData} data
* @param {Path} path
* @param {string | number} prop
* @return {JSONData}
*/
// TODO: remove this function, use copy
export function duplicate (data, path, prop) {
// console.log('duplicate', path, prop)
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 clone = cloneDeep(original)
// prevent duplicate property names
clone.name = findUniqueName(clone.name, props.map(p => p.name))
updated.splice(index + 1, 0, clone)
return updated
})
}
}
/**
* Remove an item or property
* @param {JSONData} data
@ -226,14 +46,21 @@ export function remove (data, path) {
const parentPath = _path.slice(0, _path.length - 1)
const parent = getIn(data, toDataPath(data, parentPath))
const value = dataToJson(getIn(data, toDataPath(data, _path)))
const dataValue = getIn(data, toDataPath(data, _path))
const value = dataToJson(dataValue)
// extra information attached to the patch
const jsoneditor = {
type: dataValue.type
// FIXME: store before
}
if (parent.type === 'array') {
const dataPath = toDataPath(data, _path)
return {
data: deleteIn(data, dataPath),
revert: {op: 'add', path, value}
revert: {op: 'add', path, value, jsoneditor}
}
}
else { // object.type === 'object'
@ -242,58 +69,11 @@ export function remove (data, path) {
dataPath.pop() // remove the 'value' property, we want to remove the whole object property
return {
data: deleteIn(data, dataPath),
revert: {op: 'add', path, value}
revert: {op: 'add', path, value, jsoneditor}
}
}
}
/**
* 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 one or multiple items or properties
* @param {JSONData} data
@ -330,6 +110,7 @@ export function expand (data, callback, expanded) {
* expanded/collapsed
* @param {boolean} expanded New expanded state: true to expand, false to collapse
* @return {*}
* @private
*/
function expandRecursive (data, path, callback, expanded) {
switch (data.type) {
@ -490,10 +271,13 @@ export function dataToJson (data) {
* @param {JSONData} data
* @param {string} path
* @param {JSONData} value
* @param {string} [afterProp] In case of an object, the property
* after which this new property must be added
* can be specified
* @return {{data: JSONData, revert: Object}}
* @private
*/
function add (data, path, value) {
export function add (data, path, value, afterProp) {
const _path = parseJSONPointer(path)
const parentPath = _path.slice(0, _path.length - 1)
@ -524,11 +308,19 @@ function add (data, path, value) {
else { // parent.type === 'object'
// TODO: create an immutable helper function to append an item to an Array
updatedData = updateIn(data, dataPath.concat('props'), (props) => {
const newProp = {
name: prop,
value
const newProp = { name: prop, value }
if (afterProp === undefined) {
// append
return props.concat(newProp)
}
else {
// insert after prop
const updatedProps = props.slice(0)
const index = props.findIndex(p => p.name === afterProp)
updatedProps.splice(index + 1, 0, newProp)
return updatedProps
}
return props.concat(newProp)
})
}
@ -545,14 +337,16 @@ function add (data, path, value) {
* @param {JSONData} data
* @param {string} path
* @param {string} from
* @param {string} [afterProp] In case of an object, the property
* after which this new property must be added
* can be specified
* @return {{data: JSONData, revert: Object}}
* @private
*/
// TODO: add an optional parameter `beforeProp` or `afterProp`
export function copy (data, path, from) {
export function copy (data, path, from, afterProp) {
const value = getIn(data, toDataPath(data, parseJSONPointer(from)))
return add(data, path, value)
return add(data, path, value, afterProp)
}
/**
@ -560,19 +354,24 @@ export function copy (data, path, from) {
* @param {JSONData} data
* @param {string} path
* @param {string} from
* @param {string} [afterProp] In case of an object, the property
* after which this new property must be added
* can be specified
* @return {{data: JSONData, revert: Object}}
* @private
*/
export function move (data, path, from) {
export function move (data, path, from, afterProp) {
if (path !== from) {
const value = getIn(data, toDataPath(data, parseJSONPointer(from)))
const result1 = remove(data, from)
let updatedData = result1.data
const result2 = add(updatedData, path, value)
const result2 = add(updatedData, path, value, afterProp)
updatedData = result2.data
// FIXME: the revert action should store afterProp
if (result2.revert.op === 'replace') {
return {
data: updatedData,
@ -666,8 +465,9 @@ export function patchData (data, patch) {
case 'add': {
const path = parseJSONPointer(action.path)
const value = jsonToData(path, action.value, expand)
const afterProp = getIn(action, ['jsoneditor', 'afterProp'])
const result = add(updatedData, action.path, value)
const result = add(updatedData, action.path, value, afterProp)
updatedData = result.data
revert.unshift(result.revert)
@ -684,7 +484,12 @@ export function patchData (data, patch) {
case 'replace': {
const path = parseJSONPointer(action.path)
const newValue = jsonToData(path, action.value, expand)
let newValue = jsonToData(path, action.value, expand)
if (action.jsoneditor && action.jsoneditor.type) {
// insert with type 'string' or 'value'
newValue.type = action.jsoneditor.type
}
const result = replace(updatedData, path, newValue)
updatedData = result.data
@ -694,7 +499,8 @@ export function patchData (data, patch) {
}
case 'copy': {
const result = copy(updatedData, action.path, action.from)
const afterProp = getIn(action, ['jsoneditor', 'afterProp'])
const result = copy(updatedData, action.path, action.from, afterProp)
updatedData = result.data
revert.unshift(result.revert)
@ -702,7 +508,8 @@ export function patchData (data, patch) {
}
case 'move': {
const result = move(updatedData, action.path, action.from)
const afterProp = getIn(action, ['jsoneditor', 'afterProp'])
const result = move(updatedData, action.path, action.from, afterProp)
updatedData = result.data
revert = result.revert.concat(revert)
@ -732,68 +539,6 @@ export function patchData (data, patch) {
}
}
/**
* 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 a JSONData object into a different type. When possible, data is retained
* @param {JSONData} data
* @param {JSONDataType} type
* @return {JSONData}
*/
export function convertDataType (data, type) {
const convertedEntry = createDataEntry(type)
// convert contents from old value to new value where possible
if (type === 'value' && data.type === 'string') {
convertedEntry.value = stringConvert(data.value)
}
if (type === 'string' && data.type === 'value') {
convertedEntry.value = data.value + ''
}
if (type === 'object' && data.type === 'array') {
convertedEntry.props = data.items.map((item, index) => {
return {
name: index + '',
value: item
}
})
}
if (type === 'array' && data.type === 'object') {
convertedEntry.items = data.props.map(prop => prop.value)
}
return convertedEntry
}
/**
* Parse a JSON Pointer
* WARNING: this is not a complete string implementation

View File

@ -34,3 +34,22 @@ export function compareAsc (a, b) {
export function compareDesc (a, b) {
return a > b ? -1 : a < b ? 1 : 0
}
/**
* Test whether all items of an array are strictly equal
* @param {Array} a
* @param {Array} b
*/
export function strictShallowEqual (a, b) {
if (a.length !== b.length) {
return false
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false
}
}
return true
}

View File

@ -5,8 +5,6 @@ import {
} from '../src/jsonData'
// TODO: test all functions like append, insert, duplicate etc.
const JSON_EXAMPLE = {
obj: {
arr: [1,2, {a:3,b:4}]