Use JSONPatch actions internally
This commit is contained in:
parent
44183e01bd
commit
678db6bced
|
@ -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,17 +519,20 @@ 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)
|
||||
|
||||
if (newProp !== oldProp) {
|
||||
this.props.events.onChangeProperty(parentPath, oldProp, newProp)
|
||||
}
|
||||
}
|
||||
|
||||
handleChangeValue (event) {
|
||||
const value = this._getValueFromEvent(event)
|
||||
console.log('value', value)
|
||||
|
||||
if (value !== this.props.data.value) {
|
||||
console.log('oldValue', this.props.data.value, value)
|
||||
this.props.events.onChangeValue(this.getPath(), value)
|
||||
}
|
||||
}
|
||||
|
||||
handleClickValue (event) {
|
||||
if (event.ctrlKey && event.button === 0) { // Ctrl+Left click
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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}'`)
|
||||
}
|
|
@ -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],
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
359
src/jsonData.js
359
src/jsonData.js
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}]
|
||||
|
|
Loading…
Reference in New Issue