Flatten `META` symbol, remove ordering of object keys from ESON model, simplify patch actions (WIP)
This commit is contained in:
parent
d6ad4c87d0
commit
56124cf17f
File diff suppressed because it is too large
Load Diff
|
@ -19,9 +19,10 @@
|
||||||
"bugs": "https://github.com/josdejong/jsoneditor/issues",
|
"bugs": "https://github.com/josdejong/jsoneditor/issues",
|
||||||
"private": false,
|
"private": false,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "6.5.2",
|
"ajv": "6.5.3",
|
||||||
"brace": "0.11.1",
|
"brace": "0.11.1",
|
||||||
"javascript-natural-sort": "0.7.1",
|
"javascript-natural-sort": "0.7.1",
|
||||||
|
"jest": "23.5.0",
|
||||||
"lodash": "4.17.10",
|
"lodash": "4.17.10",
|
||||||
"mitt": "1.1.3",
|
"mitt": "1.1.3",
|
||||||
"prop-types": "15.6.2",
|
"prop-types": "15.6.2",
|
||||||
|
@ -43,8 +44,8 @@
|
||||||
"css-loader": "1.0.0",
|
"css-loader": "1.0.0",
|
||||||
"node-sass-chokidar": "1.3.3",
|
"node-sass-chokidar": "1.3.3",
|
||||||
"npm-run-all": "4.1.3",
|
"npm-run-all": "4.1.3",
|
||||||
"preact": "8.3.0",
|
"preact": "8.3.1",
|
||||||
"preact-compat": "3.18.2",
|
"preact-compat": "3.18.3",
|
||||||
"react": "16.4.2",
|
"react": "16.4.2",
|
||||||
"react-dom": "16.4.2",
|
"react-dom": "16.4.2",
|
||||||
"react-scripts": "1.1.4",
|
"react-scripts": "1.1.4",
|
||||||
|
|
|
@ -159,7 +159,7 @@ class App extends Component {
|
||||||
|
|
||||||
handlePatch = (patch, revert) => {
|
handlePatch = (patch, revert) => {
|
||||||
this.log('onPatch patch=', patch, ', revert=', revert)
|
this.log('onPatch patch=', patch, ', revert=', revert)
|
||||||
window.patch = patch
|
window.immutableJsonPatch = patch
|
||||||
window.revert = revert
|
window.revert = revert
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,7 @@
|
||||||
name: 'myObject',
|
name: 'myObject',
|
||||||
onPatch: function (patch, revert) {
|
onPatch: function (patch, revert) {
|
||||||
log('onPatch patch=', patch, ', revert=', revert)
|
log('onPatch patch=', patch, ', revert=', revert)
|
||||||
window.patch = patch
|
window.immutableJsonPatch = patch
|
||||||
window.revert = revert
|
window.revert = revert
|
||||||
},
|
},
|
||||||
onPatchText: function (patch, revert) {
|
onPatchText: function (patch, revert) {
|
||||||
|
|
|
@ -1,72 +1,61 @@
|
||||||
import last from 'lodash/last'
|
import last from 'lodash/last'
|
||||||
import initial from 'lodash/initial'
|
import initial from 'lodash/initial'
|
||||||
import isEmpty from 'lodash/isEmpty'
|
import isEmpty from 'lodash/isEmpty'
|
||||||
import {
|
import { findRootPath, findSelectionIndices, pathsFromSelection } from './eson'
|
||||||
META,
|
|
||||||
compileJSONPointer, esonToJson, findNextProp,
|
|
||||||
pathsFromSelection, findRootPath, findSelectionIndices
|
|
||||||
} from './eson'
|
|
||||||
import { getIn } from './utils/immutabilityHelpers'
|
import { getIn } from './utils/immutabilityHelpers'
|
||||||
import { findUniqueName } from './utils/stringUtils'
|
import { findUniqueName } from './utils/stringUtils'
|
||||||
import { isObject, stringConvert } from './utils/typeUtils'
|
import { isObject, stringConvert } from './utils/typeUtils'
|
||||||
import { compareAsc, compareDesc } from './utils/arrayUtils'
|
import { compareAsc, compareDesc } from './utils/arrayUtils'
|
||||||
|
import { compileJSONPointer } from './jsonPointer'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a JSONPatch to change the value of a property or item
|
* Create a JSONPatch to change the value of a property or item
|
||||||
* @param {ESON} eson
|
* @param {JSON} eson
|
||||||
* @param {Path} path
|
* @param {Path} path
|
||||||
* @param {*} value
|
* @param {*} value
|
||||||
* @return {Array}
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
export function changeValue (eson, path, value) {
|
export function changeValue (eson, path, value) {
|
||||||
// console.log('changeValue', data, value)
|
// console.log('changeValue', data, value)
|
||||||
const oldDataValue = getIn(eson, path)
|
|
||||||
|
|
||||||
return [{
|
return [{
|
||||||
op: 'replace',
|
op: 'replace',
|
||||||
path: compileJSONPointer(path),
|
path: compileJSONPointer(path),
|
||||||
value: value,
|
value
|
||||||
meta: {
|
|
||||||
type: oldDataValue[META].type
|
|
||||||
}
|
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a JSONPatch to change a property name
|
* Create a JSONPatch to change a property name
|
||||||
* @param {ESON} eson
|
* @param {JSON} json
|
||||||
* @param {Path} parentPath
|
* @param {Path} parentPath
|
||||||
* @param {string} oldProp
|
* @param {string} oldProp
|
||||||
* @param {string} newProp
|
* @param {string} newProp
|
||||||
* @return {Array}
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
export function changeProperty (eson, parentPath, oldProp, newProp) {
|
export function changeProperty (json, parentPath, oldProp, newProp) {
|
||||||
// console.log('changeProperty', parentPath, oldProp, newProp)
|
// console.log('changeProperty', parentPath, oldProp, newProp)
|
||||||
const parent = getIn(eson, parentPath)
|
const parent = getIn(json, parentPath)
|
||||||
|
|
||||||
// prevent duplicate property names
|
// prevent duplicate property names
|
||||||
const uniqueNewProp = findUniqueName(newProp, parent[META].props)
|
const uniqueNewProp = findUniqueName(newProp, parent)
|
||||||
|
|
||||||
return [{
|
return [{
|
||||||
op: 'move',
|
op: 'move',
|
||||||
from: compileJSONPointer(parentPath.concat(oldProp)),
|
from: compileJSONPointer(parentPath.concat(oldProp)),
|
||||||
path: compileJSONPointer(parentPath.concat(uniqueNewProp)),
|
path: compileJSONPointer(parentPath.concat(uniqueNewProp))
|
||||||
meta: {
|
|
||||||
before: findNextProp(parent, oldProp)
|
|
||||||
}
|
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a JSONPatch to change the type of a property or item
|
* Create a JSONPatch to change the type of a property or item
|
||||||
* @param {ESON} eson
|
* @param {JSON} json
|
||||||
* @param {Path} path
|
* @param {Path} path
|
||||||
* @param {ESONType} type
|
* @param {ESONType} type
|
||||||
* @return {Array}
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
export function changeType (eson, path, type) {
|
export function changeType (json, path, type) {
|
||||||
const oldValue = esonToJson(getIn(eson, path))
|
const oldValue = getIn(json, path)
|
||||||
const newValue = convertType(oldValue, type)
|
const newValue = convertType(oldValue, type)
|
||||||
|
|
||||||
// console.log('changeType', path, type, oldValue, newValue)
|
// console.log('changeType', path, type, oldValue, newValue)
|
||||||
|
@ -74,10 +63,7 @@ export function changeType (eson, path, type) {
|
||||||
return [{
|
return [{
|
||||||
op: 'replace',
|
op: 'replace',
|
||||||
path: compileJSONPointer(path),
|
path: compileJSONPointer(path),
|
||||||
value: newValue,
|
value: newValue
|
||||||
meta: {
|
|
||||||
type
|
|
||||||
}
|
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,42 +74,37 @@ export function changeType (eson, path, type) {
|
||||||
* a unique property name for the duplicated node in case of duplicating
|
* a unique property name for the duplicated node in case of duplicating
|
||||||
* and object property
|
* and object property
|
||||||
*
|
*
|
||||||
* @param {ESON} eson
|
* @param {JSON} json
|
||||||
* @param {Selection} selection
|
* @param {Selection} selection
|
||||||
* @return {Array}
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
export function duplicate (eson, selection) {
|
export function duplicate (json, selection) {
|
||||||
// console.log('duplicate', path)
|
// console.log('duplicate', path)
|
||||||
if (!selection.start || !selection.end) {
|
if (!selection.start || !selection.end) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootPath = findRootPath(selection)
|
const rootPath = findRootPath(selection)
|
||||||
const root = getIn(eson, rootPath)
|
const root = getIn(json, rootPath)
|
||||||
const { maxIndex } = findSelectionIndices(root, rootPath, selection)
|
const { maxIndex } = findSelectionIndices(root, rootPath, selection)
|
||||||
const paths = pathsFromSelection(eson, selection)
|
const paths = pathsFromSelection(json, selection)
|
||||||
|
|
||||||
if (root[META].type === 'Array') {
|
if (Array.isArray(root)) {
|
||||||
return paths.map((path, offset) => ({
|
return paths.map((path, offset) => ({
|
||||||
op: 'copy',
|
op: 'copy',
|
||||||
from: compileJSONPointer(path),
|
from: compileJSONPointer(path),
|
||||||
path: compileJSONPointer(rootPath.concat(maxIndex + offset))
|
path: compileJSONPointer(rootPath.concat(maxIndex + offset))
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
else { // root[META].type === 'Object'
|
else { // 'object'
|
||||||
const before = root[META].props[maxIndex] || null
|
|
||||||
|
|
||||||
return paths.map(path => {
|
return paths.map(path => {
|
||||||
const prop = last(path)
|
const prop = last(path)
|
||||||
const newProp = findUniqueName(prop, root[META].props)
|
const newProp = findUniqueName(prop, root)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
op: 'copy',
|
op: 'copy',
|
||||||
from: compileJSONPointer(path),
|
from: compileJSONPointer(path),
|
||||||
path: compileJSONPointer(rootPath.concat(newProp)),
|
path: compileJSONPointer(rootPath.concat(newProp))
|
||||||
meta: {
|
|
||||||
before
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -136,38 +117,31 @@ export function duplicate (eson, selection) {
|
||||||
* a unique property name for the inserted node in case of duplicating
|
* a unique property name for the inserted node in case of duplicating
|
||||||
* and object property
|
* and object property
|
||||||
*
|
*
|
||||||
* @param {ESON} eson
|
* @param {JSON} json
|
||||||
* @param {Path} path
|
* @param {Path} path
|
||||||
* @param {Array.<{name?: string, value: JSON, type?: ESONType}>} values
|
* @param {Array.<{name?: string, value: JSON}>} values
|
||||||
* @return {Array}
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
export function insertBefore (eson, path, values) { // TODO: find a better name and define datastructure for values
|
export function insertBefore (json, path, values) { // TODO: find a better name and define datastructure for values
|
||||||
|
// TODO: refactor. path should be parent path
|
||||||
const parentPath = initial(path)
|
const parentPath = initial(path)
|
||||||
const parent = getIn(eson, parentPath)
|
const parent = getIn(json, parentPath)
|
||||||
|
|
||||||
if (parent[META].type === 'Array') {
|
if (Array.isArray(parent)) {
|
||||||
const startIndex = parseInt(last(path), 10)
|
const startIndex = parseInt(last(path), 10)
|
||||||
return values.map((entry, offset) => ({
|
return values.map((entry, offset) => ({
|
||||||
op: 'add',
|
op: 'add',
|
||||||
path: compileJSONPointer(parentPath.concat(startIndex + offset)),
|
path: compileJSONPointer(parentPath.concat(startIndex + offset)),
|
||||||
value: entry.value,
|
value: entry.value
|
||||||
meta: {
|
|
||||||
type: entry.type
|
|
||||||
}
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
else { // parent[META].type === 'Object'
|
else { // 'object'
|
||||||
const before = last(path)
|
|
||||||
return values.map(entry => {
|
return values.map(entry => {
|
||||||
const newProp = findUniqueName(entry.name, parent[META].props)
|
const newProp = findUniqueName(entry.name, parent)
|
||||||
return {
|
return {
|
||||||
op: 'add',
|
op: 'add',
|
||||||
path: compileJSONPointer(parentPath.concat(newProp)),
|
path: compileJSONPointer(parentPath.concat(newProp)),
|
||||||
value: entry.value,
|
value: entry.value
|
||||||
meta: {
|
|
||||||
type: entry.type,
|
|
||||||
before
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -180,40 +154,31 @@ export function insertBefore (eson, path, values) { // TODO: find a better name
|
||||||
* a unique property name for the inserted node in case of duplicating
|
* a unique property name for the inserted node in case of duplicating
|
||||||
* and object property
|
* and object property
|
||||||
*
|
*
|
||||||
* @param {ESON} eson
|
* @param {JSON} json
|
||||||
* @param {Path} path
|
* @param {Path} path
|
||||||
* @param {Array.<{name?: string, value: JSON, type?: ESONType}>} values
|
* @param {Array.<{name?: string, value: JSON}>} values
|
||||||
* @return {Array}
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
export function insertAfter (eson, path, values) { // TODO: find a better name and define datastructure for values
|
export function insertAfter (json, path, values) { // TODO: find a better name and define datastructure for values
|
||||||
|
// TODO: refactor. path should be parent path
|
||||||
const parentPath = initial(path)
|
const parentPath = initial(path)
|
||||||
const parent = getIn(eson, parentPath)
|
const parent = getIn(json, parentPath)
|
||||||
|
|
||||||
if (parent[META].type === 'Array') {
|
if (Array.isArray(parent)) {
|
||||||
const startIndex = parseInt(last(path), 10)
|
const startIndex = parseInt(last(path), 10)
|
||||||
return values.map((entry, offset) => ({
|
return values.map((entry, offset) => ({
|
||||||
op: 'add',
|
op: 'add',
|
||||||
path: compileJSONPointer(parentPath.concat(startIndex + 1 + offset)), // +1 to insert after
|
path: compileJSONPointer(parentPath.concat(startIndex + 1 + offset)), // +1 to insert after
|
||||||
value: entry.value,
|
value: entry.value
|
||||||
meta: {
|
|
||||||
type: entry.type
|
|
||||||
}
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
else { // parent[META].type === 'Object'
|
else { // 'object'
|
||||||
const prop = last(path)
|
|
||||||
const propIndex = parent[META].props.indexOf(prop)
|
|
||||||
const before = parent[META].props[propIndex + 1]
|
|
||||||
return values.map(entry => {
|
return values.map(entry => {
|
||||||
const newProp = findUniqueName(entry.name, parent[META].props)
|
const newProp = findUniqueName(entry.name, parent)
|
||||||
return {
|
return {
|
||||||
op: 'add',
|
op: 'add',
|
||||||
path: compileJSONPointer(parentPath.concat(newProp)),
|
path: compileJSONPointer(parentPath.concat(newProp)),
|
||||||
value: entry.value,
|
value: entry.value
|
||||||
meta: {
|
|
||||||
type: entry.type,
|
|
||||||
before
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -221,20 +186,20 @@ export function insertAfter (eson, path, values) { // TODO: find a better name
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert values at the start of an Object or Array
|
* Insert values at the start of an Object or Array
|
||||||
* @param {ESON} eson
|
* @param {JSON} json
|
||||||
* @param {Path} parentPath
|
* @param {Path} parentPath
|
||||||
* @param {Array.<{name?: string, value: JSON, type?: ESONType}>} values
|
* @param {Array.<{name?: string, value: JSON}>} values
|
||||||
* @return {Array}
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
export function insertInside (eson, parentPath, values) {
|
export function insertInside (json, parentPath, values) {
|
||||||
const parent = getIn(eson, parentPath)
|
const parent = getIn(json, parentPath)
|
||||||
|
|
||||||
if (parent[META].type === 'Array') {
|
if (Array.isArray(parent)) {
|
||||||
return insertBefore(eson, parentPath.concat('0'), values)
|
return insertBefore(json, parentPath.concat('0'), values)
|
||||||
}
|
}
|
||||||
else if (parent[META].type === 'Object') {
|
else if (parent && typeof parent === 'object') {
|
||||||
const firstProp = parent[META].props[0] || null
|
// TODO: refactor. path should be parent path
|
||||||
return insertBefore(eson, parentPath.concat(firstProp), values)
|
return insertBefore(json, parentPath.concat('foobar'), values)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new Error('Cannot insert in a value, only in an Object or Array')
|
throw new Error('Cannot insert in a value, only in an Object or Array')
|
||||||
|
@ -248,43 +213,34 @@ export function insertInside (eson, parentPath, values) {
|
||||||
* a unique property name for the inserted node in case of duplicating
|
* a unique property name for the inserted node in case of duplicating
|
||||||
* and object property
|
* and object property
|
||||||
*
|
*
|
||||||
* @param {ESON} eson
|
* @param {JSON} json
|
||||||
* @param {Selection} selection
|
* @param {Selection} selection
|
||||||
* @param {Array.<{name?: string, value: JSON, state: Object}>} values
|
* @param {Array.<{name?: string, value: JSON}>} values
|
||||||
* @return {Array}
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
export function replace (eson, selection, values) { // TODO: find a better name and define datastructure for values
|
export function replace (json, selection, values) { // TODO: find a better name and define datastructure for values
|
||||||
const rootPath = findRootPath(selection)
|
const rootPath = findRootPath(selection)
|
||||||
const root = getIn(eson, rootPath)
|
const root = getIn(json, rootPath)
|
||||||
const { minIndex, maxIndex } = findSelectionIndices(root, rootPath, selection)
|
const { minIndex, maxIndex } = findSelectionIndices(root, rootPath, selection)
|
||||||
|
|
||||||
if (root[META].type === 'Array') {
|
if (Array.isArray(root)) {
|
||||||
const removeActions = removeAll(pathsFromSelection(eson, selection))
|
const removeActions = removeAll(pathsFromSelection(json, selection))
|
||||||
const insertActions = values.map((entry, offset) => ({
|
const insertActions = values.map((entry, offset) => ({
|
||||||
op: 'add',
|
op: 'add',
|
||||||
path: compileJSONPointer(rootPath.concat(minIndex + offset)),
|
path: compileJSONPointer(rootPath.concat(minIndex + offset)),
|
||||||
value: entry.value,
|
value: entry.value
|
||||||
meta: {
|
|
||||||
state: entry.state
|
|
||||||
}
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return removeActions.concat(insertActions)
|
return removeActions.concat(insertActions)
|
||||||
}
|
}
|
||||||
else { // root[META].type === 'Object'
|
else { // root is Object
|
||||||
const before = root[META].props[maxIndex] || null
|
const removeActions = removeAll(pathsFromSelection(json, selection))
|
||||||
|
|
||||||
const removeActions = removeAll(pathsFromSelection(eson, selection))
|
|
||||||
const insertActions = values.map(entry => {
|
const insertActions = values.map(entry => {
|
||||||
const newProp = findUniqueName(entry.name, root[META].props)
|
const newProp = findUniqueName(entry.name, root)
|
||||||
return {
|
return {
|
||||||
op: 'add',
|
op: 'add',
|
||||||
path: compileJSONPointer(rootPath.concat(newProp)),
|
path: compileJSONPointer(rootPath.concat(newProp)),
|
||||||
value: entry.value,
|
value: entry.value
|
||||||
meta: {
|
|
||||||
before,
|
|
||||||
state: entry.state
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -299,37 +255,31 @@ export function replace (eson, selection, values) { // TODO: find a better name
|
||||||
* a unique property name for the inserted node in case of duplicating
|
* a unique property name for the inserted node in case of duplicating
|
||||||
* and object property
|
* and object property
|
||||||
*
|
*
|
||||||
* @param {ESON} eson
|
* @param {JSON} json
|
||||||
* @param {Path} parentPath
|
* @param {Path} parentPath
|
||||||
* @param {ESONType} type
|
* @param {ESONType} type
|
||||||
* @return {Array}
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
export function append (eson, parentPath, type) {
|
export function append (json, parentPath, type) {
|
||||||
// console.log('append', parentPath, value)
|
// console.log('append', parentPath, value)
|
||||||
|
|
||||||
const parent = getIn(eson, parentPath)
|
const parent = getIn(json, parentPath)
|
||||||
const value = createEntry(type)
|
const value = createEntry(type)
|
||||||
|
|
||||||
if (parent[META].type === 'Array') {
|
if (Array.isArray(parent)) {
|
||||||
return [{
|
return [{
|
||||||
op: 'add',
|
op: 'add',
|
||||||
path: compileJSONPointer(parentPath.concat('-')),
|
path: compileJSONPointer(parentPath.concat('-')),
|
||||||
value,
|
value
|
||||||
meta: {
|
|
||||||
type
|
|
||||||
}
|
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
else { // parent[META].type === 'Object'
|
else { // 'object'
|
||||||
const newProp = findUniqueName('', parent[META].props)
|
const newProp = findUniqueName('', parent)
|
||||||
|
|
||||||
return [{
|
return [{
|
||||||
op: 'add',
|
op: 'add',
|
||||||
path: compileJSONPointer(parentPath.concat(newProp)),
|
path: compileJSONPointer(parentPath.concat(newProp)),
|
||||||
value,
|
value
|
||||||
meta: {
|
|
||||||
type
|
|
||||||
}
|
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -364,55 +314,35 @@ export function removeAll (paths) {
|
||||||
/**
|
/**
|
||||||
* Create a JSONPatch to order the items of an array or the properties of an object in ascending
|
* Create a JSONPatch to order the items of an array or the properties of an object in ascending
|
||||||
* or descending order
|
* or descending order
|
||||||
* @param {ESON} eson
|
* @param {JSON} json
|
||||||
* @param {Path} path
|
* @param {Path} path
|
||||||
* @param {'asc' | 'desc' | null} [order=null] If not provided, will toggle current ordering
|
* @param {'asc' | 'desc' | null} [order=null] If not provided, will toggle current ordering
|
||||||
* @return {Array}
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
export function sort (eson, path, order = null) {
|
export function sort (json, path, order = null) {
|
||||||
const compare = order === 'desc' ? compareDesc : compareAsc
|
const compare = order === 'desc' ? compareDesc : compareAsc
|
||||||
const reverseCompare = (a, b) => -compare(a, b)
|
const reverseCompare = (a, b) => -compare(a, b)
|
||||||
const object = getIn(eson, path)
|
const object = getIn(json, path)
|
||||||
|
|
||||||
if (object[META].type === 'Array') {
|
if (Array.isArray(object)) {
|
||||||
const items = object.map(item => item[META].value)
|
|
||||||
const createAction = ({item, fromIndex, toIndex}) => ({
|
const createAction = ({item, fromIndex, toIndex}) => ({
|
||||||
op: 'move',
|
op: 'move',
|
||||||
from: compileJSONPointer(path.concat(String(fromIndex))),
|
from: compileJSONPointer(path.concat(String(fromIndex))),
|
||||||
path: compileJSONPointer(path.concat(String(toIndex)))
|
path: compileJSONPointer(path.concat(String(toIndex)))
|
||||||
})
|
})
|
||||||
|
|
||||||
const actions = sortWithComparator(items, compare).map(createAction)
|
const actions = sortWithComparator(object, compare).map(createAction)
|
||||||
|
|
||||||
// when no order is provided, test whether ordering ascending
|
// when no order is provided, test whether ordering ascending
|
||||||
// changed anything. If not, sort descending
|
// changed anything. If not, sort descending
|
||||||
if (!order && isEmpty(actions)) {
|
if (!order && isEmpty(actions)) {
|
||||||
return sortWithComparator(items, reverseCompare).map(createAction)
|
return sortWithComparator(object, reverseCompare).map(createAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
return actions
|
return actions
|
||||||
}
|
}
|
||||||
else { // object[META].type === 'Object'
|
else { // object is an Object, we don't allow sorting properties
|
||||||
|
return []
|
||||||
const props = object[META].props
|
|
||||||
const createAction = ({item, beforeItem, fromIndex, toIndex}) => ({
|
|
||||||
op: 'move',
|
|
||||||
from: compileJSONPointer(path.concat(item)),
|
|
||||||
path: compileJSONPointer(path.concat(item)),
|
|
||||||
meta: {
|
|
||||||
before: beforeItem
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const actions = sortWithComparator(props, compare).map(createAction)
|
|
||||||
|
|
||||||
// when no order is provided, test whether ordering ascending
|
|
||||||
// changed anything. If not, sort descending
|
|
||||||
if (!order && isEmpty(actions)) {
|
|
||||||
return sortWithComparator(props, reverseCompare).map(createAction)
|
|
||||||
}
|
|
||||||
|
|
||||||
return actions
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -466,10 +396,10 @@ function sortWithComparator (items, comparator) {
|
||||||
* @return {Array | Object | string}
|
* @return {Array | Object | string}
|
||||||
*/
|
*/
|
||||||
export function createEntry (type) {
|
export function createEntry (type) {
|
||||||
if (type === 'Array') {
|
if (type === 'array') {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
else if (type === 'Object') {
|
else if (type === 'object') {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -503,7 +433,7 @@ export function convertType (value, type) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'Object') {
|
if (type === 'object') {
|
||||||
let object = {}
|
let object = {}
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
|
@ -513,7 +443,7 @@ export function convertType (value, type) {
|
||||||
return object
|
return object
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'Array') {
|
if (type === 'array') {
|
||||||
let array = []
|
let array = []
|
||||||
|
|
||||||
if (isObject(value)) {
|
if (isObject(value)) {
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
import { sort } from './actions'
|
import { sort } from './actions'
|
||||||
import { assertDeepEqualEson } from './utils/assertDeepEqualEson'
|
import { createAssertEqualEson } from './utils/assertEqualEson'
|
||||||
import {esonToJson, expandOne, jsonToEson, META} from './eson'
|
import { ID, syncEson } from './eson'
|
||||||
import {patchEson} from './patchEson'
|
import { immutableJsonPatch } from './immutableJsonPatch'
|
||||||
|
|
||||||
|
const assertEqualEson = createAssertEqualEson(expect)
|
||||||
|
|
||||||
// TODO: test changeValue
|
// TODO: test changeValue
|
||||||
// TODO: test changeProperty
|
// TODO: test changeProperty
|
||||||
|
@ -16,66 +18,28 @@ import {patchEson} from './patchEson'
|
||||||
// TODO: test removeAll
|
// TODO: test removeAll
|
||||||
|
|
||||||
it('sort root Array', () => {
|
it('sort root Array', () => {
|
||||||
const eson = jsonToEson([1,3,2])
|
const eson = syncEson([1,3,2])
|
||||||
|
|
||||||
assertDeepEqualEson(patchEson(eson, sort(eson, [])).data, jsonToEson([1,2,3]))
|
assertEqualEson(immutableJsonPatch(eson, sort(eson, [])).json, syncEson([1,2,3]))
|
||||||
assertDeepEqualEson(patchEson(eson, sort(eson, [], 'asc')).data, jsonToEson([1,2,3]))
|
assertEqualEson(immutableJsonPatch(eson, sort(eson, [], 'asc')).json, syncEson([1,2,3]))
|
||||||
assertDeepEqualEson(patchEson(eson, sort(eson, [], 'desc')).data, jsonToEson([3,2,1]))
|
assertEqualEson(immutableJsonPatch(eson, sort(eson, [], 'desc')).json, syncEson([3,2,1]))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sort nested Array', () => {
|
it('sort nested Array', () => {
|
||||||
const eson = jsonToEson({arr: [4,1,8,5,3,9,2,7,6]})
|
const eson = syncEson({arr: [4,1,8,5,3,9,2,7,6]})
|
||||||
const actual = patchEson(eson, sort(eson, ['arr'])).data
|
const actual = immutableJsonPatch(eson, sort(eson, ['arr'])).json
|
||||||
const expected = jsonToEson({arr: [1,2,3,4,5,6,7,8,9]})
|
const expected = syncEson({arr: [1,2,3,4,5,6,7,8,9]})
|
||||||
assertDeepEqualEson(actual, expected)
|
assertEqualEson(actual, expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sort nested Array reverse order', () => {
|
it('sort nested Array reverse order', () => {
|
||||||
// no order provided -> order ascending, but if nothing changes, order descending
|
// no order provided -> order ascending, but if nothing changes, order descending
|
||||||
const eson = jsonToEson({arr: [1,2,3,4,5,6,7,8,9]})
|
const eson = syncEson({arr: [1,2,3,4,5,6,7,8,9]})
|
||||||
const actual = patchEson(eson, sort(eson, ['arr'])).data
|
const actual = immutableJsonPatch(eson, sort(eson, ['arr'])).json
|
||||||
const expected = jsonToEson({arr: [9,8,7,6,5,4,3,2,1]})
|
const expected = syncEson({arr: [9,8,7,6,5,4,3,2,1]})
|
||||||
assertDeepEqualEson(actual, expected)
|
assertEqualEson(actual, expected)
|
||||||
|
|
||||||
// id's and META should be the same
|
// id's and META should be the same
|
||||||
expect(actual.arr[META].id).toEqual(eson.arr[META].id)
|
expect(actual.arr[ID]).toEqual(eson.arr[ID])
|
||||||
expect(actual.arr[7][META].id).toEqual(eson.arr[1][META].id)
|
expect(actual.arr[7][ID]).toEqual(eson.arr[1][ID])
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
it('sort root Object', () => {
|
|
||||||
const eson = jsonToEson({c: 2, b: 3, a:4})
|
|
||||||
|
|
||||||
expect(patchEson(eson, sort(eson, [])).data[META].props).toEqual(['a', 'b', 'c'])
|
|
||||||
expect(patchEson(eson, sort(eson, [], 'asc')).data[META].props).toEqual(['a', 'b', 'c'])
|
|
||||||
expect(patchEson(eson, sort(eson, [], 'desc')).data[META].props).toEqual(['c', 'b', 'a'])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('sort nested Object', () => {
|
|
||||||
const eson = jsonToEson({obj: {c: 2, b: 3, a:4}})
|
|
||||||
eson.obj[META].expanded = true
|
|
||||||
eson.obj.c[META].expanded = true
|
|
||||||
|
|
||||||
const actual = patchEson(eson, sort(eson, ['obj'])).data
|
|
||||||
|
|
||||||
// should keep META data
|
|
||||||
expect(actual.obj[META].props).toEqual(['a', 'b', 'c'])
|
|
||||||
expect(actual.obj[META].expanded).toEqual(true)
|
|
||||||
expect(actual.obj.c[META].expanded).toEqual(true)
|
|
||||||
expect(actual.obj[META].id).toEqual(eson.obj[META].id)
|
|
||||||
expect(actual.obj.a[META].id).toEqual(eson.obj.a[META].id)
|
|
||||||
expect(actual.obj.b[META].id).toEqual(eson.obj.b[META].id)
|
|
||||||
expect(actual.obj.c[META].id).toEqual(eson.obj.c[META].id)
|
|
||||||
|
|
||||||
// asc, desc
|
|
||||||
expect(patchEson(eson, sort(eson, ['obj'])).data.obj[META].props).toEqual(['a', 'b', 'c'])
|
|
||||||
expect(patchEson(eson, sort(eson, ['obj'], 'asc')).data.obj[META].props).toEqual(['a', 'b', 'c'])
|
|
||||||
expect(patchEson(eson, sort(eson, ['obj'], 'desc')).data.obj[META].props).toEqual(['c', 'b', 'a'])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('sort nested Object (larger)', () => {
|
|
||||||
const eson = jsonToEson({obj: {h:1, c:1, e:1, d:1, g:1, b:1, a:1, f:1}})
|
|
||||||
const actual = patchEson(eson, sort(eson, ['obj'])).data
|
|
||||||
|
|
||||||
expect(actual.obj[META].props).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'])
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -50,8 +50,6 @@ export default class JSONEditor extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChangeMode = (mode) => {
|
handleChangeMode = (mode) => {
|
||||||
console.log('changeMode', mode, this.props.onChangeMode)
|
|
||||||
|
|
||||||
if (this.props.onChangeMode) {
|
if (this.props.onChangeMode) {
|
||||||
this.props.onChangeMode(mode, this.props.mode)
|
this.props.onChangeMode(mode, this.props.mode)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,27 @@
|
||||||
import { createElement as h, PureComponent } from 'react'
|
import { createElement as h, PureComponent } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import initial from 'lodash/initial'
|
import initial from 'lodash/initial'
|
||||||
|
import naturalSort from 'javascript-natural-sort'
|
||||||
|
|
||||||
import FloatingMenu from './menu/FloatingMenu'
|
import FloatingMenu from './menu/FloatingMenu'
|
||||||
import { escapeHTML, unescapeHTML } from '../utils/stringUtils'
|
import { escapeHTML, unescapeHTML } from '../utils/stringUtils'
|
||||||
import { getInnerText, insideRect } from '../utils/domUtils'
|
import { getInnerText, insideRect } from '../utils/domUtils'
|
||||||
import { stringConvert, valueType, isUrl } from '../utils/typeUtils'
|
import { stringConvert, valueType, isUrl } from '../utils/typeUtils'
|
||||||
import {
|
import {
|
||||||
compileJSONPointer,
|
|
||||||
META,
|
|
||||||
SELECTED, SELECTED_START, SELECTED_END, SELECTED_AFTER, SELECTED_INSIDE,
|
SELECTED, SELECTED_START, SELECTED_END, SELECTED_AFTER, SELECTED_INSIDE,
|
||||||
SELECTED_FIRST, SELECTED_LAST
|
SELECTED_FIRST, SELECTED_LAST
|
||||||
} from '../eson'
|
} from '../eson'
|
||||||
|
import { compileJSONPointer } from '../jsonPointer'
|
||||||
|
import { ERROR, EXPANDED, ID, SEARCH_PROPERTY, SEARCH_VALUE, SELECTION, TYPE, VALUE } from '../eson'
|
||||||
|
|
||||||
export default class JSONNode extends PureComponent {
|
export default class JSONNode extends PureComponent {
|
||||||
static URL_TITLE = 'Ctrl+Click or Ctrl+Enter to open url'
|
static URL_TITLE = 'Ctrl+Click or Ctrl+Enter to open url'
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
parentPath: PropTypes.array,
|
||||||
prop: PropTypes.string, // in case of an object property
|
prop: PropTypes.string, // in case of an object property
|
||||||
value: PropTypes.oneOfType([ PropTypes.object, PropTypes.array ]).isRequired,
|
index: PropTypes.number, // in case of an array item
|
||||||
|
eson: PropTypes.any, // enriched JSON object: Object, Array, number, string, or null
|
||||||
|
|
||||||
emit: PropTypes.func.isRequired,
|
emit: PropTypes.func.isRequired,
|
||||||
findKeyBinding: PropTypes.func.isRequired,
|
findKeyBinding: PropTypes.func.isRequired,
|
||||||
|
@ -31,10 +34,17 @@ export default class JSONNode extends PureComponent {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
menu: null, // can contain object {anchor, root}
|
menu: null, // can contain object {anchor, root}
|
||||||
appendMenu: null, // can contain object {anchor, root}
|
appendMenu: null, // can contain object {anchor, root}
|
||||||
hover: null
|
hover: null,
|
||||||
|
path: this.props.parentPath
|
||||||
|
? this.props.parentPath.concat('index' in this.props ? this.props.index : this.props.prop)
|
||||||
|
: []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
|
@ -44,14 +54,12 @@ export default class JSONNode extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
// console.log('JSONNode.render ' + JSON.stringify(this.props.value[META].path))
|
if (this.props.eson[TYPE] === 'array') {
|
||||||
const type = this.props.value[META].type
|
|
||||||
if (type === 'Object') {
|
|
||||||
return this.renderJSONObject()
|
|
||||||
}
|
|
||||||
else if (type === 'Array') {
|
|
||||||
return this.renderJSONArray()
|
return this.renderJSONArray()
|
||||||
}
|
}
|
||||||
|
else if (this.props.eson[TYPE] === 'object') {
|
||||||
|
return this.renderJSONObject()
|
||||||
|
}
|
||||||
else { // no Object or Array
|
else { // no Object or Array
|
||||||
return this.renderJSONValue()
|
return this.renderJSONValue()
|
||||||
}
|
}
|
||||||
|
@ -59,8 +67,10 @@ export default class JSONNode extends PureComponent {
|
||||||
|
|
||||||
renderJSONObject () {
|
renderJSONObject () {
|
||||||
// TODO: refactor renderJSONObject (too large/complex)
|
// TODO: refactor renderJSONObject (too large/complex)
|
||||||
const meta = this.props.value[META]
|
const eson = this.props.eson
|
||||||
const props = meta.props
|
const jsonProps = Object.keys(eson).sort(naturalSort)
|
||||||
|
const jsonPropsCount = jsonProps.length
|
||||||
|
|
||||||
const nodeStart = h('div', {
|
const nodeStart = h('div', {
|
||||||
key: 'node',
|
key: 'node',
|
||||||
onKeyDown: this.handleKeyDown,
|
onKeyDown: this.handleKeyDown,
|
||||||
|
@ -71,26 +81,27 @@ export default class JSONNode extends PureComponent {
|
||||||
this.renderProperty(),
|
this.renderProperty(),
|
||||||
this.renderSeparator(),
|
this.renderSeparator(),
|
||||||
this.renderDelimiter('{', 'jsoneditor-delimiter-start'),
|
this.renderDelimiter('{', 'jsoneditor-delimiter-start'),
|
||||||
!meta.expanded
|
!this.props.eson[EXPANDED]
|
||||||
? [
|
? [
|
||||||
this.renderTag(`${props.length} ${props.length === 1 ? 'prop' : 'props'}`,
|
this.renderTag(`${jsonPropsCount} ${jsonPropsCount === 1 ? 'prop' : 'props'}`,
|
||||||
`Object containing ${props.length} ${props.length === 1 ? 'property' : 'properties'}`),
|
`Object containing ${jsonPropsCount} ${jsonPropsCount === 1 ? 'property' : 'properties'}`),
|
||||||
this.renderDelimiter('}', 'jsoneditor-delimiter-end jsoneditor-delimiter-collapsed'),
|
this.renderDelimiter('}', 'jsoneditor-delimiter-end jsoneditor-delimiter-collapsed'),
|
||||||
this.renderInsertAfter()
|
this.renderInsertAfter()
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
this.renderInsertBefore()
|
this.renderInsertBefore()
|
||||||
],
|
],
|
||||||
this.renderError(meta.error)
|
this.renderError(this.props.eson[ERROR])
|
||||||
])
|
])
|
||||||
|
|
||||||
let childs
|
let childs
|
||||||
if (meta.expanded) {
|
if (this.props.eson[EXPANDED]) {
|
||||||
if (props.length > 0) {
|
if (jsonPropsCount > 0) {
|
||||||
const propsChilds = props.map(prop => h(this.constructor, {
|
const propsChilds = jsonProps.map((prop) => h(this.constructor, {
|
||||||
key: this.props.value[prop][META].id,
|
key: eson[prop][ID],
|
||||||
|
parentPath: this.state.path,
|
||||||
prop,
|
prop,
|
||||||
value: this.props.value[prop],
|
eson: eson[prop],
|
||||||
emit: this.props.emit,
|
emit: this.props.emit,
|
||||||
findKeyBinding: this.props.findKeyBinding,
|
findKeyBinding: this.props.findKeyBinding,
|
||||||
options: this.props.options
|
options: this.props.options
|
||||||
|
@ -105,8 +116,9 @@ export default class JSONNode extends PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const floatingMenu = this.renderFloatingMenu('Object', meta.selected)
|
// FIXME
|
||||||
const nodeEnd = meta.expanded
|
const floatingMenu = this.renderFloatingMenu('object', this.props.eson[SELECTION])
|
||||||
|
const nodeEnd = this.props.eson[EXPANDED]
|
||||||
? h('div', {key: 'node-end', className: 'jsoneditor-node-end', 'data-area': 'empty'}, [
|
? h('div', {key: 'node-end', className: 'jsoneditor-node-end', 'data-area': 'empty'}, [
|
||||||
this.renderDelimiter('}', 'jsoneditor-delimiter-end'),
|
this.renderDelimiter('}', 'jsoneditor-delimiter-end'),
|
||||||
this.renderInsertAfter()
|
this.renderInsertAfter()
|
||||||
|
@ -114,9 +126,9 @@ export default class JSONNode extends PureComponent {
|
||||||
: null
|
: null
|
||||||
|
|
||||||
return h('div', {
|
return h('div', {
|
||||||
'data-path': compileJSONPointer(meta.path),
|
'data-path': compileJSONPointer(this.state.path),
|
||||||
'data-area': 'empty',
|
'data-area': 'empty',
|
||||||
className: this.getContainerClassName(meta.selected, this.state.hover),
|
className: this.getContainerClassName(this.props.eson[SELECTION], this.state.hover),
|
||||||
// onMouseOver: this.handleMouseOver,
|
// onMouseOver: this.handleMouseOver,
|
||||||
// onMouseLeave: this.handleMouseLeave
|
// onMouseLeave: this.handleMouseLeave
|
||||||
}, [floatingMenu, nodeStart, childs, nodeEnd])
|
}, [floatingMenu, nodeStart, childs, nodeEnd])
|
||||||
|
@ -124,8 +136,7 @@ export default class JSONNode extends PureComponent {
|
||||||
|
|
||||||
renderJSONArray () {
|
renderJSONArray () {
|
||||||
// TODO: refactor renderJSONArray (too large/complex)
|
// TODO: refactor renderJSONArray (too large/complex)
|
||||||
const meta = this.props.value[META]
|
const count = this.props.eson.length
|
||||||
const count = this.props.value.length
|
|
||||||
const nodeStart = h('div', {
|
const nodeStart = h('div', {
|
||||||
key: 'node',
|
key: 'node',
|
||||||
onKeyDown: this.handleKeyDown,
|
onKeyDown: this.handleKeyDown,
|
||||||
|
@ -135,25 +146,27 @@ export default class JSONNode extends PureComponent {
|
||||||
this.renderProperty(),
|
this.renderProperty(),
|
||||||
this.renderSeparator(),
|
this.renderSeparator(),
|
||||||
this.renderDelimiter('[', 'jsoneditor-delimiter-start'),
|
this.renderDelimiter('[', 'jsoneditor-delimiter-start'),
|
||||||
!meta.expanded
|
!this.props.eson[EXPANDED]
|
||||||
? [
|
? [
|
||||||
this.renderTag(`${count} ${count === 1 ? 'item' : 'items'}`,
|
this.renderTag(`${count} ${count === 1 ? 'item' : 'items'}`,
|
||||||
`Array containing ${count} item${count === 1 ? 'item' : 'items'}`),
|
`Array containing ${count} ${count === 1 ? 'item' : 'items'}`),
|
||||||
this.renderDelimiter(']', 'jsoneditor-delimiter-end jsoneditor-delimiter-collapsed'),
|
this.renderDelimiter(']', 'jsoneditor-delimiter-end jsoneditor-delimiter-collapsed'),
|
||||||
this.renderInsertAfter(),
|
this.renderInsertAfter(),
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
this.renderInsertBefore()
|
this.renderInsertBefore()
|
||||||
],
|
],
|
||||||
this.renderError(meta.error)
|
this.renderError(this.props.eson[ERROR])
|
||||||
])
|
])
|
||||||
|
|
||||||
let childs
|
let childs
|
||||||
if (meta.expanded) {
|
if (this.props.eson[EXPANDED]) {
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
const items = this.props.value.map(item => h(this.constructor, {
|
const items = this.props.eson.map((item, index) => h(this.constructor, {
|
||||||
key : item[META].id,
|
key: item[ID],
|
||||||
value: item,
|
parentPath: this.state.path,
|
||||||
|
index,
|
||||||
|
eson: item,
|
||||||
options: this.props.options,
|
options: this.props.options,
|
||||||
emit: this.props.emit,
|
emit: this.props.emit,
|
||||||
findKeyBinding: this.props.findKeyBinding
|
findKeyBinding: this.props.findKeyBinding
|
||||||
|
@ -168,8 +181,8 @@ export default class JSONNode extends PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const floatingMenu = this.renderFloatingMenu('Array', meta.selected)
|
const floatingMenu = this.renderFloatingMenu('array', this.props.eson[SELECTION])
|
||||||
const nodeEnd = meta.expanded
|
const nodeEnd = this.props.eson[EXPANDED]
|
||||||
? h('div', {key: 'node-end', className: 'jsoneditor-node-end', 'data-area': 'empty'}, [
|
? h('div', {key: 'node-end', className: 'jsoneditor-node-end', 'data-area': 'empty'}, [
|
||||||
this.renderDelimiter(']', 'jsoneditor-delimiter-end'),
|
this.renderDelimiter(']', 'jsoneditor-delimiter-end'),
|
||||||
this.renderInsertAfter()
|
this.renderInsertAfter()
|
||||||
|
@ -177,16 +190,15 @@ export default class JSONNode extends PureComponent {
|
||||||
: null
|
: null
|
||||||
|
|
||||||
return h('div', {
|
return h('div', {
|
||||||
'data-path': compileJSONPointer(meta.path),
|
'data-path': compileJSONPointer(this.state.path),
|
||||||
'data-area': 'empty',
|
'data-area': 'empty',
|
||||||
className: this.getContainerClassName(meta.selected, this.state.hover),
|
className: this.getContainerClassName(this.props.eson[SELECTION], this.state.hover),
|
||||||
// onMouseOver: this.handleMouseOver,
|
// onMouseOver: this.handleMouseOver,
|
||||||
// onMouseLeave: this.handleMouseLeave
|
// onMouseLeave: this.handleMouseLeave
|
||||||
}, [floatingMenu, nodeStart, childs, nodeEnd])
|
}, [floatingMenu, nodeStart, childs, nodeEnd])
|
||||||
}
|
}
|
||||||
|
|
||||||
renderJSONValue () {
|
renderJSONValue () {
|
||||||
const meta = this.props.value[META]
|
|
||||||
const node = h('div', {
|
const node = h('div', {
|
||||||
key: 'node',
|
key: 'node',
|
||||||
onKeyDown: this.handleKeyDown,
|
onKeyDown: this.handleKeyDown,
|
||||||
|
@ -195,19 +207,19 @@ export default class JSONNode extends PureComponent {
|
||||||
this.renderPlaceholder(),
|
this.renderPlaceholder(),
|
||||||
this.renderProperty(),
|
this.renderProperty(),
|
||||||
this.renderSeparator(),
|
this.renderSeparator(),
|
||||||
this.renderValue(meta.value, meta.searchValue, this.props.options),
|
this.renderValue(this.props.eson[VALUE], this.props.eson[SEARCH_VALUE], this.props.options), // FIXME
|
||||||
this.renderInsertAfter(),
|
this.renderInsertAfter(),
|
||||||
this.renderError(meta.error)
|
this.renderError(this.props.eson[ERROR])
|
||||||
])
|
])
|
||||||
|
|
||||||
const floatingMenu = this.renderFloatingMenu('value', meta.selected)
|
const floatingMenu = this.renderFloatingMenu('value', this.props.eson[SELECTION])
|
||||||
|
|
||||||
// const insertArea = this.renderInsertBeforeArea()
|
// const insertArea = this.renderInsertBeforeArea()
|
||||||
|
|
||||||
return h('div', {
|
return h('div', {
|
||||||
'data-path': compileJSONPointer(meta.path),
|
'data-path': compileJSONPointer(this.state.path),
|
||||||
'data-area': 'empty',
|
'data-area': 'empty',
|
||||||
className: this.getContainerClassName(meta.selected, this.state.hover),
|
className: this.getContainerClassName(this.props.eson[SELECTION], this.state.hover),
|
||||||
// onMouseOver: this.handleMouseOver,
|
// onMouseOver: this.handleMouseOver,
|
||||||
// onMouseLeave: this.handleMouseLeave
|
// onMouseLeave: this.handleMouseLeave
|
||||||
}, [node, floatingMenu])
|
}, [node, floatingMenu])
|
||||||
|
@ -238,7 +250,7 @@ export default class JSONNode extends PureComponent {
|
||||||
*/
|
*/
|
||||||
renderAppend (text) {
|
renderAppend (text) {
|
||||||
return h('div', {
|
return h('div', {
|
||||||
'data-path': compileJSONPointer(this.props.value[META].path) + '/-',
|
'data-path': compileJSONPointer(this.state.path) + '/-',
|
||||||
'data-area': 'empty',
|
'data-area': 'empty',
|
||||||
className: 'jsoneditor-node',
|
className: 'jsoneditor-node',
|
||||||
onKeyDown: this.handleKeyDownAppend
|
onKeyDown: this.handleKeyDownAppend
|
||||||
|
@ -287,10 +299,10 @@ export default class JSONNode extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
const editable = !this.props.options.isPropertyEditable ||
|
const editable = !this.props.options.isPropertyEditable ||
|
||||||
this.props.options.isPropertyEditable(this.props.value[META].path)
|
this.props.options.isPropertyEditable(this.state.path)
|
||||||
|
|
||||||
const emptyClassName = (this.props.prop != null && this.props.prop.length === 0) ? ' jsoneditor-empty' : ''
|
const emptyClassName = (this.props.prop != null && this.props.prop.length === 0) ? ' jsoneditor-empty' : ''
|
||||||
const searchClassName = this.props.prop != null ? JSONNode.getSearchResultClass(this.props.value[META].searchProperty) : ''
|
const searchClassName = this.props.prop != null ? JSONNode.getSearchResultClass(this.props.eson[SEARCH_PROPERTY]) : ''
|
||||||
const escapedPropName = this.props.prop != null ? escapeHTML(this.props.prop, this.props.options.escapeUnicode) : null
|
const escapedPropName = this.props.prop != null ? escapeHTML(this.props.prop, this.props.options.escapeUnicode) : null
|
||||||
|
|
||||||
if (editable) {
|
if (editable) {
|
||||||
|
@ -341,7 +353,7 @@ export default class JSONNode extends PureComponent {
|
||||||
const itsAnUrl = isUrl(value)
|
const itsAnUrl = isUrl(value)
|
||||||
const isEmpty = escapedValue.length === 0
|
const isEmpty = escapedValue.length === 0
|
||||||
|
|
||||||
const editable = !options.isValueEditable || options.isValueEditable(this.props.value[META].path)
|
const editable = !options.isValueEditable || options.isValueEditable(this.state.path)
|
||||||
if (editable) {
|
if (editable) {
|
||||||
return h('div', {
|
return h('div', {
|
||||||
key: 'value',
|
key: 'value',
|
||||||
|
@ -460,7 +472,7 @@ export default class JSONNode extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
target.className = JSONNode.getValueClass(type, itsAnUrl, isEmpty) +
|
target.className = JSONNode.getValueClass(type, itsAnUrl, isEmpty) +
|
||||||
JSONNode.getSearchResultClass(this.props.value[META].searchValue)
|
JSONNode.getSearchResultClass(this.props.eson[SEARCH_VALUE])
|
||||||
target.title = itsAnUrl ? JSONNode.URL_TITLE : ''
|
target.title = itsAnUrl ? JSONNode.URL_TITLE : ''
|
||||||
|
|
||||||
// remove all classNames from childs (needed for IE and Edge)
|
// remove all classNames from childs (needed for IE and Edge)
|
||||||
|
@ -514,7 +526,7 @@ export default class JSONNode extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderExpandButton () {
|
renderExpandButton () {
|
||||||
const className = `jsoneditor-button jsoneditor-${this.props.value[META].expanded ? 'expanded' : 'collapsed'}`
|
const className = `jsoneditor-button jsoneditor-${this.props.eson[EXPANDED] ? 'expanded' : 'collapsed'}`
|
||||||
|
|
||||||
return h('div', {key: 'expand', className: 'jsoneditor-button-container'},
|
return h('div', {key: 'expand', className: 'jsoneditor-button-container'},
|
||||||
h('button', {
|
h('button', {
|
||||||
|
@ -541,7 +553,7 @@ export default class JSONNode extends PureComponent {
|
||||||
|
|
||||||
return h(FloatingMenu, {
|
return h(FloatingMenu, {
|
||||||
key: 'floating-menu',
|
key: 'floating-menu',
|
||||||
path: this.props.value[META].path,
|
path: this.state.path,
|
||||||
emit: this.props.emit,
|
emit: this.props.emit,
|
||||||
items: this.getFloatingMenuItems(type, selected),
|
items: this.getFloatingMenuItems(type, selected),
|
||||||
position: isLastOfMultiple || isAfter ? 'bottom' : 'top'
|
position: isLastOfMultiple || isAfter ? 'bottom' : 'top'
|
||||||
|
@ -569,7 +581,7 @@ export default class JSONNode extends PureComponent {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'Object') {
|
if (type === 'object') {
|
||||||
return [
|
return [
|
||||||
{type: 'sort'},
|
{type: 'sort'},
|
||||||
{type: 'duplicate'},
|
{type: 'duplicate'},
|
||||||
|
@ -580,7 +592,7 @@ export default class JSONNode extends PureComponent {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'Array') {
|
if (type === 'array') {
|
||||||
return [
|
return [
|
||||||
{type: 'sort'},
|
{type: 'sort'},
|
||||||
{type: 'duplicate'},
|
{type: 'duplicate'},
|
||||||
|
@ -636,12 +648,12 @@ export default class JSONNode extends PureComponent {
|
||||||
static getRootName (value, options) {
|
static getRootName (value, options) {
|
||||||
return typeof options.name === 'string'
|
return typeof options.name === 'string'
|
||||||
? options.name
|
? options.name
|
||||||
: value[META].type
|
: value[TYPE]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @private */
|
/** @private */
|
||||||
handleChangeProperty = (event) => {
|
handleChangeProperty = (event) => {
|
||||||
const parentPath = initial(this.props.value[META].path)
|
const parentPath = initial(this.state.path)
|
||||||
const oldProp = this.props.prop
|
const oldProp = this.props.prop
|
||||||
const newProp = unescapeHTML(getInnerText(event.target))
|
const newProp = unescapeHTML(getInnerText(event.target))
|
||||||
|
|
||||||
|
@ -653,9 +665,9 @@ export default class JSONNode extends PureComponent {
|
||||||
/** @private */
|
/** @private */
|
||||||
handleChangeValue = (event) => {
|
handleChangeValue = (event) => {
|
||||||
const value = this.getValueFromEvent(event)
|
const value = this.getValueFromEvent(event)
|
||||||
const path = this.props.value[META].path
|
const path = this.state.path
|
||||||
|
|
||||||
if (value !== this.props.value[META].value) {
|
if (value !== this.props.eson[VALUE]) {
|
||||||
this.props.emit('changeValue', {path, value})
|
this.props.emit('changeValue', {path, value})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -670,7 +682,7 @@ export default class JSONNode extends PureComponent {
|
||||||
/** @private */
|
/** @private */
|
||||||
handleKeyDown = (event) => {
|
handleKeyDown = (event) => {
|
||||||
const keyBinding = this.props.findKeyBinding(event)
|
const keyBinding = this.props.findKeyBinding(event)
|
||||||
const path = this.props.value[META].path
|
const path = this.state.path
|
||||||
|
|
||||||
if (keyBinding === 'duplicate') {
|
if (keyBinding === 'duplicate') {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
@ -690,7 +702,7 @@ export default class JSONNode extends PureComponent {
|
||||||
if (keyBinding === 'expand') {
|
if (keyBinding === 'expand') {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
const recurse = false
|
const recurse = false
|
||||||
const expanded = !this.props.value[META].expanded
|
const expanded = !this.props.eson[EXPANDED]
|
||||||
this.props.emit('expand', {path, expanded, recurse})
|
this.props.emit('expand', {path, expanded, recurse})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -703,7 +715,7 @@ export default class JSONNode extends PureComponent {
|
||||||
/** @private */
|
/** @private */
|
||||||
handleKeyDownAppend = (event) => {
|
handleKeyDownAppend = (event) => {
|
||||||
const keyBinding = this.props.findKeyBinding(event)
|
const keyBinding = this.props.findKeyBinding(event)
|
||||||
const path = this.props.value[META].path
|
const path = this.state.path
|
||||||
|
|
||||||
if (keyBinding === 'insert') {
|
if (keyBinding === 'insert') {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
@ -728,8 +740,8 @@ export default class JSONNode extends PureComponent {
|
||||||
/** @private */
|
/** @private */
|
||||||
handleExpand = (event) => {
|
handleExpand = (event) => {
|
||||||
const recurse = event.ctrlKey
|
const recurse = event.ctrlKey
|
||||||
const path = this.props.value[META].path
|
const path = this.state.path
|
||||||
const expanded = !this.props.value[META].expanded
|
const expanded = !this.props.eson[EXPANDED]
|
||||||
|
|
||||||
this.props.emit('expand', {path, expanded, recurse})
|
this.props.emit('expand', {path, expanded, recurse})
|
||||||
}
|
}
|
||||||
|
@ -758,7 +770,7 @@ export default class JSONNode extends PureComponent {
|
||||||
*/
|
*/
|
||||||
getValueFromEvent (event) {
|
getValueFromEvent (event) {
|
||||||
const stringValue = unescapeHTML(getInnerText(event.target))
|
const stringValue = unescapeHTML(getInnerText(event.target))
|
||||||
return this.props.value[META].type === 'string'
|
return this.state.type === 'string' // FIXME
|
||||||
? stringValue
|
? stringValue
|
||||||
: stringConvert(stringValue)
|
: stringConvert(stringValue)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,11 @@ import Ajv from 'ajv'
|
||||||
import { parseJSON } from '../utils/jsonUtils'
|
import { parseJSON } from '../utils/jsonUtils'
|
||||||
import { escapeUnicodeChars } from '../utils/stringUtils'
|
import { escapeUnicodeChars } from '../utils/stringUtils'
|
||||||
import { enrichSchemaError, limitErrors } from '../utils/schemaUtils'
|
import { enrichSchemaError, limitErrors } from '../utils/schemaUtils'
|
||||||
import { jsonToEson, esonToJson } from '../eson'
|
|
||||||
import { patchEson } from '../patchEson'
|
|
||||||
import { createFindKeyBinding } from '../utils/keyBindings'
|
import { createFindKeyBinding } from '../utils/keyBindings'
|
||||||
import { KEY_BINDINGS } from '../constants'
|
import { KEY_BINDINGS } from '../constants'
|
||||||
|
|
||||||
import ModeButton from './menu/ModeButton'
|
import ModeButton from './menu/ModeButton'
|
||||||
|
import { immutableJsonPatch } from '../immutableJsonPatch'
|
||||||
|
|
||||||
const AJV_OPTIONS = {
|
const AJV_OPTIONS = {
|
||||||
allErrors: true,
|
allErrors: true,
|
||||||
|
@ -331,10 +330,9 @@ export default class TextMode extends Component {
|
||||||
patch (actions) {
|
patch (actions) {
|
||||||
const json = this.get()
|
const json = this.get()
|
||||||
|
|
||||||
const data = jsonToEson(json)
|
const result = immutableJsonPatch(json, actions)
|
||||||
const result = patchEson(data, actions)
|
|
||||||
|
|
||||||
this.set(esonToJson(result.data))
|
this.set(result.data)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
patch: actions,
|
patch: actions,
|
||||||
|
|
|
@ -8,21 +8,24 @@ import Hammer from 'react-hammerjs'
|
||||||
import jump from '../assets/jump.js/src/jump'
|
import jump from '../assets/jump.js/src/jump'
|
||||||
import Ajv from 'ajv'
|
import Ajv from 'ajv'
|
||||||
|
|
||||||
import { getIn, updateIn } from '../utils/immutabilityHelpers'
|
import { existsIn, setIn, updateIn } from '../utils/immutabilityHelpers'
|
||||||
import { parseJSON } from '../utils/jsonUtils'
|
import { parseJSON } from '../utils/jsonUtils'
|
||||||
import { enrichSchemaError } from '../utils/schemaUtils'
|
import { enrichSchemaError } from '../utils/schemaUtils'
|
||||||
|
import { compileJSONPointer, parseJSONPointer } from '../jsonPointer'
|
||||||
import {
|
import {
|
||||||
META,
|
append,
|
||||||
jsonToEson, esonToJson, pathExists,
|
changeProperty,
|
||||||
expand, expandOne, expandPath, applyErrors,
|
changeType,
|
||||||
search, nextSearchResult, previousSearchResult,
|
changeValue,
|
||||||
applySelection, pathsFromSelection, contentsFromPaths,
|
createEntry,
|
||||||
compileJSONPointer, parseJSONPointer
|
duplicate,
|
||||||
} from '../eson'
|
insertAfter,
|
||||||
import { patchEson } from '../patchEson'
|
insertBefore,
|
||||||
import {
|
insertInside,
|
||||||
duplicate, insertBefore, insertAfter, insertInside, append, remove, removeAll, replace,
|
remove,
|
||||||
createEntry, changeType, changeValue, changeProperty, sort
|
removeAll,
|
||||||
|
replace,
|
||||||
|
sort
|
||||||
} from '../actions'
|
} from '../actions'
|
||||||
import JSONNode from './JSONNode'
|
import JSONNode from './JSONNode'
|
||||||
import JSONNodeView from './JSONNodeView'
|
import JSONNodeView from './JSONNodeView'
|
||||||
|
@ -30,11 +33,32 @@ import JSONNodeForm from './JSONNodeForm'
|
||||||
import ModeButton from './menu/ModeButton'
|
import ModeButton from './menu/ModeButton'
|
||||||
import Search from './menu/Search'
|
import Search from './menu/Search'
|
||||||
import {
|
import {
|
||||||
moveUp, moveDown, moveLeft, moveRight, moveDownSibling, moveHome, moveEnd,
|
findBaseNode,
|
||||||
findNode, findBaseNode, selectFind, searchHasFocus, setSelection
|
findNode,
|
||||||
|
moveDown,
|
||||||
|
moveDownSibling,
|
||||||
|
moveEnd,
|
||||||
|
moveHome,
|
||||||
|
moveLeft,
|
||||||
|
moveRight,
|
||||||
|
moveUp,
|
||||||
|
searchHasFocus,
|
||||||
|
selectFind,
|
||||||
|
setSelection
|
||||||
} from './utils/domSelector'
|
} from './utils/domSelector'
|
||||||
import { createFindKeyBinding } from '../utils/keyBindings'
|
import { createFindKeyBinding } from '../utils/keyBindings'
|
||||||
import { KEY_BINDINGS } from '../constants'
|
import { KEY_BINDINGS } from '../constants'
|
||||||
|
import { immutableJsonPatch } from '../immutableJsonPatch'
|
||||||
|
import {
|
||||||
|
applyErrors, applySelection, contentsFromPaths,
|
||||||
|
expand,
|
||||||
|
EXPANDED,
|
||||||
|
expandPath,
|
||||||
|
nextSearchResult, pathsFromSelection, previousSearchResult,
|
||||||
|
search,
|
||||||
|
syncEson,
|
||||||
|
toEsonPatchAction
|
||||||
|
} from '../eson'
|
||||||
|
|
||||||
const AJV_OPTIONS = {
|
const AJV_OPTIONS = {
|
||||||
allErrors: true,
|
allErrors: true,
|
||||||
|
@ -56,12 +80,9 @@ export default class TreeMode extends PureComponent {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
// const json = this.props.json || {}
|
|
||||||
// const expandCallback = this.props.expand || TreeMode.expandRoot
|
|
||||||
// const eson = expand(jsonToEson(json), expandCallback)
|
|
||||||
|
|
||||||
const json = {}
|
const json = {}
|
||||||
const eson = jsonToEson(json)
|
const expandCallback = this.props.expand || TreeMode.expandRoot
|
||||||
|
const eson = expand(syncEson(json, {}), expandCallback)
|
||||||
|
|
||||||
this.keyDownActions = {
|
this.keyDownActions = {
|
||||||
'up': this.moveUp,
|
'up': this.moveUp,
|
||||||
|
@ -155,14 +176,11 @@ export default class TreeMode extends PureComponent {
|
||||||
|
|
||||||
// Apply json
|
// Apply json
|
||||||
if (nextProps.json !== this.state.json) {
|
if (nextProps.json !== this.state.json) {
|
||||||
// FIXME: merge meta data from existing eson
|
|
||||||
const callback = this.props.expand || TreeMode.expandRoot
|
|
||||||
const json = nextProps.json
|
const json = nextProps.json
|
||||||
const eson = expand(jsonToEson(json), callback)
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
json,
|
json,
|
||||||
eson
|
eson: syncEson(json, this.state.eson)
|
||||||
})
|
})
|
||||||
// FIXME: use patch again -> patch should keep existing meta data when for the unchanged parts of the json
|
// FIXME: use patch again -> patch should keep existing meta data when for the unchanged parts of the json
|
||||||
// this.patch([{
|
// this.patch([{
|
||||||
|
@ -231,9 +249,10 @@ export default class TreeMode extends PureComponent {
|
||||||
onMouseDown: this.handleTouchStart,
|
onMouseDown: this.handleTouchStart,
|
||||||
onTouchStart: this.handleTouchStart,
|
onTouchStart: this.handleTouchStart,
|
||||||
className: 'jsoneditor-list jsoneditor-root' +
|
className: 'jsoneditor-list jsoneditor-root' +
|
||||||
(eson[META].selected ? ' jsoneditor-selected' : '')},
|
(/*eson[META].selected*/ false ? ' jsoneditor-selected' : '')}, // FIXME
|
||||||
h(Node, {
|
h(Node, {
|
||||||
value: eson,
|
path: [],
|
||||||
|
eson,
|
||||||
emit: this.emitter.emit,
|
emit: this.emitter.emit,
|
||||||
findKeyBinding: this.findKeyBinding,
|
findKeyBinding: this.findKeyBinding,
|
||||||
options: this.state.options
|
options: this.state.options
|
||||||
|
@ -603,7 +622,7 @@ export default class TreeMode extends PureComponent {
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.setState({
|
this.setState({
|
||||||
eson: expandOne(this.state.eson, path, expanded)
|
eson: setIn(this.state.eson, path.concat(EXPANDED), expanded)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -626,6 +645,7 @@ export default class TreeMode extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSearch = (text) => {
|
handleSearch = (text) => {
|
||||||
|
// FIXME
|
||||||
// FIXME: also apply search when eson is changed
|
// FIXME: also apply search when eson is changed
|
||||||
const { eson, searchResult } = search(this.state.eson, text)
|
const { eson, searchResult } = search(this.state.eson, text)
|
||||||
if (searchResult.matches.length > 0) {
|
if (searchResult.matches.length > 0) {
|
||||||
|
@ -639,7 +659,7 @@ export default class TreeMode extends PureComponent {
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.setState({
|
this.setState({
|
||||||
eson,
|
eson: eson,
|
||||||
searchResult
|
searchResult
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -657,7 +677,7 @@ export default class TreeMode extends PureComponent {
|
||||||
const { eson, searchResult } = nextSearchResult(this.state.eson, this.state.searchResult)
|
const { eson, searchResult } = nextSearchResult(this.state.eson, this.state.searchResult)
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
eson,
|
eson: expandPath(eson, initial(searchResult.active.path)),
|
||||||
searchResult
|
searchResult
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -682,7 +702,7 @@ export default class TreeMode extends PureComponent {
|
||||||
const { eson, searchResult } = previousSearchResult(this.state.eson, this.state.searchResult)
|
const { eson, searchResult } = previousSearchResult(this.state.eson, this.state.searchResult)
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
eson,
|
eson: expandPath(eson, initial(searchResult.active.path)),
|
||||||
searchResult
|
searchResult
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -700,15 +720,15 @@ export default class TreeMode extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply a ESONPatch to the current JSON document and emit a change event
|
* Apply a JSONPatch to the current JSON document and emit a change event
|
||||||
* @param {ESONPatch} actions
|
* @param {JSONPatch} actions
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
handlePatch = (actions) => {
|
handlePatch = (actions) => {
|
||||||
// apply changes
|
// apply changes
|
||||||
const result = this.patch(actions)
|
const result = this.patch(actions)
|
||||||
|
|
||||||
this.emitOnChange (actions, result.revert, result.eson, result.json)
|
this.emitOnChange (actions, result.revert, result.json)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTouchStart = (event) => {
|
handleTouchStart = (event) => {
|
||||||
|
@ -717,15 +737,15 @@ export default class TreeMode extends PureComponent {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const pointer = this.findESONPointerFromElement(event.target)
|
const pointer = this.findJSONPointerFromElement(event.target)
|
||||||
const clickedOnEmptySpace = (event.target.nodeName === 'DIV') &&
|
const clickedOnEmptySpace = (event.target.nodeName === 'DIV') &&
|
||||||
(event.target.contentEditable !== 'true')
|
(event.target.contentEditable !== 'true')
|
||||||
|
|
||||||
// TODO: cleanup
|
// TODO: cleanup
|
||||||
// console.log('handleTouchStart', clickedOnEmptySpace && pointer, pointer && this.selectionFromESONPointer(pointer))
|
// console.log('handleTouchStart', clickedOnEmptySpace && pointer, pointer && this.selectionFromJSONPointer(pointer))
|
||||||
|
|
||||||
if (clickedOnEmptySpace && pointer) {
|
if (clickedOnEmptySpace && pointer) {
|
||||||
this.setState({ selection: this.selectionFromESONPointer(pointer)})
|
this.setState({ selection: this.selectionFromJSONPointer(pointer)})
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.setState({ selection: null })
|
this.setState({ selection: null })
|
||||||
|
@ -780,11 +800,11 @@ export default class TreeMode extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find ESON pointer from an HTML element
|
* Find JSON pointer from an HTML element
|
||||||
* @param {Element} element
|
* @param {Element} element
|
||||||
* @return {ESONPointer | null}
|
* @return {ESONPointer | null}
|
||||||
*/
|
*/
|
||||||
findESONPointerFromElement (element) {
|
findJSONPointerFromElement (element) {
|
||||||
const path = this.findDataPathFromElement(element)
|
const path = this.findDataPathFromElement(element)
|
||||||
const area = (element && element.getAttribute && element.getAttribute('data-area')) || null
|
const area = (element && element.getAttribute && element.getAttribute('data-area')) || null
|
||||||
|
|
||||||
|
@ -792,11 +812,11 @@ export default class TreeMode extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get selection from an ESON pointer
|
* Get selection from an JSONPointer
|
||||||
* @param {ESONPointer} pointer
|
* @param {ESONPointer} pointer
|
||||||
* @return {Selection}
|
* @return {Selection}
|
||||||
*/
|
*/
|
||||||
selectionFromESONPointer (pointer) {
|
selectionFromJSONPointer (pointer) {
|
||||||
// FIXME: does pointer have .area === 'after' ? if so adjust type defs
|
// FIXME: does pointer have .area === 'after' ? if so adjust type defs
|
||||||
if (pointer.area === 'after') {
|
if (pointer.area === 'after') {
|
||||||
return {after: pointer.path}
|
return {after: pointer.path}
|
||||||
|
@ -836,10 +856,9 @@ export default class TreeMode extends PureComponent {
|
||||||
* @private
|
* @private
|
||||||
* @param {ESONPatch} patch
|
* @param {ESONPatch} patch
|
||||||
* @param {ESONPatch} revert
|
* @param {ESONPatch} revert
|
||||||
* @param {ESON} eson
|
|
||||||
* @param {JSON} json
|
* @param {JSON} json
|
||||||
*/
|
*/
|
||||||
emitOnChange (patch, revert, eson, json) {
|
emitOnChange (patch, revert, json) {
|
||||||
const onPatch = this.props.onPatch
|
const onPatch = this.props.onPatch
|
||||||
if (onPatch) {
|
if (onPatch) {
|
||||||
setTimeout(() => onPatch(patch, revert))
|
setTimeout(() => onPatch(patch, revert))
|
||||||
|
@ -885,16 +904,18 @@ export default class TreeMode extends PureComponent {
|
||||||
const historyIndex = this.state.historyIndex
|
const historyIndex = this.state.historyIndex
|
||||||
const historyItem = history[historyIndex]
|
const historyItem = history[historyIndex]
|
||||||
|
|
||||||
const result = patchEson(this.state.eson, historyItem.undo)
|
const jsonResult = immutableJsonPatch(this.state.json, historyItem.undo)
|
||||||
|
const esonResult = immutableJsonPatch(this.state.eson, historyItem.undo.map(toEsonPatchAction))
|
||||||
|
|
||||||
// FIXME: apply search
|
// FIXME: apply search
|
||||||
this.setState({
|
this.setState({
|
||||||
eson: result.data,
|
json: jsonResult.json,
|
||||||
|
eson: esonResult.json,
|
||||||
history,
|
history,
|
||||||
historyIndex: historyIndex + 1
|
historyIndex: historyIndex + 1
|
||||||
})
|
})
|
||||||
|
|
||||||
this.emitOnChange(historyItem.undo, historyItem.redo, result.data, esonToJson(result.data))
|
this.emitOnChange(historyItem.undo, historyItem.redo, jsonResult.json)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -904,45 +925,43 @@ export default class TreeMode extends PureComponent {
|
||||||
const historyIndex = this.state.historyIndex - 1
|
const historyIndex = this.state.historyIndex - 1
|
||||||
const historyItem = history[historyIndex]
|
const historyItem = history[historyIndex]
|
||||||
|
|
||||||
const result = patchEson(this.state.eson, historyItem.redo)
|
const jsonResult = immutableJsonPatch(this.state.json, historyItem.redo)
|
||||||
|
const esonResult = immutableJsonPatch(this.state.eson, historyItem.undo.map(toEsonPatchAction))
|
||||||
|
|
||||||
// FIXME: apply search
|
// FIXME: apply search
|
||||||
this.setState({
|
this.setState({
|
||||||
eson: result.data,
|
json: jsonResult.json,
|
||||||
|
eson: esonResult.json,
|
||||||
history,
|
history,
|
||||||
historyIndex
|
historyIndex
|
||||||
})
|
})
|
||||||
|
|
||||||
this.emitOnChange(historyItem.redo, historyItem.undo, result.data, esonToJson(result.data))
|
this.emitOnChange(historyItem.redo, historyItem.undo, jsonResult.json)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply a ESONPatch to the current JSON document
|
* Apply a JSONPatch to the current JSON document
|
||||||
* @param {ESONPatch} actions ESONPatch actions
|
* @param {JSONPatch} actions ESONPatch actions
|
||||||
* @param {ESONPatchOptions} [options] If no expand function is provided, the
|
* @return {Object} Returns a object result containing the
|
||||||
* expanded state will be kept as is for
|
|
||||||
* existing paths. New paths will be fully
|
|
||||||
* expanded.
|
|
||||||
* @return {ESONPatchAction} Returns a ESONPatch result containing the
|
|
||||||
* patch, a patch to revert the action, and
|
* patch, a patch to revert the action, and
|
||||||
* an error object which is null when successful
|
* an error object which is null when successful
|
||||||
*/
|
*/
|
||||||
patch (actions, options = {}) {
|
patch (actions) {
|
||||||
if (!Array.isArray(actions)) {
|
if (!Array.isArray(actions)) {
|
||||||
throw new TypeError('Array with patch actions expected')
|
throw new TypeError('Array with patch actions expected')
|
||||||
}
|
}
|
||||||
|
|
||||||
const expand = options.expand || (path => this.expandKeepOrExpandAll(path))
|
console.log('patch', actions)
|
||||||
const result = patchEson(this.state.eson, actions, expand)
|
|
||||||
const eson = result.data
|
const jsonResult = immutableJsonPatch(this.state.json, actions)
|
||||||
const json = esonToJson(eson) // FIXME: apply the patch to the json too, instead of completely replacing it
|
const esonResult = immutableJsonPatch(this.state.eson, actions.map(toEsonPatchAction))
|
||||||
|
|
||||||
if (this.props.history !== false) {
|
if (this.props.history !== false) {
|
||||||
// update data and store history
|
// update data and store history
|
||||||
const historyItem = {
|
const historyItem = {
|
||||||
redo: actions,
|
redo: actions,
|
||||||
undo: result.revert
|
undo: jsonResult.revert
|
||||||
}
|
}
|
||||||
|
|
||||||
const history = [historyItem]
|
const history = [historyItem]
|
||||||
|
@ -951,8 +970,8 @@ export default class TreeMode extends PureComponent {
|
||||||
|
|
||||||
// FIXME: apply search
|
// FIXME: apply search
|
||||||
this.setState({
|
this.setState({
|
||||||
eson,
|
json: jsonResult.json,
|
||||||
json,
|
eson: esonResult.json,
|
||||||
history,
|
history,
|
||||||
historyIndex: 0
|
historyIndex: 0
|
||||||
})
|
})
|
||||||
|
@ -961,18 +980,16 @@ export default class TreeMode extends PureComponent {
|
||||||
// update data and don't store history
|
// update data and don't store history
|
||||||
// FIXME: apply search
|
// FIXME: apply search
|
||||||
this.setState({
|
this.setState({
|
||||||
eson,
|
json: jsonResult.json,
|
||||||
json
|
eson: esonResult.json
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
patch: actions,
|
patch: actions,
|
||||||
revert: result.revert,
|
revert: jsonResult.revert,
|
||||||
error: result.error,
|
error: jsonResult.error,
|
||||||
data: eson, // FIXME: shouldn't pass data here?
|
json: jsonResult.json // FIXME: shouldn't pass json here?
|
||||||
eson, // FIXME: shouldn't pass eson here
|
|
||||||
json // FIXME: shouldn't pass json here
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -982,13 +999,11 @@ export default class TreeMode extends PureComponent {
|
||||||
*/
|
*/
|
||||||
set (json) {
|
set (json) {
|
||||||
// FIXME: when both json and expand are being changed via React, this.props must be updated before set(json) is called
|
// FIXME: when both json and expand are being changed via React, this.props must be updated before set(json) is called
|
||||||
// TODO: document option expand
|
|
||||||
const expandCallback = this.props.expand || TreeMode.expandRoot
|
|
||||||
|
|
||||||
// FIXME: apply search
|
// FIXME: apply search
|
||||||
this.setState({
|
this.setState({
|
||||||
json: json,
|
json,
|
||||||
eson: expand(jsonToEson(json), expandCallback),
|
eson: syncEson(json, this.state.eson), // FIXME: reset eson in set, keep in update?
|
||||||
|
|
||||||
// TODO: do we want to keep history when .set(json) is called? (currently we remove history)
|
// TODO: do we want to keep history when .set(json) is called? (currently we remove history)
|
||||||
history: [],
|
history: [],
|
||||||
|
@ -1090,29 +1105,7 @@ export default class TreeMode extends PureComponent {
|
||||||
* @param {Path} path
|
* @param {Path} path
|
||||||
*/
|
*/
|
||||||
exists (path) {
|
exists (path) {
|
||||||
return pathExists(this.state.eson, path)
|
return existsIn(this.state.json, path)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test whether an Array or Object at a certain path is expanded.
|
|
||||||
* When the node does not exist, the function throws an error
|
|
||||||
* @param {Path} path
|
|
||||||
* @return {boolean} Returns true when expanded, false otherwise
|
|
||||||
*/
|
|
||||||
isExpanded (path) {
|
|
||||||
return getIn(this.state.eson, path)[META].expanded
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Expand function which keeps the expanded state the same as the current data.
|
|
||||||
* When the path doesn't yet exist, it will be expanded.
|
|
||||||
* @param {Path} path
|
|
||||||
* @return {boolean}
|
|
||||||
*/
|
|
||||||
expandKeepOrExpandAll (path) {
|
|
||||||
return this.exists(path)
|
|
||||||
? this.isExpanded(path)
|
|
||||||
: TreeMode.expandAll(path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -101,14 +101,14 @@ const CREATE_TYPE = {
|
||||||
insertObjectAfter: (path, emit) => h('button', {
|
insertObjectAfter: (path, emit) => h('button', {
|
||||||
key: 'insertObjectAfter',
|
key: 'insertObjectAfter',
|
||||||
className: MENU_ITEM_CLASS_NAME,
|
className: MENU_ITEM_CLASS_NAME,
|
||||||
onClick: () => emit('insertAfter', {path, type: 'Object'}),
|
onClick: () => emit('insertAfter', {path, type: 'object'}),
|
||||||
title: 'Insert Object'
|
title: 'Insert Object'
|
||||||
}, 'Insert Object'),
|
}, 'Insert Object'),
|
||||||
|
|
||||||
insertArrayAfter: (path, emit) => h('button', {
|
insertArrayAfter: (path, emit) => h('button', {
|
||||||
key: 'insertArrayAfter',
|
key: 'insertArrayAfter',
|
||||||
className: MENU_ITEM_CLASS_NAME,
|
className: MENU_ITEM_CLASS_NAME,
|
||||||
onClick: () => emit('insertAfter', {path, type: 'Array'}),
|
onClick: () => emit('insertAfter', {path, type: 'array'}),
|
||||||
title: 'Insert Array'
|
title: 'Insert Array'
|
||||||
}, 'Insert Array'),
|
}, 'Insert Array'),
|
||||||
|
|
||||||
|
@ -129,14 +129,14 @@ const CREATE_TYPE = {
|
||||||
insertObjectInside: (path, emit) => h('button', {
|
insertObjectInside: (path, emit) => h('button', {
|
||||||
key: 'insertObjectInside',
|
key: 'insertObjectInside',
|
||||||
className: MENU_ITEM_CLASS_NAME,
|
className: MENU_ITEM_CLASS_NAME,
|
||||||
onClick: () => emit('insertInside', {path, type: 'Object'}),
|
onClick: () => emit('insertInside', {path, type: 'object'}),
|
||||||
title: 'Insert Object'
|
title: 'Insert Object'
|
||||||
}, 'Insert Object'),
|
}, 'Insert Object'),
|
||||||
|
|
||||||
insertArrayInside: (path, emit) => h('button', {
|
insertArrayInside: (path, emit) => h('button', {
|
||||||
key: 'insertArrayInside',
|
key: 'insertArrayInside',
|
||||||
className: MENU_ITEM_CLASS_NAME,
|
className: MENU_ITEM_CLASS_NAME,
|
||||||
onClick: () => emit('insertInside', {path, type: 'Array'}),
|
onClick: () => emit('insertInside', {path, type: 'array'}),
|
||||||
title: 'Insert Array'
|
title: 'Insert Array'
|
||||||
}, 'Insert Array'),
|
}, 'Insert Array'),
|
||||||
|
|
||||||
|
@ -164,7 +164,9 @@ export default class FloatingMenu extends PureComponent {
|
||||||
})
|
})
|
||||||
]).isRequired
|
]).isRequired
|
||||||
).isRequired,
|
).isRequired,
|
||||||
path: PropTypes.arrayOf(PropTypes.string).isRequired,
|
path: PropTypes.arrayOf(PropTypes.oneOfType([
|
||||||
|
PropTypes.string, PropTypes.number
|
||||||
|
])).isRequired,
|
||||||
emit: PropTypes.func.isRequired,
|
emit: PropTypes.func.isRequired,
|
||||||
position: PropTypes.string // 'top' or 'bottom'
|
position: PropTypes.string // 'top' or 'bottom'
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {
|
||||||
selectContentEditable, hasClassName,
|
selectContentEditable, hasClassName,
|
||||||
findParentWithAttribute, findParentWithClassName
|
findParentWithAttribute, findParentWithClassName
|
||||||
} from '../../utils/domUtils'
|
} from '../../utils/domUtils'
|
||||||
import { compileJSONPointer, parseJSONPointer } from '../../eson'
|
import { compileJSONPointer, parseJSONPointer } from '../../jsonPointer'
|
||||||
|
|
||||||
// singleton
|
// singleton
|
||||||
let lastInputName = null
|
let lastInputName = null
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
/**
|
import { deleteIn, getIn, setIn, shallowCloneWithSymbols, transform, updateIn } from './utils/immutabilityHelpers'
|
||||||
* This file contains functions to act on a ESON object.
|
|
||||||
* All functions are pure and don't mutate the ESON.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { setIn, getIn, updateIn, deleteIn, cloneWithSymbols } from './utils/immutabilityHelpers'
|
|
||||||
import { isObject } from './utils/typeUtils'
|
|
||||||
import isEqual from 'lodash/isEqual'
|
|
||||||
import isEmpty from 'lodash/isEmpty'
|
|
||||||
import range from 'lodash/range'
|
import range from 'lodash/range'
|
||||||
import times from 'lodash/times'
|
import { compileJSONPointer, parseJSONPointer } from './jsonPointer'
|
||||||
import initial from 'lodash/initial'
|
|
||||||
import last from 'lodash/last'
|
import last from 'lodash/last'
|
||||||
|
import initial from 'lodash/initial'
|
||||||
|
import isEmpty from 'lodash/isEmpty'
|
||||||
|
import isEqual from 'lodash/isEqual'
|
||||||
|
import naturalSort from 'javascript-natural-sort'
|
||||||
|
import times from 'lodash/times'
|
||||||
|
|
||||||
|
export const ID = typeof Symbol === 'function' ? Symbol('id') : '@jsoneditor-id'
|
||||||
|
export const TYPE = typeof Symbol === 'function' ? Symbol('type') : '@jsoneditor-type' // 'object', 'array', 'value', or 'undefined'
|
||||||
|
export const VALUE = typeof Symbol === 'function' ? Symbol('value') : '@jsoneditor-value'
|
||||||
|
export const EXPANDED = typeof Symbol === 'function' ? Symbol('expanded') : '@jsoneditor-expanded'
|
||||||
|
export const ERROR = typeof Symbol === 'function' ? Symbol('error') : '@jsoneditor-error'
|
||||||
|
export const SEARCH_PROPERTY = typeof Symbol === 'function' ? Symbol('searchProperty') : '@jsoneditor-search-property'
|
||||||
|
export const SEARCH_VALUE = typeof Symbol === 'function' ? Symbol('searchValue') : '@jsoneditor-search-value'
|
||||||
|
export const SELECTION = typeof Symbol === 'function' ? Symbol('selection') : '@jsoneditor-selection'
|
||||||
|
|
||||||
export const SELECTED = 1
|
export const SELECTED = 1
|
||||||
export const SELECTED_START = 2
|
export const SELECTED_START = 2
|
||||||
|
@ -22,122 +27,88 @@ export const SELECTED_AFTER = 64
|
||||||
export const SELECTED_EMPTY = 128
|
export const SELECTED_EMPTY = 128
|
||||||
export const SELECTED_EMPTY_BEFORE = 256
|
export const SELECTED_EMPTY_BEFORE = 256
|
||||||
|
|
||||||
export const META = Symbol('meta')
|
// TODO: comment
|
||||||
|
export function syncEson(json, eson) {
|
||||||
|
const jsonType = getType(json)
|
||||||
|
const esonType = eson ? eson[TYPE] : 'undefined'
|
||||||
|
const sameType = jsonType === esonType
|
||||||
|
|
||||||
/**
|
if (jsonType === 'array') {
|
||||||
* Expand function which will expand all nodes
|
// TODO: instead of creating updatedEson beforehand, only created as soon as we have a changed item
|
||||||
* @param {Path} path
|
let changed = (esonType !== jsonType) || (eson.length !== esonType.length)
|
||||||
* @return {boolean}
|
let updatedEson = []
|
||||||
*/
|
|
||||||
export function expandAll (path) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
for (let i = 0; i < json.length; i++) {
|
||||||
*
|
const esonI = eson ? eson[i] : undefined
|
||||||
* @param {JSON} json
|
|
||||||
* @param {Path} path
|
|
||||||
* @return {ESON}
|
|
||||||
*/
|
|
||||||
export function jsonToEson (json, path = []) {
|
|
||||||
const id = createId()
|
|
||||||
|
|
||||||
if (isObject(json)) {
|
updatedEson[i] = syncEson(json[i], esonI)
|
||||||
let eson = {}
|
|
||||||
const props = Object.keys(json)
|
|
||||||
props.forEach((prop) => eson[prop] = jsonToEson(json[prop], path.concat(prop)))
|
|
||||||
eson[META] = { id, path, type: 'Object', props }
|
|
||||||
return eson
|
|
||||||
}
|
|
||||||
else if (Array.isArray(json)) {
|
|
||||||
let eson = json.map((value, index) => jsonToEson(value, path.concat(String(index))))
|
|
||||||
eson[META] = { id, path, type: 'Array' }
|
|
||||||
return eson
|
|
||||||
}
|
|
||||||
else { // json is a number, string, boolean, or null
|
|
||||||
let eson = {}
|
|
||||||
eson[META] = { id, path, type: 'value', value: json }
|
|
||||||
return eson
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (updatedEson[i] !== esonI) {
|
||||||
* Convert an ESON object to a JSON object
|
changed = true
|
||||||
* @param {ESON} eson
|
|
||||||
* @return {Object | Array | string | number | boolean | null} json
|
|
||||||
*/
|
|
||||||
export function esonToJson (eson) {
|
|
||||||
switch (eson[META].type) {
|
|
||||||
case 'Array':
|
|
||||||
return eson.map(item => esonToJson(item))
|
|
||||||
|
|
||||||
case 'Object':
|
|
||||||
const object = {}
|
|
||||||
|
|
||||||
eson[META].props.forEach(prop => {
|
|
||||||
object[prop] = esonToJson(eson[prop])
|
|
||||||
})
|
|
||||||
|
|
||||||
return object
|
|
||||||
|
|
||||||
default: // type 'string' or 'value'
|
|
||||||
return eson[META].value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform an eson object, traverse over the whole object (excluding the _meta)
|
|
||||||
* objects, and allow replacing Objects/Arrays/values
|
|
||||||
* @param {ESON} eson
|
|
||||||
* @param {function (ESON, Path) : ESON} callback
|
|
||||||
* @param {Path} [path]
|
|
||||||
* @return {ESON}
|
|
||||||
*/
|
|
||||||
export function transform (eson, callback, path = []) {
|
|
||||||
const updated = callback(eson, path)
|
|
||||||
|
|
||||||
if (updated[META].type === 'Object') {
|
|
||||||
let changed = false
|
|
||||||
let updatedChilds = {}
|
|
||||||
for (let key in updated) {
|
|
||||||
if (updated.hasOwnProperty(key)) {
|
|
||||||
updatedChilds[key] = transform(updated[key], callback, path.concat(key))
|
|
||||||
changed = changed || (updatedChilds[key] !== updated[key])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updatedChilds[META] = updated[META]
|
|
||||||
return changed ? updatedChilds : updated
|
|
||||||
}
|
|
||||||
else if (updated[META].type === 'Array') {
|
|
||||||
let changed = false
|
|
||||||
let updatedChilds = []
|
|
||||||
for (let i = 0; i < updated.length; i++) {
|
|
||||||
updatedChilds[i] = transform(updated[i], callback, path.concat(String(i)))
|
|
||||||
changed = changed || (updatedChilds[i] !== updated[i])
|
|
||||||
}
|
|
||||||
updatedChilds[META] = updated[META]
|
|
||||||
return changed ? updatedChilds : updated
|
|
||||||
}
|
|
||||||
else { // eson[META].type === 'value'
|
|
||||||
return updated
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (changed) {
|
||||||
* Recursively update all paths in an eson object, array or value
|
updatedEson[ID] = sameType ? eson[ID] : createId()
|
||||||
* @param {ESON} eson
|
updatedEson[TYPE] = jsonType
|
||||||
* @param {Path} [path]
|
updatedEson[VALUE] = json
|
||||||
* @return {ESON}
|
updatedEson[EXPANDED] = sameType ? eson[EXPANDED] : false
|
||||||
*/
|
|
||||||
export function updatePaths(eson, path = []) {
|
return updatedEson
|
||||||
return transform(eson, function (value, path) {
|
|
||||||
if (!isEqual(value[META].path, path)) {
|
|
||||||
return setIn(value, [META, 'path'], path)
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return value
|
return eson
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jsonType === 'object') {
|
||||||
|
const jsonKeys = Object.keys(json)
|
||||||
|
const esonKeys = esonType === 'object' ? Object.keys(eson) : []
|
||||||
|
|
||||||
|
// TODO: instead of creating updatedEson beforehand, only created as soon as we have a changed item
|
||||||
|
let changed = (esonType !== jsonType) || (jsonKeys.length !== esonKeys.length)
|
||||||
|
let updatedEson = {}
|
||||||
|
|
||||||
|
for (let i = 0; i < jsonKeys.length; i++) {
|
||||||
|
const key = jsonKeys[i]
|
||||||
|
const esonValue = eson ? eson[key] : undefined
|
||||||
|
|
||||||
|
updatedEson[key] = syncEson(json[key], esonValue)
|
||||||
|
|
||||||
|
if (updatedEson[key] !== esonValue) {
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
updatedEson[ID] = sameType ? eson[ID] : createId()
|
||||||
|
updatedEson[TYPE] = jsonType
|
||||||
|
updatedEson[VALUE] = json
|
||||||
|
updatedEson[EXPANDED] = sameType ? eson[EXPANDED] : false
|
||||||
|
|
||||||
|
return updatedEson
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return eson
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jsonType === 'value') { // json is a value
|
||||||
|
if (sameType) {
|
||||||
|
return eson
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const updatedEson = {}
|
||||||
|
|
||||||
|
updatedEson[ID] = sameType ? eson[ID] : createId()
|
||||||
|
updatedEson[TYPE] = jsonType
|
||||||
|
updatedEson[VALUE] = json
|
||||||
|
updatedEson[EXPANDED] = false
|
||||||
|
|
||||||
|
return updatedEson
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else { // jsonType === 'undefined'
|
||||||
|
return undefined
|
||||||
}
|
}
|
||||||
}, path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -151,7 +122,7 @@ export function updatePaths(eson, path = []) {
|
||||||
*/
|
*/
|
||||||
export function expand (eson, callback) {
|
export function expand (eson, callback) {
|
||||||
return transform(eson, function (value, path) {
|
return transform(eson, function (value, path) {
|
||||||
if (value[META].type === 'Array' || value[META].type === 'Object') {
|
if (value[TYPE] === 'array' || value[TYPE] === 'object') {
|
||||||
const expanded = callback(path)
|
const expanded = callback(path)
|
||||||
return (typeof expanded === 'boolean')
|
return (typeof expanded === 'boolean')
|
||||||
? expandOne(value, [], expanded) // adjust expanded state
|
? expandOne(value, [], expanded) // adjust expanded state
|
||||||
|
@ -171,7 +142,7 @@ export function expand (eson, callback) {
|
||||||
* @return {ESON}
|
* @return {ESON}
|
||||||
*/
|
*/
|
||||||
export function expandOne (eson, path, expanded = true) {
|
export function expandOne (eson, path, expanded = true) {
|
||||||
return setIn(eson, path.concat([META, 'expanded']), expanded)
|
return setIn(eson, path.concat(EXPANDED), expanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -206,7 +177,7 @@ export function applyErrors (eson, errors = []) {
|
||||||
const esonWithErrors = errors.reduce((eson, error) => {
|
const esonWithErrors = errors.reduce((eson, error) => {
|
||||||
const path = parseJSONPointer(error.dataPath)
|
const path = parseJSONPointer(error.dataPath)
|
||||||
// TODO: do we want to be able to store multiple errors per item?
|
// TODO: do we want to be able to store multiple errors per item?
|
||||||
return setIn(eson, path.concat([META, 'error']), error)
|
return setIn(eson, path.concat(ERROR), error)
|
||||||
}, eson)
|
}, eson)
|
||||||
|
|
||||||
// cleanup any old error messages
|
// cleanup any old error messages
|
||||||
|
@ -216,11 +187,12 @@ export function applyErrors (eson, errors = []) {
|
||||||
/**
|
/**
|
||||||
* Cleanup meta data from an eson object
|
* Cleanup meta data from an eson object
|
||||||
* @param {ESON} eson Object to be cleaned up
|
* @param {ESON} eson Object to be cleaned up
|
||||||
* @param {String} field Field name, for example 'error' or 'selected'
|
* @param {String | Symbol} symbol A meta data field name, for example ERROR or SELECTED
|
||||||
* @param {Path[]} [ignorePaths=[]] An optional array with paths to be ignored
|
* @param {Array.<string | Path>} [ignorePaths=[]] An optional array with paths to be ignored
|
||||||
* @return {ESON}
|
* @return {ESON}
|
||||||
*/
|
*/
|
||||||
export function cleanupMetaData(eson, field, ignorePaths = []) {
|
export function cleanupMetaData(eson, symbol, ignorePaths = []) {
|
||||||
|
// TODO: change ignorePaths to an object with path as key and true as value
|
||||||
const pathsMap = {}
|
const pathsMap = {}
|
||||||
ignorePaths.forEach(path => {
|
ignorePaths.forEach(path => {
|
||||||
const pathString = (typeof path === 'string') ? path : compileJSONPointer(path)
|
const pathString = (typeof path === 'string') ? path : compileJSONPointer(path)
|
||||||
|
@ -228,8 +200,8 @@ export function cleanupMetaData(eson, field, ignorePaths = []) {
|
||||||
})
|
})
|
||||||
|
|
||||||
return transform(eson, function (value, path) {
|
return transform(eson, function (value, path) {
|
||||||
return (value[META][field] && !pathsMap[compileJSONPointer(path)])
|
return (value[symbol] && !pathsMap[compileJSONPointer(path)])
|
||||||
? deleteIn(value, [META, field])
|
? deleteIn(value, [symbol])
|
||||||
: value
|
: value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -253,23 +225,23 @@ export function search (eson, text) {
|
||||||
// check property name
|
// check property name
|
||||||
const prop = last(path)
|
const prop = last(path)
|
||||||
if (text !== '' && containsCaseInsensitive(prop, text) &&
|
if (text !== '' && containsCaseInsensitive(prop, text) &&
|
||||||
getIn(eson, initial(path))[META].type === 'Object') { // parent must be an Object
|
getIn(eson, initial(path))[TYPE] === 'object') { // parent must be an Object
|
||||||
const searchState = isEmpty(matches) ? 'active' : 'normal'
|
const searchState = isEmpty(matches) ? 'active' : 'normal'
|
||||||
matches.push({path, area: 'property'})
|
matches.push({path, area: 'property'})
|
||||||
updatedValue = setIn(updatedValue, [META, 'searchProperty'], searchState)
|
updatedValue = setIn(updatedValue, [SEARCH_PROPERTY], searchState)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
updatedValue = deleteIn(updatedValue, [META, 'searchProperty'])
|
updatedValue = deleteIn(updatedValue, [SEARCH_PROPERTY])
|
||||||
}
|
}
|
||||||
|
|
||||||
// check value
|
// check value
|
||||||
if (value[META].type === 'value' && text !== '' && containsCaseInsensitive(value[META].value, text)) {
|
if (value[TYPE] === 'value' && text !== '' && containsCaseInsensitive(value[VALUE], text)) {
|
||||||
const searchState = isEmpty(matches) ? 'active' : 'normal'
|
const searchState = isEmpty(matches) ? 'active' : 'normal'
|
||||||
matches.push({path, area: 'value'})
|
matches.push({path, area: 'value'})
|
||||||
updatedValue = setIn(updatedValue, [META, 'searchValue'], searchState)
|
updatedValue = setIn(updatedValue, [SEARCH_VALUE], searchState)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
updatedValue = deleteIn(updatedValue, [META, 'searchValue'])
|
updatedValue = deleteIn(updatedValue, [SEARCH_VALUE])
|
||||||
}
|
}
|
||||||
|
|
||||||
return updatedValue
|
return updatedValue
|
||||||
|
@ -346,9 +318,9 @@ export function nextSearchResult (eson, searchResult) {
|
||||||
* @return {Object|Array}
|
* @return {Object|Array}
|
||||||
*/
|
*/
|
||||||
function setSearchStatus (eson, esonPointer, searchStatus) {
|
function setSearchStatus (eson, esonPointer, searchStatus) {
|
||||||
const metaProp = esonPointer.area === 'property' ? 'searchProperty': 'searchValue'
|
const searchSymbol = esonPointer.area === 'property' ? SEARCH_PROPERTY : SEARCH_VALUE
|
||||||
|
|
||||||
return setIn(eson, esonPointer.path.concat([META, metaProp]), searchStatus)
|
return setIn(eson, esonPointer.path.concat([searchSymbol]), searchStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -362,23 +334,19 @@ export function applySelection (eson, selection) {
|
||||||
return cleanupMetaData(eson, 'selected')
|
return cleanupMetaData(eson, 'selected')
|
||||||
}
|
}
|
||||||
else if (selection.inside) {
|
else if (selection.inside) {
|
||||||
const updatedEson = setIn(eson, selection.inside.concat([META, 'selected']),
|
const updatedEson = setIn(eson, selection.inside.concat([SELECTION]), SELECTED_INSIDE)
|
||||||
SELECTED_INSIDE)
|
|
||||||
return cleanupMetaData(updatedEson, 'selected', [selection.inside])
|
return cleanupMetaData(updatedEson, 'selected', [selection.inside])
|
||||||
}
|
}
|
||||||
else if (selection.after) {
|
else if (selection.after) {
|
||||||
const updatedEson = setIn(eson, selection.after.concat([META, 'selected']),
|
const updatedEson = setIn(eson, selection.after.concat([SELECTION]), SELECTED_AFTER)
|
||||||
SELECTED_AFTER)
|
|
||||||
return cleanupMetaData(updatedEson, 'selected', [selection.after])
|
return cleanupMetaData(updatedEson, 'selected', [selection.after])
|
||||||
}
|
}
|
||||||
else if (selection.empty) {
|
else if (selection.empty) {
|
||||||
const updatedEson = setIn(eson, selection.empty.concat([META, 'selected']),
|
const updatedEson = setIn(eson, selection.empty.concat([SELECTION]), SELECTED_EMPTY)
|
||||||
SELECTED_EMPTY)
|
|
||||||
return cleanupMetaData(updatedEson, 'selected', [selection.empty])
|
return cleanupMetaData(updatedEson, 'selected', [selection.empty])
|
||||||
}
|
}
|
||||||
else if (selection.emptyBefore) {
|
else if (selection.emptyBefore) {
|
||||||
const updatedEson = setIn(eson, selection.emptyBefore.concat([META, 'selected']),
|
const updatedEson = setIn(eson, selection.emptyBefore.concat([SELECTION]), SELECTED_EMPTY_BEFORE)
|
||||||
SELECTED_EMPTY_BEFORE)
|
|
||||||
return cleanupMetaData(updatedEson, 'selected', [selection.emptyBefore])
|
return cleanupMetaData(updatedEson, 'selected', [selection.emptyBefore])
|
||||||
}
|
}
|
||||||
else { // selection.start and selection.end
|
else { // selection.start and selection.end
|
||||||
|
@ -392,30 +360,31 @@ export function applySelection (eson, selection) {
|
||||||
|
|
||||||
// TODO: simplify the update function. Use pathsFromSelection ?
|
// TODO: simplify the update function. Use pathsFromSelection ?
|
||||||
|
|
||||||
if (root[META].type === 'Object') {
|
if (root[TYPE] === 'object') {
|
||||||
const startIndex = root[META].props.indexOf(start)
|
const props = Object.keys(root).sort(naturalSort) // TODO: create a util function getSortedProps
|
||||||
const endIndex = root[META].props.indexOf(end)
|
const startIndex = props.indexOf(start)
|
||||||
|
const endIndex = props.indexOf(end)
|
||||||
|
|
||||||
const firstIndex = Math.min(startIndex, endIndex)
|
const firstIndex = Math.min(startIndex, endIndex)
|
||||||
const lastIndex = Math.max(startIndex, endIndex)
|
const lastIndex = Math.max(startIndex, endIndex)
|
||||||
const firstProp = root[META].props[firstIndex]
|
const firstProp = props[firstIndex]
|
||||||
const lastProp = root[META].props[lastIndex]
|
const lastProp = props[lastIndex]
|
||||||
|
|
||||||
const selectedProps = root[META].props.slice(firstIndex, lastIndex + 1)// include max index itself
|
const selectedProps = props.slice(firstIndex, lastIndex + 1)// include max index itself
|
||||||
selectedPaths = selectedProps.map(prop => rootPath.concat(prop))
|
selectedPaths = selectedProps.map(prop => rootPath.concat(prop))
|
||||||
let updatedObj = cloneWithSymbols(root)
|
let updatedObj = shallowCloneWithSymbols(root)
|
||||||
selectedProps.forEach(prop => {
|
selectedProps.forEach(prop => {
|
||||||
const selected = SELECTED +
|
const selected = SELECTED +
|
||||||
(prop === start ? SELECTED_START : 0) +
|
(prop === start ? SELECTED_START : 0) +
|
||||||
(prop === end ? SELECTED_END : 0) +
|
(prop === end ? SELECTED_END : 0) +
|
||||||
(prop === firstProp ? SELECTED_FIRST : 0) +
|
(prop === firstProp ? SELECTED_FIRST : 0) +
|
||||||
(prop === lastProp ? SELECTED_LAST : 0)
|
(prop === lastProp ? SELECTED_LAST : 0)
|
||||||
updatedObj[prop] = setIn(updatedObj[prop], [META, 'selected'], selected)
|
updatedObj[prop] = setIn(updatedObj[prop], [SELECTION], selected)
|
||||||
})
|
})
|
||||||
|
|
||||||
return updatedObj
|
return updatedObj
|
||||||
}
|
}
|
||||||
else { // root[META].type === 'Array'
|
else { // root[TYPE] === 'array'
|
||||||
const startIndex = parseInt(start, 10)
|
const startIndex = parseInt(start, 10)
|
||||||
const endIndex = parseInt(end, 10)
|
const endIndex = parseInt(end, 10)
|
||||||
|
|
||||||
|
@ -426,14 +395,14 @@ export function applySelection (eson, selection) {
|
||||||
selectedPaths = selectedIndices.map(index => rootPath.concat(String(index)))
|
selectedPaths = selectedIndices.map(index => rootPath.concat(String(index)))
|
||||||
|
|
||||||
let updatedArr = root.slice()
|
let updatedArr = root.slice()
|
||||||
updatedArr = cloneWithSymbols(root)
|
updatedArr = shallowCloneWithSymbols(root)
|
||||||
selectedIndices.forEach(index => {
|
selectedIndices.forEach(index => {
|
||||||
const selected = SELECTED +
|
const selected = SELECTED +
|
||||||
(index === startIndex ? SELECTED_START : 0) +
|
(index === startIndex ? SELECTED_START : 0) +
|
||||||
(index === endIndex ? SELECTED_END : 0) +
|
(index === endIndex ? SELECTED_END : 0) +
|
||||||
(index === firstIndex ? SELECTED_FIRST : 0) +
|
(index === firstIndex ? SELECTED_FIRST : 0) +
|
||||||
(index === lastIndex ? SELECTED_LAST : 0)
|
(index === lastIndex ? SELECTED_LAST : 0)
|
||||||
updatedArr[index] = setIn(updatedArr[index], [META, 'selected'], selected)
|
updatedArr[index] = setIn(updatedArr[index], [SELECTION], selected)
|
||||||
})
|
})
|
||||||
|
|
||||||
return updatedArr
|
return updatedArr
|
||||||
|
@ -459,8 +428,9 @@ export function findSelectionIndices (root, rootPath, selection) {
|
||||||
const end = (selection.after || selection.inside || selection.end)[rootPath.length]
|
const end = (selection.after || selection.inside || selection.end)[rootPath.length]
|
||||||
|
|
||||||
// if no object we assume it's an Array
|
// if no object we assume it's an Array
|
||||||
const startIndex = root[META].type === 'Object' ? root[META].props.indexOf(start) : parseInt(start, 10)
|
const props = Object.keys(root).sort(naturalSort) // TODO: create a util function getSortedProps
|
||||||
const endIndex = root[META].type === 'Object' ? root[META].props.indexOf(end) : parseInt(end, 10)
|
const startIndex = root[TYPE] === 'object' ? props.indexOf(start) : parseInt(start, 10)
|
||||||
|
const endIndex = root[TYPE] === 'object' ? props.indexOf(end) : parseInt(end, 10)
|
||||||
|
|
||||||
const minIndex = Math.min(startIndex, endIndex)
|
const minIndex = Math.min(startIndex, endIndex)
|
||||||
const maxIndex = Math.max(startIndex, endIndex) +
|
const maxIndex = Math.max(startIndex, endIndex) +
|
||||||
|
@ -469,92 +439,23 @@ export function findSelectionIndices (root, rootPath, selection) {
|
||||||
return { minIndex, maxIndex }
|
return { minIndex, maxIndex }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the JSON paths from a selection, sorted from first to last
|
|
||||||
* @param {ESON} eson
|
|
||||||
* @param {Selection} selection
|
|
||||||
* @return {Path[]}
|
|
||||||
*/
|
|
||||||
export function pathsFromSelection (eson, selection) {
|
|
||||||
// find the parent node shared by both start and end of the selection
|
|
||||||
const rootPath = findRootPath(selection)
|
|
||||||
const root = getIn(eson, rootPath)
|
|
||||||
|
|
||||||
const { minIndex, maxIndex } = findSelectionIndices(root, rootPath, selection)
|
|
||||||
|
|
||||||
if (root[META].type === 'Object') {
|
|
||||||
return times(maxIndex - minIndex, i => rootPath.concat(root[META].props[i + minIndex]))
|
|
||||||
}
|
|
||||||
else { // root[META].type === 'Array'
|
|
||||||
return times(maxIndex - minIndex, i => rootPath.concat(String(i + minIndex)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the contents of a list with paths
|
* Get the contents of a list with paths
|
||||||
* @param {ESON} data
|
* @param {ESON} eson
|
||||||
* @param {Path[]} paths
|
* @param {Path[]} paths
|
||||||
* @return {Array.<{name: string, value: JSON, state: Object}>}
|
* @return {Array.<{name: string, value: JSON, state: Object}>}
|
||||||
*/
|
*/
|
||||||
export function contentsFromPaths (data, paths) {
|
export function contentsFromPaths (eson, paths) {
|
||||||
return paths.map(path => {
|
return paths.map(path => {
|
||||||
const esonValue = getIn(data, path)
|
const value = getIn(eson, path.concat(VALUE))
|
||||||
return {
|
return {
|
||||||
name: last(path),
|
name: last(path),
|
||||||
value: esonToJson(esonValue),
|
value,
|
||||||
state: getEsonState(esonValue)
|
state: {} // FIXME: state
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an object with paths and state (expanded, type) of an eson object
|
|
||||||
* @param {ESON} eson
|
|
||||||
* @return {Object.<key, Object>} An object with compiled JSON paths as key,
|
|
||||||
* And a META object as state
|
|
||||||
*/
|
|
||||||
export function getEsonState (eson) {
|
|
||||||
let state = {}
|
|
||||||
|
|
||||||
transform(eson, function (eson, path) {
|
|
||||||
let meta = {}
|
|
||||||
if (eson[META].expanded === true) {
|
|
||||||
meta.expanded = true
|
|
||||||
}
|
|
||||||
if (eson[META].type === 'string') {
|
|
||||||
meta.type = 'string'
|
|
||||||
}
|
|
||||||
if (!isEmpty(meta)) {
|
|
||||||
state[compileJSONPointer(path)] = meta
|
|
||||||
}
|
|
||||||
|
|
||||||
return eson
|
|
||||||
})
|
|
||||||
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Merge ESON meta data to an ESON object: expanded state, type
|
|
||||||
* @param {ESON} data
|
|
||||||
* @param {Object.<String, Object>} state
|
|
||||||
* @return {ESON}
|
|
||||||
*/
|
|
||||||
export function applyEsonState(data, state) {
|
|
||||||
let updatedData = data
|
|
||||||
|
|
||||||
for (let path in state) {
|
|
||||||
if (state.hasOwnProperty(path)) {
|
|
||||||
const metaPath = parseJSONPointer(path).concat(META)
|
|
||||||
updatedData = updateIn(updatedData, metaPath, function (meta) {
|
|
||||||
return Object.assign({}, meta, state[path])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatedData
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the root path of a selection: the parent node shared by both start
|
* Find the root path of a selection: the parent node shared by both start
|
||||||
* and end of the selection
|
* and end of the selection
|
||||||
|
@ -605,93 +506,56 @@ function findSharedPath (path1, path2) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test whether a path exists in the eson object
|
* Get the JSON paths from a selection, sorted from first to last
|
||||||
* @param {ESON} eson
|
* @param {ESON} eson
|
||||||
* @param {Path} path
|
* @param {Selection} selection
|
||||||
* @return {boolean} Returns true if the path exists, else returns false
|
* @return {Path[]}
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
export function pathExists (eson, path) {
|
// TODO: move pathsFromSelection to a separate file clipboard.js or selection.js?
|
||||||
if (eson === undefined) {
|
export function pathsFromSelection (eson, selection) {
|
||||||
return false
|
// find the parent node shared by both start and end of the selection
|
||||||
}
|
const rootPath = findRootPath(selection)
|
||||||
|
const root = getIn(eson, rootPath)
|
||||||
|
|
||||||
if (path.length === 0) {
|
const { minIndex, maxIndex } = findSelectionIndices(root, rootPath, selection)
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(eson)) {
|
if (root[TYPE] === 'object') {
|
||||||
// index of an array
|
const props = Object.keys(root).sort(naturalSort) // TODO: create a util function getSortedProps
|
||||||
return pathExists(eson[parseInt(path[0], 10)], path.slice(1))
|
return times(maxIndex - minIndex, i => rootPath.concat(props[i + minIndex]))
|
||||||
}
|
}
|
||||||
else { // Object
|
else { // root[TYPE] === 'array'
|
||||||
// object property. find the index of this property
|
return times(maxIndex - minIndex, i => rootPath.concat(String(i + minIndex)))
|
||||||
return pathExists(eson[path[0]], path.slice(1))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the index for `arr/-`, replace it with an index value equal to the
|
* Convert the value of a JSON Patch action into a ESON object
|
||||||
* length of the array
|
* @param {JSONPatchAction} action
|
||||||
* @param {ESON} eson
|
* @returns {ESONPatchAction}
|
||||||
* @param {Path} path
|
|
||||||
* @return {Path}
|
|
||||||
*/
|
*/
|
||||||
export function resolvePathIndex (eson, path) {
|
export function toEsonPatchAction (action) {
|
||||||
if (path[path.length - 1] === '-') {
|
return ('value' in action)
|
||||||
const parentPath = initial(path)
|
? setIn(action, ['value'], syncEson(action.value))
|
||||||
const parent = getIn(eson, parentPath)
|
: action
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(parent)) {
|
// TODO: comment
|
||||||
const index = parent.length
|
export function getType (any) {
|
||||||
return parentPath.concat(String(index))
|
if (any === undefined) {
|
||||||
}
|
return 'undefined'
|
||||||
}
|
}
|
||||||
|
|
||||||
return path
|
if (Array.isArray(any)) {
|
||||||
|
return 'array'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (any && typeof any === 'object') {
|
||||||
|
return 'object'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'value'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the property after provided property
|
|
||||||
* @param {ESON} parent
|
|
||||||
* @param {string} prop
|
|
||||||
* @return {string | null} Returns the name of the next property,
|
|
||||||
* or null if there is none
|
|
||||||
*/
|
|
||||||
export function findNextProp (parent, prop) {
|
|
||||||
const index = parent[META].props.indexOf(prop)
|
|
||||||
return parent[META].props[index + 1] || null
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: move parseJSONPointer and compileJSONPointer to a separate file
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a JSON Pointer
|
|
||||||
* WARNING: this is not a complete implementation
|
|
||||||
* @param {string} pointer
|
|
||||||
* @return {Path}
|
|
||||||
*/
|
|
||||||
export function parseJSONPointer (pointer) {
|
|
||||||
const path = pointer.split('/')
|
|
||||||
path.shift() // remove the first empty entry
|
|
||||||
|
|
||||||
return path.map(p => p.replace(/~1/g, '/').replace(/~0/g, '~'))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compile a JSON Pointer
|
|
||||||
* WARNING: this is not a complete implementation
|
|
||||||
* @param {Path} path
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
export function compileJSONPointer (path) {
|
|
||||||
return path
|
|
||||||
.map(p => '/' + String(p).replace(/~/g, '~0').replace(/\//g, '~1'))
|
|
||||||
.join('')
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: move createId to a separate file
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Do a case insensitive search for a search text in a text
|
* Do a case insensitive search for a search text in a text
|
||||||
* @param {String} text
|
* @param {String} text
|
||||||
|
@ -706,8 +570,8 @@ export function containsCaseInsensitive (text, search) {
|
||||||
* Get a new "unique" id. Id's are created from an incremental counter.
|
* Get a new "unique" id. Id's are created from an incremental counter.
|
||||||
* @return {number}
|
* @return {number}
|
||||||
*/
|
*/
|
||||||
export function createId () {
|
function createId () {
|
||||||
_id++
|
_id++
|
||||||
return _id
|
return 'node-' + _id
|
||||||
}
|
}
|
||||||
let _id = 0
|
let _id = 0
|
||||||
|
|
|
@ -1,57 +1,67 @@
|
||||||
'use strict'
|
|
||||||
|
|
||||||
import { readFileSync } from 'fs'
|
|
||||||
import { setIn, getIn, deleteIn } from './utils/immutabilityHelpers'
|
|
||||||
import {
|
import {
|
||||||
META,
|
applyErrors,
|
||||||
esonToJson, pathExists, transform,
|
applySelection, ERROR,
|
||||||
parseJSONPointer, compileJSONPointer,
|
expand,
|
||||||
jsonToEson,
|
EXPANDED,
|
||||||
expand, expandOne, expandPath, applyErrors, search, nextSearchResult,
|
expandOne,
|
||||||
|
expandPath,
|
||||||
|
ID,
|
||||||
|
nextSearchResult,
|
||||||
|
pathsFromSelection,
|
||||||
previousSearchResult,
|
previousSearchResult,
|
||||||
applySelection, pathsFromSelection,
|
search, SEARCH_PROPERTY, SEARCH_VALUE,
|
||||||
getEsonState,
|
SELECTED,
|
||||||
SELECTED, SELECTED_START, SELECTED_END, SELECTED_FIRST, SELECTED_LAST
|
SELECTED_END,
|
||||||
|
SELECTED_FIRST,
|
||||||
|
SELECTED_LAST,
|
||||||
|
SELECTED_START, SELECTION,
|
||||||
|
syncEson,
|
||||||
|
TYPE
|
||||||
} from './eson'
|
} from './eson'
|
||||||
import 'console.table'
|
import { getIn, setIn } from './utils/immutabilityHelpers'
|
||||||
import repeat from 'lodash/repeat'
|
import { createAssertEqualEson } from './utils/assertEqualEson'
|
||||||
import { assertDeepEqualEson } from './utils/assertDeepEqualEson'
|
|
||||||
|
|
||||||
test('jsonToEson', () => {
|
const assertEqualEson = createAssertEqualEson(expect)
|
||||||
assertDeepEqualEson(jsonToEson(1), {[META]: {id: '[ID]', path: [], type: 'value', value: 1}})
|
|
||||||
assertDeepEqualEson(jsonToEson("foo"), {[META]: {id: '[ID]', path: [], type: 'value', value: "foo"}})
|
test('syncEson', () => {
|
||||||
assertDeepEqualEson(jsonToEson(null), {[META]: {id: '[ID]', path: [], type: 'value', value: null}})
|
const json1 = {
|
||||||
assertDeepEqualEson(jsonToEson(false), {[META]: {id: '[ID]', path: [], type: 'value', value: false}})
|
arr: [1,2,3],
|
||||||
assertDeepEqualEson(jsonToEson({a:1, b: 2}), {
|
obj: {a : 2}
|
||||||
[META]: {id: '[ID]', path: [], type: 'Object', props: ['a', 'b']},
|
}
|
||||||
a: {[META]: {id: '[ID]', path: ['a'], type: 'value', value: 1}},
|
|
||||||
b: {[META]: {id: '[ID]', path: ['b'], type: 'value', value: 2}}
|
const nodeState1 = syncEson(json1, undefined)
|
||||||
|
|
||||||
|
expect(nodeState1).toEqual({
|
||||||
|
arr: [{}, {}, {}],
|
||||||
|
obj: {a: {}}
|
||||||
|
})
|
||||||
|
expect(nodeState1.arr[0][ID]).toBeDefined()
|
||||||
|
expect(nodeState1.arr[0][TYPE]).toEqual('value')
|
||||||
|
expect(nodeState1.arr[TYPE]).toEqual('array')
|
||||||
|
expect(nodeState1[TYPE]).toEqual('object')
|
||||||
|
|
||||||
|
const json2 = {
|
||||||
|
arr: [1, 2],
|
||||||
|
obj: {a : 2, b : 4}
|
||||||
|
}
|
||||||
|
const nodeState2 = syncEson(json2, nodeState1)
|
||||||
|
|
||||||
|
expect(nodeState2).toEqual({
|
||||||
|
arr: [{}, {}],
|
||||||
|
obj: {a: {}, b: {}}
|
||||||
})
|
})
|
||||||
|
|
||||||
const actual = jsonToEson([1,2])
|
// ID's should be the same for unchanged contents
|
||||||
const expected = [
|
expect(nodeState2[ID]).toEqual(nodeState1[ID])
|
||||||
{[META]: {id: '[ID]', path: ['0'], type: 'value', value: 1}},
|
expect(nodeState2.arr[ID]).toEqual(nodeState1.arr[ID])
|
||||||
{[META]: {id: '[ID]', path: ['1'], type: 'value', value: 2}}
|
expect(nodeState2.arr[0][ID]).toEqual(nodeState1.arr[0][ID])
|
||||||
]
|
expect(nodeState2.arr[1][ID]).toEqual(nodeState1.arr[1][ID])
|
||||||
expected[META] = {id: '[ID]', path: [], type: 'Array'}
|
expect(nodeState2.obj[ID]).toEqual(nodeState1.obj[ID])
|
||||||
assertDeepEqualEson(actual, expected)
|
expect(nodeState2.obj.a[ID]).toEqual(nodeState1.obj.a[ID])
|
||||||
})
|
|
||||||
|
|
||||||
test('esonToJson', () => {
|
|
||||||
const json = {
|
|
||||||
"obj": {
|
|
||||||
"arr": [1,2, {"first":3,"last":4}]
|
|
||||||
},
|
|
||||||
"str": "hello world",
|
|
||||||
"nill": null,
|
|
||||||
"bool": false
|
|
||||||
}
|
|
||||||
const eson = jsonToEson(json)
|
|
||||||
expect(esonToJson(eson)).toEqual(json)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('expand a single path', () => {
|
test('expand a single path', () => {
|
||||||
const eson = jsonToEson({
|
const eson = syncEson({
|
||||||
"obj": {
|
"obj": {
|
||||||
"arr": [1,2, {"first":3,"last":4}]
|
"arr": [1,2, {"first":3,"last":4}]
|
||||||
},
|
},
|
||||||
|
@ -62,16 +72,14 @@ test('expand a single path', () => {
|
||||||
|
|
||||||
const path = ['obj', 'arr', 2]
|
const path = ['obj', 'arr', 2]
|
||||||
const collapsed = expandOne(eson, path, false)
|
const collapsed = expandOne(eson, path, false)
|
||||||
expect(collapsed.obj.arr[2][META].expanded).toEqual(false)
|
expect(collapsed.obj.arr[2][EXPANDED]).toEqual(false)
|
||||||
assertDeepEqualEson(deleteIn(collapsed, path.concat([META, 'expanded'])), eson)
|
|
||||||
|
|
||||||
const expanded = expandOne(eson, path, true)
|
const expanded = expandOne(eson, path, true)
|
||||||
expect(expanded.obj.arr[2][META].expanded).toEqual(true)
|
expect(expanded.obj.arr[2][EXPANDED]).toEqual(true)
|
||||||
assertDeepEqualEson(deleteIn(expanded, path.concat([META, 'expanded'])), eson)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('expand all objects/arrays on a path', () => {
|
test('expand all objects/arrays on a path', () => {
|
||||||
const eson = jsonToEson({
|
const eson = syncEson({
|
||||||
"obj": {
|
"obj": {
|
||||||
"arr": [1,2, {"first":3,"last":4}]
|
"arr": [1,2, {"first":3,"last":4}]
|
||||||
},
|
},
|
||||||
|
@ -83,28 +91,20 @@ test('expand all objects/arrays on a path', () => {
|
||||||
const path = ['obj', 'arr', 2]
|
const path = ['obj', 'arr', 2]
|
||||||
|
|
||||||
const collapsed = expandPath(eson, path, false)
|
const collapsed = expandPath(eson, path, false)
|
||||||
expect(collapsed[META].expanded).toEqual(false)
|
expect(collapsed[EXPANDED]).toEqual(false)
|
||||||
expect(collapsed.obj[META].expanded).toEqual(false)
|
expect(collapsed.obj[EXPANDED]).toEqual(false)
|
||||||
expect(collapsed.obj.arr[META].expanded).toEqual(false)
|
expect(collapsed.obj.arr[EXPANDED]).toEqual(false)
|
||||||
expect(collapsed.obj.arr[2][META].expanded).toEqual(false)
|
expect(collapsed.obj.arr[2][EXPANDED]).toEqual(false)
|
||||||
|
|
||||||
const expanded = expandPath(eson, path, true)
|
const expanded = expandPath(eson, path, true)
|
||||||
expect(expanded[META].expanded).toEqual(true)
|
expect(expanded[EXPANDED]).toEqual(true)
|
||||||
expect(expanded.obj[META].expanded).toEqual(true)
|
expect(expanded.obj[EXPANDED]).toEqual(true)
|
||||||
expect(expanded.obj.arr[META].expanded).toEqual(true)
|
expect(expanded.obj.arr[EXPANDED]).toEqual(true)
|
||||||
expect(expanded.obj.arr[2][META].expanded).toEqual(true)
|
expect(expanded.obj.arr[2][EXPANDED]).toEqual(true)
|
||||||
|
|
||||||
let orig = expanded
|
|
||||||
orig = deleteIn(orig, [].concat([META, 'expanded']))
|
|
||||||
orig = deleteIn(orig, ['obj'].concat([META, 'expanded']))
|
|
||||||
orig = deleteIn(orig, ['obj', 'arr'].concat([META, 'expanded']))
|
|
||||||
orig = deleteIn(orig, ['obj', 'arr', 2].concat([META, 'expanded']))
|
|
||||||
|
|
||||||
assertDeepEqualEson(orig, eson)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('expand a callback', () => {
|
test('expand a callback', () => {
|
||||||
const eson = jsonToEson({
|
const eson = syncEson({
|
||||||
"obj": {
|
"obj": {
|
||||||
"arr": [1,2, {"first":3,"last":4}]
|
"arr": [1,2, {"first":3,"last":4}]
|
||||||
},
|
},
|
||||||
|
@ -114,25 +114,20 @@ test('expand a callback', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
function callback (path) {
|
function callback (path) {
|
||||||
|
console.log('callback')
|
||||||
return (path.length >= 1)
|
return (path.length >= 1)
|
||||||
? false // collapse
|
? true // expand
|
||||||
: undefined // leave untouched
|
: undefined // leave untouched
|
||||||
}
|
}
|
||||||
const collapsed = expand(eson, callback)
|
const expanded = expand(eson, callback)
|
||||||
expect(collapsed[META].expanded).toEqual(undefined)
|
expect(expanded[EXPANDED]).toEqual(false)
|
||||||
expect(collapsed.obj[META].expanded).toEqual(false)
|
expect(expanded.obj[EXPANDED]).toEqual(true)
|
||||||
expect(collapsed.obj.arr[META].expanded).toEqual(false)
|
expect(expanded.obj.arr[EXPANDED]).toEqual(true)
|
||||||
expect(collapsed.obj.arr[2][META].expanded).toEqual(false)
|
expect(expanded.obj.arr[2][EXPANDED]).toEqual(true)
|
||||||
|
|
||||||
let orig = collapsed
|
|
||||||
orig = deleteIn(orig, ['obj'].concat([META, 'expanded']))
|
|
||||||
orig = deleteIn(orig, ['obj', 'arr'].concat([META, 'expanded']))
|
|
||||||
orig = deleteIn(orig, ['obj', 'arr', 2].concat([META, 'expanded']))
|
|
||||||
assertDeepEqualEson(orig, eson)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('expand a callback should not change the object when nothing happens', () => {
|
test('expand a callback should not change the object when nothing happens', () => {
|
||||||
const eson = jsonToEson({a: [1,2,3], b: {c: 4}})
|
const eson = syncEson({a: [1,2,3], b: {c: 4}})
|
||||||
function callback (path) {
|
function callback (path) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
@ -141,69 +136,8 @@ test('expand a callback should not change the object when nothing happens', () =
|
||||||
expect(collapsed).toBe(eson)
|
expect(collapsed).toBe(eson)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('transform (no change)', () => {
|
|
||||||
const eson = jsonToEson({a: [1,2,3], b: {c: 4}})
|
|
||||||
const updated = transform(eson, (value, path) => value)
|
|
||||||
assertDeepEqualEson(updated, eson)
|
|
||||||
expect(updated).toBe(eson)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('transform (change based on value)', () => {
|
|
||||||
const eson = jsonToEson({a: [1,2,3], b: {c: 4}})
|
|
||||||
|
|
||||||
const updated = transform(eson,
|
|
||||||
(value, path) => value[META].value === 2 ? jsonToEson(20, path) : value)
|
|
||||||
const expected = jsonToEson({a: [1,20,3], b: {c: 4}})
|
|
||||||
|
|
||||||
assertDeepEqualEson(updated, expected)
|
|
||||||
expect(updated.b).toBe(eson.b) // should not have replaced b
|
|
||||||
})
|
|
||||||
|
|
||||||
test('transform (change based on path)', () => {
|
|
||||||
const eson = jsonToEson({a: [1,2,3], b: {c: 4}})
|
|
||||||
|
|
||||||
const updated = transform(eson,
|
|
||||||
(value, path) => path.join('.') === 'a.1' ? jsonToEson(20, path) : value)
|
|
||||||
const expected = jsonToEson({a: [1,20,3], b: {c: 4}})
|
|
||||||
|
|
||||||
assertDeepEqualEson(updated, expected)
|
|
||||||
expect(updated.b).toBe(eson.b) // should not have replaced b
|
|
||||||
})
|
|
||||||
|
|
||||||
test('pathExists', () => {
|
|
||||||
const eson = jsonToEson({
|
|
||||||
"obj": {
|
|
||||||
"arr": [1,2, {"first":3,"last":4}]
|
|
||||||
},
|
|
||||||
"str": "hello world",
|
|
||||||
"nill": null,
|
|
||||||
"bool": false
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(pathExists(eson, ['obj', 'arr', 2, 'first'])).toEqual(true)
|
|
||||||
expect(pathExists(eson, ['obj', 'foo'])).toEqual(false)
|
|
||||||
expect(pathExists(eson, ['obj', 'foo', 'bar'])).toEqual(false)
|
|
||||||
expect(pathExists(eson, [])).toEqual(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('parseJSONPointer', () => {
|
|
||||||
expect(parseJSONPointer('/obj/a')).toEqual(['obj', 'a'])
|
|
||||||
expect(parseJSONPointer('/arr/-')).toEqual(['arr', '-'])
|
|
||||||
expect(parseJSONPointer('/foo/~1~0 ~0~1')).toEqual(['foo', '/~ ~/'])
|
|
||||||
expect(parseJSONPointer('/obj')).toEqual(['obj'])
|
|
||||||
expect(parseJSONPointer('/')).toEqual([''])
|
|
||||||
expect(parseJSONPointer('')).toEqual([])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('compileJSONPointer', () => {
|
|
||||||
expect(compileJSONPointer(['foo', 'bar'])).toEqual('/foo/bar')
|
|
||||||
expect(compileJSONPointer(['foo', '/~ ~/'])).toEqual('/foo/~1~0 ~0~1')
|
|
||||||
expect(compileJSONPointer([''])).toEqual('/')
|
|
||||||
expect(compileJSONPointer([])).toEqual('')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('add and remove errors', () => {
|
test('add and remove errors', () => {
|
||||||
const eson = jsonToEson({
|
const eson = syncEson({
|
||||||
"obj": {
|
"obj": {
|
||||||
"arr": [1,2, {"first":3,"last":4}]
|
"arr": [1,2, {"first":3,"last":4}]
|
||||||
},
|
},
|
||||||
|
@ -220,9 +154,13 @@ test('add and remove errors', () => {
|
||||||
const actual1 = applyErrors(eson, jsonSchemaErrors)
|
const actual1 = applyErrors(eson, jsonSchemaErrors)
|
||||||
|
|
||||||
let expected = eson
|
let expected = eson
|
||||||
expected = setIn(expected, ['obj', 'arr', '2', 'last', META, 'error'], jsonSchemaErrors[0])
|
expected = setIn(expected, ['obj', 'arr', '2', 'last', ERROR], jsonSchemaErrors[0])
|
||||||
expected = setIn(expected, ['nill', META, 'error'], jsonSchemaErrors[1])
|
expected = setIn(expected, ['nill', ERROR], jsonSchemaErrors[1])
|
||||||
assertDeepEqualEson(actual1, expected)
|
|
||||||
|
console.log(actual1)
|
||||||
|
console.log(expected)
|
||||||
|
|
||||||
|
assertEqualEson(actual1, expected)
|
||||||
|
|
||||||
// re-applying the same errors should not change eson
|
// re-applying the same errors should not change eson
|
||||||
const actual2 = applyErrors(actual1, jsonSchemaErrors)
|
const actual2 = applyErrors(actual1, jsonSchemaErrors)
|
||||||
|
@ -230,12 +168,12 @@ test('add and remove errors', () => {
|
||||||
|
|
||||||
// clear errors
|
// clear errors
|
||||||
const actual3 = applyErrors(actual2, [])
|
const actual3 = applyErrors(actual2, [])
|
||||||
assertDeepEqualEson(actual3, eson)
|
assertEqualEson(actual3, eson)
|
||||||
expect(actual3.str).toEqual(eson.str) // shouldn't have touched values not affected by the errors
|
expect(actual3.str).toEqual(eson.str) // shouldn't have touched values not affected by the errors
|
||||||
})
|
})
|
||||||
|
|
||||||
test('search', () => {
|
test('search', () => {
|
||||||
const eson = jsonToEson({
|
const eson = syncEson({
|
||||||
"obj": {
|
"obj": {
|
||||||
"arr": [1,2, {"first":3,"last":4}]
|
"arr": [1,2, {"first":3,"last":4}]
|
||||||
},
|
},
|
||||||
|
@ -259,18 +197,18 @@ test('search', () => {
|
||||||
expect(active).toEqual({path: ['obj', 'arr', '2', 'last'], area: 'property'})
|
expect(active).toEqual({path: ['obj', 'arr', '2', 'last'], area: 'property'})
|
||||||
|
|
||||||
let expected = esonWithSearch
|
let expected = esonWithSearch
|
||||||
expected = setIn(expected, ['obj', 'arr', '2', 'last', META, 'searchProperty'], 'active')
|
expected = setIn(expected, ['obj', 'arr', '2', 'last', SEARCH_PROPERTY], 'active')
|
||||||
expected = setIn(expected, ['str', META, 'searchValue'], 'normal')
|
expected = setIn(expected, ['str', SEARCH_VALUE], 'normal')
|
||||||
expected = setIn(expected, ['nill', META, 'searchProperty'], 'normal')
|
expected = setIn(expected, ['nill', SEARCH_PROPERTY], 'normal')
|
||||||
expected = setIn(expected, ['nill', META, 'searchValue'], 'normal')
|
expected = setIn(expected, ['nill', SEARCH_VALUE], 'normal')
|
||||||
expected = setIn(expected, ['bool', META, 'searchProperty'], 'normal')
|
expected = setIn(expected, ['bool', SEARCH_PROPERTY], 'normal')
|
||||||
expected = setIn(expected, ['bool', META, 'searchValue'], 'normal')
|
expected = setIn(expected, ['bool', SEARCH_VALUE], 'normal')
|
||||||
|
|
||||||
assertDeepEqualEson(esonWithSearch, expected)
|
assertEqualEson(esonWithSearch, expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('search number', () => {
|
test('search number', () => {
|
||||||
const eson = jsonToEson({
|
const eson = syncEson({
|
||||||
"2": "two",
|
"2": "two",
|
||||||
"arr": ["a", "b", "c", "2"]
|
"arr": ["a", "b", "c", "2"]
|
||||||
})
|
})
|
||||||
|
@ -285,7 +223,7 @@ test('search number', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('nextSearchResult', () => {
|
test('nextSearchResult', () => {
|
||||||
const eson = jsonToEson({
|
const eson = syncEson({
|
||||||
"obj": {
|
"obj": {
|
||||||
"arr": [1,2, {"first":3,"last":4}]
|
"arr": [1,2, {"first":3,"last":4}]
|
||||||
},
|
},
|
||||||
|
@ -302,31 +240,31 @@ test('nextSearchResult', () => {
|
||||||
])
|
])
|
||||||
|
|
||||||
expect(first.searchResult.active).toEqual({path: ['obj', 'arr'], area: 'property'})
|
expect(first.searchResult.active).toEqual({path: ['obj', 'arr'], area: 'property'})
|
||||||
expect(getIn(first.eson, ['obj', 'arr', META, 'searchProperty'])).toEqual('active')
|
expect(getIn(first.eson, ['obj', 'arr', SEARCH_PROPERTY])).toEqual('active')
|
||||||
expect(getIn(first.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty'])).toEqual('normal')
|
expect(getIn(first.eson, ['obj', 'arr', '2', 'last', SEARCH_PROPERTY])).toEqual('normal')
|
||||||
expect(getIn(first.eson, ['bool', META, 'searchValue'])).toEqual('normal')
|
expect(getIn(first.eson, ['bool', SEARCH_VALUE])).toEqual('normal')
|
||||||
|
|
||||||
const second = nextSearchResult(first.eson, first.searchResult)
|
const second = nextSearchResult(first.eson, first.searchResult)
|
||||||
expect(second.searchResult.active).toEqual({path: ['obj', 'arr', '2', 'last'], area: 'property'})
|
expect(second.searchResult.active).toEqual({path: ['obj', 'arr', '2', 'last'], area: 'property'})
|
||||||
expect(getIn(second.eson, ['obj', 'arr', META, 'searchProperty'])).toEqual('normal')
|
expect(getIn(second.eson, ['obj', 'arr', SEARCH_PROPERTY])).toEqual('normal')
|
||||||
expect(getIn(second.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty'])).toEqual('active')
|
expect(getIn(second.eson, ['obj', 'arr', '2', 'last', SEARCH_PROPERTY])).toEqual('active')
|
||||||
expect(getIn(second.eson, ['bool', META, 'searchValue'])).toEqual('normal')
|
expect(getIn(second.eson, ['bool', SEARCH_VALUE])).toEqual('normal')
|
||||||
|
|
||||||
const third = nextSearchResult(second.eson, second.searchResult)
|
const third = nextSearchResult(second.eson, second.searchResult)
|
||||||
expect(third.searchResult.active).toEqual({path: ['bool'], area: 'value'})
|
expect(third.searchResult.active).toEqual({path: ['bool'], area: 'value'})
|
||||||
expect(getIn(third.eson, ['obj', 'arr', META, 'searchProperty'])).toEqual('normal')
|
expect(getIn(third.eson, ['obj', 'arr', SEARCH_PROPERTY])).toEqual('normal')
|
||||||
expect(getIn(third.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty'])).toEqual('normal')
|
expect(getIn(third.eson, ['obj', 'arr', '2', 'last', SEARCH_PROPERTY])).toEqual('normal')
|
||||||
expect(getIn(third.eson, ['bool', META, 'searchValue'])).toEqual('active')
|
expect(getIn(third.eson, ['bool', SEARCH_VALUE])).toEqual('active')
|
||||||
|
|
||||||
const wrappedAround = nextSearchResult(third.eson, third.searchResult)
|
const wrappedAround = nextSearchResult(third.eson, third.searchResult)
|
||||||
expect(wrappedAround.searchResult.active).toEqual({path: ['obj', 'arr'], area: 'property'})
|
expect(wrappedAround.searchResult.active).toEqual({path: ['obj', 'arr'], area: 'property'})
|
||||||
expect(getIn(wrappedAround.eson, ['obj', 'arr', META, 'searchProperty'])).toEqual('active')
|
expect(getIn(wrappedAround.eson, ['obj', 'arr', SEARCH_PROPERTY])).toEqual('active')
|
||||||
expect(getIn(wrappedAround.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty'])).toEqual('normal')
|
expect(getIn(wrappedAround.eson, ['obj', 'arr', '2', 'last', SEARCH_PROPERTY])).toEqual('normal')
|
||||||
expect(getIn(wrappedAround.eson, ['bool', META, 'searchValue'])).toEqual('normal')
|
expect(getIn(wrappedAround.eson, ['bool', SEARCH_VALUE])).toEqual('normal')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('previousSearchResult', () => {
|
test('previousSearchResult', () => {
|
||||||
const eson = jsonToEson({
|
const eson = syncEson({
|
||||||
"obj": {
|
"obj": {
|
||||||
"arr": [1,2, {"first":3,"last":4}]
|
"arr": [1,2, {"first":3,"last":4}]
|
||||||
},
|
},
|
||||||
|
@ -343,31 +281,31 @@ test('previousSearchResult', () => {
|
||||||
])
|
])
|
||||||
|
|
||||||
expect(init.searchResult.active).toEqual({path: ['obj', 'arr'], area: 'property'})
|
expect(init.searchResult.active).toEqual({path: ['obj', 'arr'], area: 'property'})
|
||||||
expect(getIn(init.eson, ['obj', 'arr', META, 'searchProperty'])).toEqual('active')
|
expect(getIn(init.eson, ['obj', 'arr', SEARCH_PROPERTY])).toEqual('active')
|
||||||
expect(getIn(init.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty'])).toEqual('normal')
|
expect(getIn(init.eson, ['obj', 'arr', '2', 'last', SEARCH_PROPERTY])).toEqual('normal')
|
||||||
expect(getIn(init.eson, ['bool', META, 'searchValue'])).toEqual('normal')
|
expect(getIn(init.eson, ['bool', SEARCH_VALUE])).toEqual('normal')
|
||||||
|
|
||||||
const third = previousSearchResult(init.eson, init.searchResult)
|
const third = previousSearchResult(init.eson, init.searchResult)
|
||||||
expect(third.searchResult.active).toEqual({path: ['bool'], area: 'value'})
|
expect(third.searchResult.active).toEqual({path: ['bool'], area: 'value'})
|
||||||
expect(getIn(third.eson, ['obj', 'arr', META, 'searchProperty'])).toEqual('normal')
|
expect(getIn(third.eson, ['obj', 'arr', SEARCH_PROPERTY])).toEqual('normal')
|
||||||
expect(getIn(third.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty'])).toEqual('normal')
|
expect(getIn(third.eson, ['obj', 'arr', '2', 'last', SEARCH_PROPERTY])).toEqual('normal')
|
||||||
expect(getIn(third.eson, ['bool', META, 'searchValue'])).toEqual('active')
|
expect(getIn(third.eson, ['bool', SEARCH_VALUE])).toEqual('active')
|
||||||
|
|
||||||
const second = previousSearchResult(third.eson, third.searchResult)
|
const second = previousSearchResult(third.eson, third.searchResult)
|
||||||
expect(second.searchResult.active).toEqual({path: ['obj', 'arr', '2', 'last'], area: 'property'})
|
expect(second.searchResult.active).toEqual({path: ['obj', 'arr', '2', 'last'], area: 'property'})
|
||||||
expect(getIn(second.eson, ['obj', 'arr', META, 'searchProperty'])).toEqual('normal')
|
expect(getIn(second.eson, ['obj', 'arr', SEARCH_PROPERTY])).toEqual('normal')
|
||||||
expect(getIn(second.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty'])).toEqual('active')
|
expect(getIn(second.eson, ['obj', 'arr', '2', 'last', SEARCH_PROPERTY])).toEqual('active')
|
||||||
expect(getIn(second.eson, ['bool', META, 'searchValue'])).toEqual('normal')
|
expect(getIn(second.eson, ['bool', SEARCH_VALUE])).toEqual('normal')
|
||||||
|
|
||||||
const first = previousSearchResult(second.eson, second.searchResult)
|
const first = previousSearchResult(second.eson, second.searchResult)
|
||||||
expect(first.searchResult.active).toEqual({path: ['obj', 'arr'], area: 'property'})
|
expect(first.searchResult.active).toEqual({path: ['obj', 'arr'], area: 'property'})
|
||||||
expect(getIn(first.eson, ['obj', 'arr', META, 'searchProperty'])).toEqual('active')
|
expect(getIn(first.eson, ['obj', 'arr', SEARCH_PROPERTY])).toEqual('active')
|
||||||
expect(getIn(first.eson, ['obj', 'arr', '2', 'last', META, 'searchProperty'])).toEqual('normal')
|
expect(getIn(first.eson, ['obj', 'arr', '2', 'last', SEARCH_PROPERTY])).toEqual('normal')
|
||||||
expect(getIn(first.eson, ['bool', META, 'searchValue'])).toEqual('normal')
|
expect(getIn(first.eson, ['bool', SEARCH_VALUE])).toEqual('normal')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('selection (object)', () => {
|
test('selection (object)', () => {
|
||||||
const eson = jsonToEson({
|
const eson = syncEson({
|
||||||
"obj": {
|
"obj": {
|
||||||
"arr": [1,2, {"first":3,"last":4}]
|
"arr": [1,2, {"first":3,"last":4}]
|
||||||
},
|
},
|
||||||
|
@ -383,10 +321,10 @@ test('selection (object)', () => {
|
||||||
const actual = applySelection(eson, selection)
|
const actual = applySelection(eson, selection)
|
||||||
|
|
||||||
let expected = eson
|
let expected = eson
|
||||||
expected = setIn(expected, ['obj', META, 'selected'], SELECTED + SELECTED_START + SELECTED_FIRST)
|
expected = setIn(expected, ['obj', SELECTION], SELECTED + SELECTED_START + SELECTED_FIRST)
|
||||||
expected = setIn(expected, ['str', META, 'selected'], SELECTED)
|
expected = setIn(expected, ['str', SELECTION], SELECTED)
|
||||||
expected = setIn(expected, ['nill', META, 'selected'], SELECTED + SELECTED_END + SELECTED_LAST)
|
expected = setIn(expected, ['nill', SELECTION], SELECTED + SELECTED_END + SELECTED_LAST)
|
||||||
assertDeepEqualEson(actual, expected)
|
assertEqualEson(actual, expected)
|
||||||
|
|
||||||
// test whether old selection results are cleaned up
|
// test whether old selection results are cleaned up
|
||||||
const selection2 = {
|
const selection2 = {
|
||||||
|
@ -395,13 +333,13 @@ test('selection (object)', () => {
|
||||||
}
|
}
|
||||||
const actual2 = applySelection(actual, selection2)
|
const actual2 = applySelection(actual, selection2)
|
||||||
let expected2 = eson
|
let expected2 = eson
|
||||||
expected2 = setIn(expected2, ['nill', META, 'selected'], SELECTED + SELECTED_START + SELECTED_FIRST)
|
expected2 = setIn(expected2, ['nill', SELECTION], SELECTED + SELECTED_START + SELECTED_FIRST)
|
||||||
expected2 = setIn(expected2, ['bool', META, 'selected'], SELECTED + SELECTED_END + SELECTED_LAST)
|
expected2 = setIn(expected2, ['bool', SELECTION], SELECTED + SELECTED_END + SELECTED_LAST)
|
||||||
assertDeepEqualEson(actual2, expected2)
|
assertEqualEson(actual2, expected2)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('selection (array)', () => {
|
test('selection (array)', () => {
|
||||||
const eson = jsonToEson({
|
const eson = syncEson({
|
||||||
"obj": {
|
"obj": {
|
||||||
"arr": [1,2, {"first":3,"last":4}]
|
"arr": [1,2, {"first":3,"last":4}]
|
||||||
},
|
},
|
||||||
|
@ -417,16 +355,16 @@ test('selection (array)', () => {
|
||||||
const actual = applySelection(eson, selection)
|
const actual = applySelection(eson, selection)
|
||||||
|
|
||||||
let expected = eson
|
let expected = eson
|
||||||
expected = setIn(expected, ['obj', 'arr', '0', META, 'selected'],
|
expected = setIn(expected, ['obj', 'arr', '0', SELECTION],
|
||||||
SELECTED + SELECTED_END + SELECTED_FIRST)
|
SELECTED + SELECTED_END + SELECTED_FIRST)
|
||||||
expected = setIn(expected, ['obj', 'arr', '1', META, 'selected'],
|
expected = setIn(expected, ['obj', 'arr', '1', SELECTION],
|
||||||
SELECTED + SELECTED_START + SELECTED_LAST)
|
SELECTED + SELECTED_START + SELECTED_LAST)
|
||||||
|
|
||||||
assertDeepEqualEson(actual, expected)
|
assertEqualEson(actual, expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('selection (value)', () => {
|
test('selection (value)', () => {
|
||||||
const eson = jsonToEson({
|
const eson = syncEson({
|
||||||
"obj": {
|
"obj": {
|
||||||
"arr": [1,2, {"first":3,"last":4}]
|
"arr": [1,2, {"first":3,"last":4}]
|
||||||
},
|
},
|
||||||
|
@ -440,13 +378,13 @@ test('selection (value)', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const actual = applySelection(eson, selection)
|
const actual = applySelection(eson, selection)
|
||||||
const expected = setIn(eson, ['obj', 'arr', '2', 'first', META, 'selected'],
|
const expected = setIn(eson, ['obj', 'arr', '2', 'first', SELECTION],
|
||||||
SELECTED + SELECTED_START + SELECTED_END + SELECTED_FIRST + SELECTED_LAST)
|
SELECTED + SELECTED_START + SELECTED_END + SELECTED_FIRST + SELECTED_LAST)
|
||||||
assertDeepEqualEson(actual, expected)
|
assertEqualEson(actual, expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('selection (node)', () => {
|
test('selection (node)', () => {
|
||||||
const eson = jsonToEson({
|
const eson = syncEson({
|
||||||
"obj": {
|
"obj": {
|
||||||
"arr": [1,2, {"first":3,"last":4}]
|
"arr": [1,2, {"first":3,"last":4}]
|
||||||
},
|
},
|
||||||
|
@ -460,26 +398,26 @@ test('selection (node)', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const actual = applySelection(eson, selection)
|
const actual = applySelection(eson, selection)
|
||||||
const expected = setIn(eson, ['obj', 'arr', META, 'selected'],
|
const expected = setIn(eson, ['obj', 'arr', SELECTION],
|
||||||
SELECTED + SELECTED_START + SELECTED_END + SELECTED_FIRST + SELECTED_LAST)
|
SELECTED + SELECTED_START + SELECTED_END + SELECTED_FIRST + SELECTED_LAST)
|
||||||
assertDeepEqualEson(actual, expected)
|
assertEqualEson(actual, expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('pathsFromSelection (object)', () => {
|
test('pathsFromSelection (object)', () => {
|
||||||
const eson = jsonToEson({
|
const json = {
|
||||||
"obj": {
|
"obj": {
|
||||||
"arr": [1,2, {"first":3,"last":4}]
|
"arr": [1,2, {"first":3,"last":4}]
|
||||||
},
|
},
|
||||||
"str": "hello world",
|
"str": "hello world",
|
||||||
"nill": null,
|
"nill": null,
|
||||||
"bool": false
|
"bool": false
|
||||||
})
|
}
|
||||||
const selection = {
|
const selection = {
|
||||||
start: ['obj', 'arr', '2', 'last'],
|
start: ['obj', 'arr', '2', 'last'],
|
||||||
end: ['nill']
|
end: ['nill']
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(pathsFromSelection(eson, selection)).toEqual([
|
expect(pathsFromSelection(json, selection)).toEqual([
|
||||||
['obj'],
|
['obj'],
|
||||||
['str'],
|
['str'],
|
||||||
['nill']
|
['nill']
|
||||||
|
@ -487,138 +425,56 @@ test('pathsFromSelection (object)', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('pathsFromSelection (array)', () => {
|
test('pathsFromSelection (array)', () => {
|
||||||
const eson = jsonToEson({
|
const json = {
|
||||||
"obj": {
|
"obj": {
|
||||||
"arr": [1,2, {"first":3,"last":4}]
|
"arr": [1,2, {"first":3,"last":4}]
|
||||||
},
|
},
|
||||||
"str": "hello world",
|
"str": "hello world",
|
||||||
"nill": null,
|
"nill": null,
|
||||||
"bool": false
|
"bool": false
|
||||||
})
|
}
|
||||||
const selection = {
|
const selection = {
|
||||||
start: ['obj', 'arr', '1'],
|
start: ['obj', 'arr', '1'],
|
||||||
end: ['obj', 'arr', '0'] // note the "wrong" order of start and end
|
end: ['obj', 'arr', '0'] // note the "backward" order of start and end
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(pathsFromSelection(eson, selection)).toEqual([
|
expect(pathsFromSelection(json, selection)).toEqual([
|
||||||
['obj', 'arr', '0'],
|
['obj', 'arr', '0'],
|
||||||
['obj', 'arr', '1']
|
['obj', 'arr', '1']
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('pathsFromSelection (value)', () => {
|
test('pathsFromSelection (value)', () => {
|
||||||
const eson = jsonToEson({
|
const json = {
|
||||||
"obj": {
|
"obj": {
|
||||||
"arr": [1,2, {"first":3,"last":4}]
|
"arr": [1,2, {"first":3,"last":4}]
|
||||||
},
|
},
|
||||||
"str": "hello world",
|
"str": "hello world",
|
||||||
"nill": null,
|
"nill": null,
|
||||||
"bool": false
|
"bool": false
|
||||||
})
|
}
|
||||||
const selection = {
|
const selection = {
|
||||||
start: ['obj', 'arr', '2', 'first'],
|
start: ['obj', 'arr', '2', 'first'],
|
||||||
end: ['obj', 'arr', '2', 'first']
|
end: ['obj', 'arr', '2', 'first']
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(pathsFromSelection(eson, selection)).toEqual([
|
expect(pathsFromSelection(json, selection)).toEqual([
|
||||||
['obj', 'arr', '2', 'first'],
|
['obj', 'arr', '2', 'first'],
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('pathsFromSelection (before)', () => {
|
|
||||||
const eson = jsonToEson({
|
|
||||||
"obj": {
|
|
||||||
"arr": [1,2, {"first":3,"last":4}]
|
|
||||||
},
|
|
||||||
"str": "hello world",
|
|
||||||
"nill": null,
|
|
||||||
"bool": false
|
|
||||||
})
|
|
||||||
const selection = {
|
|
||||||
after: ['obj', 'arr', '2', 'first']
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(pathsFromSelection(eson, selection)).toEqual([])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('pathsFromSelection (after)', () => {
|
test('pathsFromSelection (after)', () => {
|
||||||
const eson = jsonToEson({
|
const json = {
|
||||||
"obj": {
|
"obj": {
|
||||||
"arr": [1,2, {"first":3,"last":4}]
|
"arr": [1,2, {"first":3,"last":4}]
|
||||||
},
|
},
|
||||||
"str": "hello world",
|
"str": "hello world",
|
||||||
"nill": null,
|
"nill": null,
|
||||||
"bool": false
|
"bool": false
|
||||||
})
|
}
|
||||||
const selection = {
|
const selection = {
|
||||||
after: ['obj', 'arr', '2', 'first']
|
after: ['obj', 'arr', '2', 'first']
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(pathsFromSelection(eson, selection)).toEqual([])
|
expect(pathsFromSelection(json, selection)).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('getEsonState', () => {
|
|
||||||
const eson = jsonToEson({
|
|
||||||
"obj": {
|
|
||||||
"arr": ["1",2, {"first":3,"last":4}]
|
|
||||||
},
|
|
||||||
"str": "hello world",
|
|
||||||
"nill": null,
|
|
||||||
"bool": false
|
|
||||||
})
|
|
||||||
|
|
||||||
eson.obj[META].expanded = true
|
|
||||||
eson.obj.arr[META].expanded = false
|
|
||||||
eson.obj.arr[0][META].type = 'string'
|
|
||||||
eson.obj.arr[2][META].expanded = true
|
|
||||||
|
|
||||||
const state = getEsonState(eson)
|
|
||||||
|
|
||||||
expect(state).toEqual({
|
|
||||||
'/obj': { expanded: true },
|
|
||||||
'/obj/arr/0': { type: 'string' },
|
|
||||||
'/obj/arr/2': { expanded: true },
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO: test applyEsonState
|
|
||||||
|
|
||||||
// helper function to print JSON in the console
|
|
||||||
function printJSON (json, message = null) {
|
|
||||||
if (message) {
|
|
||||||
console.log(message)
|
|
||||||
}
|
|
||||||
console.log(JSON.stringify(json, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
function printESON (eson, message = null) {
|
|
||||||
if (message) {
|
|
||||||
console.log(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = []
|
|
||||||
|
|
||||||
transform(eson, function (value, path) {
|
|
||||||
// const strPath = padEnd(, 20)
|
|
||||||
// console.log(`${strPath} ${'value' in value[META] ? value[META].value : ''} ${JSON.stringify(value[META])}`)
|
|
||||||
|
|
||||||
data.push({
|
|
||||||
path: '[' + path.join(', ') + ']',
|
|
||||||
value: repeat(' ', path.length) + (value[META].type === 'Object'
|
|
||||||
? '{...}'
|
|
||||||
: value[META].type === 'Array'
|
|
||||||
? '[...]'
|
|
||||||
: JSON.stringify(value[META].value)),
|
|
||||||
meta: JSON.stringify(value[META])
|
|
||||||
})
|
|
||||||
|
|
||||||
return value
|
|
||||||
})
|
|
||||||
|
|
||||||
console.table(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper function to load a JSON file
|
|
||||||
function loadJSON (filename) {
|
|
||||||
return JSON.parse(readFileSync(__dirname + '/' + filename, 'utf-8'))
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,287 @@
|
||||||
|
import isEqual from 'lodash/isEqual'
|
||||||
|
import initial from 'lodash/initial'
|
||||||
|
|
||||||
|
import { setIn, getIn, deleteIn, insertAt, existsIn } from './utils/immutabilityHelpers'
|
||||||
|
import { parseJSONPointer, compileJSONPointer } from './jsonPointer'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a patch to a JSON object
|
||||||
|
* The original JSON object will not be changed,
|
||||||
|
* instead, the patch is applied in an immutable way
|
||||||
|
* @param {JSON} json
|
||||||
|
* @param {JSONPatch} patch Array with JSON patch actions
|
||||||
|
* @return {{json: JSON, revert: JSONPatch, error: Error | null}}
|
||||||
|
*/
|
||||||
|
export function immutableJsonPatch (json, patch) {
|
||||||
|
let updatedJson = json
|
||||||
|
let revert = []
|
||||||
|
|
||||||
|
for (let i = 0; i < patch.length; i++) {
|
||||||
|
const action = patch[i]
|
||||||
|
const path = action.path ? parseJSONPointer(action.path) : null
|
||||||
|
const from = action.from ? parseJSONPointer(action.from) : null
|
||||||
|
|
||||||
|
switch (action.op) {
|
||||||
|
case 'add': {
|
||||||
|
const result = add(updatedJson, path, action.value)
|
||||||
|
updatedJson = result.json
|
||||||
|
revert = result.revert.concat(revert)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'remove': {
|
||||||
|
const result = remove(updatedJson, path)
|
||||||
|
updatedJson = result.json
|
||||||
|
revert = result.revert.concat(revert)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'replace': {
|
||||||
|
const result = replace(updatedJson, path, action.value)
|
||||||
|
updatedJson = result.json
|
||||||
|
revert = result.revert.concat(revert)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'copy': {
|
||||||
|
if (!action.from) {
|
||||||
|
return {
|
||||||
|
json: updatedJson,
|
||||||
|
revert: [],
|
||||||
|
error: new Error('Property "from" expected in copy action ' + JSON.stringify(action))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = copy(updatedJson, path, from)
|
||||||
|
updatedJson = result.json
|
||||||
|
revert = result.revert.concat(revert)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'move': {
|
||||||
|
if (!action.from) {
|
||||||
|
return {
|
||||||
|
json: updatedJson,
|
||||||
|
revert: [],
|
||||||
|
error: new Error('Property "from" expected in move action ' + JSON.stringify(action))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = move(updatedJson, path, from)
|
||||||
|
updatedJson = result.json
|
||||||
|
revert = result.revert.concat(revert)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'test': {
|
||||||
|
// when a test fails, cancel the whole patch and return the error
|
||||||
|
const error = test(updatedJson, path, action.value)
|
||||||
|
if (error) {
|
||||||
|
return { json, revert: [], error}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
// unknown patch operation. Cancel the whole patch and return an error
|
||||||
|
return {
|
||||||
|
json,
|
||||||
|
revert: [],
|
||||||
|
error: new Error('Unknown JSONPatch op ' + JSON.stringify(action.op))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
json: updatedJson,
|
||||||
|
revert,
|
||||||
|
error: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace an existing item
|
||||||
|
* @param {JSON} json
|
||||||
|
* @param {Path} path
|
||||||
|
* @param {JSON} value
|
||||||
|
* @return {{json: JSON, revert: JSONPatch}}
|
||||||
|
*/
|
||||||
|
export function replace (json, path, value) {
|
||||||
|
const oldValue = getIn(json, path)
|
||||||
|
|
||||||
|
return {
|
||||||
|
json: setIn(json, path, value),
|
||||||
|
revert: [{
|
||||||
|
op: 'replace',
|
||||||
|
path: compileJSONPointer(path),
|
||||||
|
value: oldValue
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an item or property
|
||||||
|
* @param {JSON} json
|
||||||
|
* @param {Path} path
|
||||||
|
* @return {{json: JSON, revert: JSONPatch}}
|
||||||
|
*/
|
||||||
|
export function remove (json, path) {
|
||||||
|
const oldValue = getIn(json, path)
|
||||||
|
|
||||||
|
return {
|
||||||
|
json: deleteIn(json, path),
|
||||||
|
revert: [{
|
||||||
|
op: 'add',
|
||||||
|
path: compileJSONPointer(path),
|
||||||
|
value: oldValue
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {JSON} json
|
||||||
|
* @param {Path} path
|
||||||
|
* @param {JSON} value
|
||||||
|
* @return {{json: JSON, revert: JSONPatch}}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export function add (json, path, value) {
|
||||||
|
const resolvedPath = resolvePathIndex(json, path)
|
||||||
|
const parent = getIn(json, initial(path))
|
||||||
|
const parentIsArray = Array.isArray(parent)
|
||||||
|
|
||||||
|
const updatedJson = parentIsArray
|
||||||
|
? insertAt(json, resolvedPath, value)
|
||||||
|
: setIn(json, resolvedPath, value)
|
||||||
|
|
||||||
|
if (!parentIsArray && existsIn(json, resolvedPath)) {
|
||||||
|
const oldValue = getIn(json, resolvedPath)
|
||||||
|
|
||||||
|
return {
|
||||||
|
json: updatedJson,
|
||||||
|
revert: [{
|
||||||
|
op: 'replace',
|
||||||
|
path: compileJSONPointer(resolvedPath),
|
||||||
|
value: oldValue
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return {
|
||||||
|
json: updatedJson,
|
||||||
|
revert: [{
|
||||||
|
op: 'remove',
|
||||||
|
path: compileJSONPointer(resolvedPath)
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy a value
|
||||||
|
* @param {JSON} json
|
||||||
|
* @param {Path} path
|
||||||
|
* @param {Path} from
|
||||||
|
* @return {{json: JSON, revert: ESONPatch}}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export function copy (json, path, from) {
|
||||||
|
const value = getIn(json, from)
|
||||||
|
|
||||||
|
return add(json, path, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a value
|
||||||
|
* @param {JSON} json
|
||||||
|
* @param {Path} path
|
||||||
|
* @param {Path} from
|
||||||
|
* @return {{json: JSON, revert: ESONPatch}}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export function move (json, path, from) {
|
||||||
|
const resolvedPath = resolvePathIndex(json, path)
|
||||||
|
const parent = getIn(json, initial(path))
|
||||||
|
const parentIsArray = Array.isArray(parent)
|
||||||
|
const oldValue = getIn(json, path)
|
||||||
|
const value = getIn(json, from)
|
||||||
|
|
||||||
|
const removedJson = remove(json, from).json
|
||||||
|
const updatedJson = parentIsArray
|
||||||
|
? insertAt(removedJson, resolvedPath, value)
|
||||||
|
: setIn(removedJson, resolvedPath, value)
|
||||||
|
|
||||||
|
if (oldValue !== undefined && !parentIsArray) {
|
||||||
|
// replaces an existing value in an object
|
||||||
|
return {
|
||||||
|
json: updatedJson,
|
||||||
|
revert: [
|
||||||
|
{
|
||||||
|
op: 'move',
|
||||||
|
from: compileJSONPointer(resolvedPath),
|
||||||
|
path: compileJSONPointer(from)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
op: 'add',
|
||||||
|
path: compileJSONPointer(resolvedPath),
|
||||||
|
value: oldValue
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return {
|
||||||
|
json: updatedJson,
|
||||||
|
revert: [
|
||||||
|
{
|
||||||
|
op: 'move',
|
||||||
|
from: compileJSONPointer(resolvedPath),
|
||||||
|
path: compileJSONPointer(from)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test whether the data contains the provided value at the specified path.
|
||||||
|
* Throws an error when the test fails.
|
||||||
|
* @param {JSON} json
|
||||||
|
* @param {Path} path
|
||||||
|
* @param {*} value
|
||||||
|
* @return {null | Error} Returns an error when the tests, returns null otherwise
|
||||||
|
*/
|
||||||
|
export function test (json, path, value) {
|
||||||
|
if (value === undefined) {
|
||||||
|
return new Error('Test failed, no value provided')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsIn(json, path)) {
|
||||||
|
return new Error('Test failed, path not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
const actualValue = getIn(json, path)
|
||||||
|
if (!isEqual(actualValue, value)) {
|
||||||
|
return new Error('Test failed, value differs')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the path of an index like '''
|
||||||
|
* @param {JSON} json
|
||||||
|
* @param {Path} path
|
||||||
|
* @returns {Path} Returns the resolved path
|
||||||
|
*/
|
||||||
|
export function resolvePathIndex (json, path) {
|
||||||
|
const parent = getIn(json, initial(path))
|
||||||
|
|
||||||
|
return (path[path.length - 1] === '-')
|
||||||
|
? path.slice(0, path.length - 1).concat(parent.length)
|
||||||
|
: path
|
||||||
|
}
|
|
@ -0,0 +1,334 @@
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
import { immutableJsonPatch } from './immutableJsonPatch'
|
||||||
|
|
||||||
|
test('test toBe', () => {
|
||||||
|
const a = { x: 2 }
|
||||||
|
const b = { x: 2 }
|
||||||
|
|
||||||
|
// just to be sure toBe does what I think it does...
|
||||||
|
expect(a).toBe(a)
|
||||||
|
expect(b).not.toBe(a)
|
||||||
|
expect(b).toEqual(a)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('jsonpatch add', () => {
|
||||||
|
const json = {
|
||||||
|
arr: [1,2,3],
|
||||||
|
obj: {a : 2}
|
||||||
|
}
|
||||||
|
|
||||||
|
const patch = [
|
||||||
|
{op: 'add', path: '/obj/b', value: {foo: 'bar'}}
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = immutableJsonPatch(json, patch)
|
||||||
|
|
||||||
|
expect(result.json).toEqual({
|
||||||
|
arr: [1,2,3],
|
||||||
|
obj: {a : 2, b: {foo: 'bar'}}
|
||||||
|
})
|
||||||
|
expect(result.revert).toEqual([
|
||||||
|
{op: 'remove', path: '/obj/b'}
|
||||||
|
])
|
||||||
|
expect(result.json.arr).toBe(json.arr)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('jsonpatch add: insert in matrix', () => {
|
||||||
|
const json = {
|
||||||
|
arr: [1,2,3],
|
||||||
|
obj: {a : 2}
|
||||||
|
}
|
||||||
|
|
||||||
|
const patch = [
|
||||||
|
{op: 'add', path: '/arr/1', value: 4}
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = immutableJsonPatch(json, patch)
|
||||||
|
|
||||||
|
expect(result.json).toEqual({
|
||||||
|
arr: [1,4,2,3],
|
||||||
|
obj: {a : 2}
|
||||||
|
})
|
||||||
|
expect(result.revert).toEqual([
|
||||||
|
{op: 'remove', path: '/arr/1'}
|
||||||
|
])
|
||||||
|
expect(result.json.obj).toBe(json.obj)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('jsonpatch add: append to matrix', () => {
|
||||||
|
const json = {
|
||||||
|
arr: [1,2,3],
|
||||||
|
obj: {a : 2}
|
||||||
|
}
|
||||||
|
|
||||||
|
const patch = [
|
||||||
|
{op: 'add', path: '/arr/-', value: 4}
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = immutableJsonPatch(json, patch)
|
||||||
|
|
||||||
|
expect(result.json).toEqual({
|
||||||
|
arr: [1,2,3,4],
|
||||||
|
obj: {a : 2}
|
||||||
|
})
|
||||||
|
expect(result.revert).toEqual([
|
||||||
|
{op: 'remove', path: '/arr/3'}
|
||||||
|
])
|
||||||
|
expect(result.json.obj).toBe(json.obj)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('jsonpatch remove', () => {
|
||||||
|
const json = {
|
||||||
|
arr: [1,2,3],
|
||||||
|
obj: {a : 4},
|
||||||
|
unchanged: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const patch = [
|
||||||
|
{op: 'remove', path: '/obj/a'},
|
||||||
|
{op: 'remove', path: '/arr/1'},
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = immutableJsonPatch(json, patch)
|
||||||
|
|
||||||
|
expect(result.json).toEqual({
|
||||||
|
arr: [1,3],
|
||||||
|
obj: {},
|
||||||
|
unchanged: {}
|
||||||
|
})
|
||||||
|
expect(result.revert).toEqual([
|
||||||
|
{op: 'add', path: '/arr/1', value: 2},
|
||||||
|
{op: 'add', path: '/obj/a', value: 4}
|
||||||
|
])
|
||||||
|
|
||||||
|
// test revert
|
||||||
|
const result2 = immutableJsonPatch(result.json, result.revert)
|
||||||
|
|
||||||
|
expect(result2.json).toEqual(json)
|
||||||
|
expect(result2.revert).toEqual(patch)
|
||||||
|
expect(result.json.unchanged).toBe(json.unchanged)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('jsonpatch replace', () => {
|
||||||
|
const json = {
|
||||||
|
arr: [1,2,3],
|
||||||
|
obj: {a : 4},
|
||||||
|
unchanged: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const patch = [
|
||||||
|
{op: 'replace', path: '/obj/a', value: 400},
|
||||||
|
{op: 'replace', path: '/arr/1', value: 200},
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = immutableJsonPatch(json, patch)
|
||||||
|
|
||||||
|
expect(result.json).toEqual({
|
||||||
|
arr: [1,200,3],
|
||||||
|
obj: {a: 400},
|
||||||
|
unchanged: {}
|
||||||
|
})
|
||||||
|
expect(result.revert).toEqual([
|
||||||
|
{op: 'replace', path: '/arr/1', value: 2},
|
||||||
|
{op: 'replace', path: '/obj/a', value: 4}
|
||||||
|
])
|
||||||
|
|
||||||
|
// test revert
|
||||||
|
const result2 = immutableJsonPatch(result.json, result.revert)
|
||||||
|
|
||||||
|
expect(result2.json).toEqual(json)
|
||||||
|
expect(result2.revert).toEqual([
|
||||||
|
{op: 'replace', path: '/obj/a', value: 400},
|
||||||
|
{op: 'replace', path: '/arr/1', value: 200}
|
||||||
|
])
|
||||||
|
expect(result.json.unchanged).toBe(json.unchanged)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('jsonpatch copy', () => {
|
||||||
|
const json = {
|
||||||
|
arr: [1,2,3],
|
||||||
|
obj: {a : 4}
|
||||||
|
}
|
||||||
|
|
||||||
|
const patch = [
|
||||||
|
{op: 'copy', from: '/obj', path: '/arr/2'},
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = immutableJsonPatch(json, patch)
|
||||||
|
|
||||||
|
expect(result.json).toEqual({
|
||||||
|
arr: [1, 2, {a:4}, 3],
|
||||||
|
obj: {a: 4}
|
||||||
|
})
|
||||||
|
expect(result.revert).toEqual([
|
||||||
|
{op: 'remove', path: '/arr/2'}
|
||||||
|
])
|
||||||
|
|
||||||
|
// test revert
|
||||||
|
const result2 = immutableJsonPatch(result.json, result.revert)
|
||||||
|
|
||||||
|
expect(result2.json).toEqual(json)
|
||||||
|
expect(result2.revert).toEqual([
|
||||||
|
{op: 'add', path: '/arr/2', value: {a: 4}}
|
||||||
|
])
|
||||||
|
expect(result.json.obj).toBe(json.obj)
|
||||||
|
expect(result.json.arr[2]).toBe(json.obj)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('jsonpatch move', () => {
|
||||||
|
const json = {
|
||||||
|
arr: [1,2,3],
|
||||||
|
obj: {a : 4},
|
||||||
|
unchanged: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const patch = [
|
||||||
|
{op: 'move', from: '/obj', path: '/arr/2'},
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = immutableJsonPatch(json, patch)
|
||||||
|
|
||||||
|
expect(result.error).toEqual(null)
|
||||||
|
expect(result.json).toEqual({
|
||||||
|
arr: [1, 2, {a:4}, 3],
|
||||||
|
unchanged: {}
|
||||||
|
})
|
||||||
|
expect(result.revert).toEqual([
|
||||||
|
{op: 'move', from: '/arr/2', path: '/obj'}
|
||||||
|
])
|
||||||
|
|
||||||
|
// test revert
|
||||||
|
const result2 = immutableJsonPatch(result.json, result.revert)
|
||||||
|
|
||||||
|
expect(result2.json).toEqual(json)
|
||||||
|
expect(result2.revert).toEqual(patch)
|
||||||
|
expect(result.json.arr[2]).toBe(json.obj)
|
||||||
|
expect(result.json.unchanged).toBe(json.unchanged)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('jsonpatch move and replace', () => {
|
||||||
|
const json = { a: 2, b: 3 }
|
||||||
|
|
||||||
|
const patch = [
|
||||||
|
{op: 'move', from: '/a', path: '/b'},
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = immutableJsonPatch(json, patch)
|
||||||
|
|
||||||
|
expect(result.json).toEqual({ b : 2 })
|
||||||
|
expect(result.revert).toEqual([
|
||||||
|
{op:'move', from: '/b', path: '/a'},
|
||||||
|
{op:'add', path:'/b', value: 3}
|
||||||
|
])
|
||||||
|
|
||||||
|
// test revert
|
||||||
|
const result2 = immutableJsonPatch(result.json, result.revert)
|
||||||
|
|
||||||
|
expect(result2.json).toEqual(json)
|
||||||
|
expect(result2.revert).toEqual([
|
||||||
|
{op: 'remove', path: '/b'},
|
||||||
|
{op: 'move', from: '/a', path: '/b'}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('jsonpatch move and replace (nested)', () => {
|
||||||
|
const json = {
|
||||||
|
arr: [1,2,3],
|
||||||
|
obj: {a : 4},
|
||||||
|
unchanged: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const patch = [
|
||||||
|
{op: 'move', from: '/obj', path: '/arr'},
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = immutableJsonPatch(json, patch)
|
||||||
|
|
||||||
|
expect(result.json).toEqual({
|
||||||
|
arr: {a:4},
|
||||||
|
unchanged: {}
|
||||||
|
})
|
||||||
|
expect(result.revert).toEqual([
|
||||||
|
{op:'move', from: '/arr', path: '/obj'},
|
||||||
|
{op:'add', path:'/arr', value: [1,2,3]}
|
||||||
|
])
|
||||||
|
|
||||||
|
// test revert
|
||||||
|
const result2 = immutableJsonPatch(result.json, result.revert)
|
||||||
|
|
||||||
|
expect(result2.json).toEqual(json)
|
||||||
|
expect(result2.revert).toEqual([
|
||||||
|
{op: 'remove', path: '/arr'},
|
||||||
|
{op: 'move', from: '/obj', path: '/arr'}
|
||||||
|
])
|
||||||
|
expect(result.json.unchanged).toBe(json.unchanged)
|
||||||
|
expect(result2.json.unchanged).toBe(json.unchanged)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('jsonpatch test (ok)', () => {
|
||||||
|
const json = {
|
||||||
|
arr: [1,2,3],
|
||||||
|
obj: {a : 4}
|
||||||
|
}
|
||||||
|
|
||||||
|
const patch = [
|
||||||
|
{op: 'test', path: '/arr', value: [1,2,3]},
|
||||||
|
{op: 'add', path: '/added', value: 'ok'}
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = immutableJsonPatch(json, patch)
|
||||||
|
|
||||||
|
expect(result.json).toEqual({
|
||||||
|
arr: [1,2,3],
|
||||||
|
obj: {a : 4},
|
||||||
|
added: 'ok'
|
||||||
|
})
|
||||||
|
expect(result.revert).toEqual([
|
||||||
|
{op: 'remove', path: '/added'}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('jsonpatch test (fail: path not found)', () => {
|
||||||
|
const json = {
|
||||||
|
arr: [1,2,3],
|
||||||
|
obj: {a : 4}
|
||||||
|
}
|
||||||
|
|
||||||
|
const patch = [
|
||||||
|
{op: 'test', path: '/arr/5', value: [1,2,3]},
|
||||||
|
{op: 'add', path: '/added', value: 'ok'}
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = immutableJsonPatch(json, patch)
|
||||||
|
|
||||||
|
// patch shouldn't be applied
|
||||||
|
expect(result.json).toEqual({
|
||||||
|
arr: [1,2,3],
|
||||||
|
obj: {a : 4}
|
||||||
|
})
|
||||||
|
expect(result.revert).toEqual([])
|
||||||
|
expect(result.error.toString()).toEqual('Error: Test failed, path not found')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('jsonpatch test (fail: value not equal)', () => {
|
||||||
|
const json = {
|
||||||
|
arr: [1,2,3],
|
||||||
|
obj: {a : 4}
|
||||||
|
}
|
||||||
|
|
||||||
|
const patch = [
|
||||||
|
{op: 'test', path: '/obj', value: {a:4, b: 6}},
|
||||||
|
{op: 'add', path: '/added', value: 'ok'}
|
||||||
|
]
|
||||||
|
|
||||||
|
const result = immutableJsonPatch(json, patch)
|
||||||
|
|
||||||
|
// patch shouldn't be applied
|
||||||
|
expect(result.json).toEqual({
|
||||||
|
arr: [1,2,3],
|
||||||
|
obj: {a : 4}
|
||||||
|
})
|
||||||
|
expect(result.revert).toEqual([])
|
||||||
|
expect(result.error.toString()).toEqual('Error: Test failed, value differs')
|
||||||
|
})
|
|
@ -4,7 +4,7 @@ import JSONEditor from './components/JSONEditor'
|
||||||
import CodeMode from './components/CodeMode'
|
import CodeMode from './components/CodeMode'
|
||||||
import TextMode from './components/TextMode'
|
import TextMode from './components/TextMode'
|
||||||
import TreeMode from './components/TreeMode'
|
import TreeMode from './components/TreeMode'
|
||||||
import { compileJSONPointer, parseJSONPointer } from './eson'
|
import { compileJSONPointer, parseJSONPointer } from './jsonPointer'
|
||||||
|
|
||||||
const modes = {
|
const modes = {
|
||||||
code: CodeMode,
|
code: CodeMode,
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
/**
|
||||||
|
* Parse a JSON Pointer
|
||||||
|
* WARNING: this is not a complete implementation
|
||||||
|
* @param {string} pointer
|
||||||
|
* @return {Path}
|
||||||
|
*/
|
||||||
|
export function parseJSONPointer (pointer) {
|
||||||
|
const path = pointer.split('/')
|
||||||
|
path.shift() // remove the first empty entry
|
||||||
|
|
||||||
|
return path.map(p => p.replace(/~1/g, '/').replace(/~0/g, '~'))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compile a JSON Pointer
|
||||||
|
* WARNING: this is not a complete implementation
|
||||||
|
* @param {Path} path
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
export function compileJSONPointer (path) {
|
||||||
|
return path
|
||||||
|
.map(p => '/' + String(p).replace(/~/g, '~0').replace(/\//g, '~1'))
|
||||||
|
.join('')
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
import {compileJSONPointer, parseJSONPointer} from './jsonPointer'
|
||||||
|
|
||||||
|
test('parseJSONPointer', () => {
|
||||||
|
expect(parseJSONPointer('/obj/a')).toEqual(['obj', 'a'])
|
||||||
|
expect(parseJSONPointer('/arr/-')).toEqual(['arr', '-'])
|
||||||
|
expect(parseJSONPointer('/foo/~1~0 ~0~1')).toEqual(['foo', '/~ ~/'])
|
||||||
|
expect(parseJSONPointer('/obj')).toEqual(['obj'])
|
||||||
|
expect(parseJSONPointer('/')).toEqual([''])
|
||||||
|
expect(parseJSONPointer('')).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('compileJSONPointer', () => {
|
||||||
|
expect(compileJSONPointer(['foo', 'bar'])).toEqual('/foo/bar')
|
||||||
|
expect(compileJSONPointer(['foo', '/~ ~/'])).toEqual('/foo/~1~0 ~0~1')
|
||||||
|
expect(compileJSONPointer([''])).toEqual('/')
|
||||||
|
expect(compileJSONPointer([])).toEqual('')
|
||||||
|
})
|
|
@ -1,356 +0,0 @@
|
||||||
import isEqual from 'lodash/isEqual'
|
|
||||||
import initial from 'lodash/initial'
|
|
||||||
import last from 'lodash/last'
|
|
||||||
|
|
||||||
import {
|
|
||||||
setIn, updateIn, getIn, deleteIn, insertAt,
|
|
||||||
cloneWithSymbols
|
|
||||||
} from './utils/immutabilityHelpers'
|
|
||||||
import {
|
|
||||||
META,
|
|
||||||
jsonToEson, esonToJson, updatePaths,
|
|
||||||
parseJSONPointer, compileJSONPointer,
|
|
||||||
expandAll, pathExists, resolvePathIndex, createId, applyEsonState
|
|
||||||
} from './eson'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply a patch to a ESON object
|
|
||||||
* @param {ESON} eson
|
|
||||||
* @param {Array} patch A JSON patch
|
|
||||||
* @param {function(path: Path)} [expand] Optional function to determine
|
|
||||||
* what nodes must be expanded
|
|
||||||
* @return {{data: ESON, revert: Object[], error: Error | null}}
|
|
||||||
*/
|
|
||||||
export function patchEson (eson, patch, expand = expandAll) {
|
|
||||||
let updatedEson = eson
|
|
||||||
let revert = []
|
|
||||||
|
|
||||||
for (let i = 0; i < patch.length; i++) {
|
|
||||||
const action = patch[i]
|
|
||||||
const path = action.path ? parseJSONPointer(action.path) : null
|
|
||||||
const from = action.from ? parseJSONPointer(action.from) : null
|
|
||||||
const options = action.meta
|
|
||||||
|
|
||||||
// TODO: check whether action.op and action.path exist
|
|
||||||
|
|
||||||
switch (action.op) {
|
|
||||||
case 'add': {
|
|
||||||
const newValue = jsonToEson(action.value, path)
|
|
||||||
const newValueWithState = (options && options.state)
|
|
||||||
? applyEsonState(newValue, options.state)
|
|
||||||
: newValue
|
|
||||||
const result = add(updatedEson, path, newValueWithState, options)
|
|
||||||
updatedEson = result.data
|
|
||||||
revert = result.revert.concat(revert)
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'remove': {
|
|
||||||
const result = remove(updatedEson, path)
|
|
||||||
updatedEson = result.data
|
|
||||||
revert = result.revert.concat(revert)
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'replace': {
|
|
||||||
const newValue = jsonToEson(action.value, path)
|
|
||||||
const newValueWithState = (options && options.state)
|
|
||||||
? applyEsonState(newValue, options.state)
|
|
||||||
: newValue
|
|
||||||
const result = replace(updatedEson, path, newValueWithState)
|
|
||||||
updatedEson = result.data
|
|
||||||
revert = result.revert.concat(revert)
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'copy': {
|
|
||||||
if (!action.from) {
|
|
||||||
return {
|
|
||||||
data: eson,
|
|
||||||
revert: [],
|
|
||||||
error: new Error('Property "from" expected in copy action ' + JSON.stringify(action))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = copy(updatedEson, path, from, options)
|
|
||||||
updatedEson = result.data
|
|
||||||
revert = result.revert.concat(revert)
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'move': {
|
|
||||||
if (!action.from) {
|
|
||||||
return {
|
|
||||||
data: eson,
|
|
||||||
revert: [],
|
|
||||||
error: new Error('Property "from" expected in move action ' + JSON.stringify(action))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = move(updatedEson, path, from, options)
|
|
||||||
updatedEson = result.data
|
|
||||||
revert = result.revert.concat(revert)
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'test': {
|
|
||||||
// when a test fails, cancel the whole patch and return the error
|
|
||||||
const error = test(updatedEson, path, action.value)
|
|
||||||
if (error) {
|
|
||||||
return { data: eson, revert: [], error}
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
// unknown ESONPatch operation. Cancel the whole patch and return an error
|
|
||||||
return {
|
|
||||||
data: eson,
|
|
||||||
revert: [],
|
|
||||||
error: new Error('Unknown ESONPatch op ' + JSON.stringify(action.op))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: updatedEson,
|
|
||||||
revert,
|
|
||||||
error: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace an existing item
|
|
||||||
* @param {ESON} data
|
|
||||||
* @param {Path} path
|
|
||||||
* @param {ESON} value
|
|
||||||
* @return {{data: ESON, revert: ESONPatch}}
|
|
||||||
*/
|
|
||||||
export function replace (data, path, value) {
|
|
||||||
const oldValue = getIn(data, path)
|
|
||||||
|
|
||||||
// keep the original id
|
|
||||||
let newValue = setIn(value, [META, 'id'], oldValue[META].id)
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: setIn(data, path, newValue),
|
|
||||||
revert: [{
|
|
||||||
op: 'replace',
|
|
||||||
path: compileJSONPointer(path),
|
|
||||||
value: esonToJson(oldValue),
|
|
||||||
meta: {
|
|
||||||
type: oldValue[META].type
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove an item or property
|
|
||||||
* @param {ESON} data
|
|
||||||
* @param {Path} path
|
|
||||||
* @return {{data: ESON, revert: ESONPatch}}
|
|
||||||
*/
|
|
||||||
export function remove (data, path) {
|
|
||||||
// console.log('remove', path)
|
|
||||||
|
|
||||||
const parentPath = initial(path)
|
|
||||||
const parent = getIn(data, parentPath)
|
|
||||||
const dataValue = getIn(data, path)
|
|
||||||
const value = esonToJson(dataValue)
|
|
||||||
|
|
||||||
if (parent[META].type === 'Array') {
|
|
||||||
return {
|
|
||||||
data: updatePaths(deleteIn(data, path)),
|
|
||||||
revert: [{
|
|
||||||
op: 'add',
|
|
||||||
path: compileJSONPointer(path),
|
|
||||||
value,
|
|
||||||
meta: {
|
|
||||||
type: dataValue[META].type
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else { // parent[META].type === 'Object'
|
|
||||||
const prop = last(path)
|
|
||||||
const index = parent[META].props.indexOf(prop)
|
|
||||||
const nextProp = parent[META].props[index + 1] || null
|
|
||||||
|
|
||||||
let updatedParent = deleteIn(parent, [prop]) // delete property itself
|
|
||||||
updatedParent = deleteIn(updatedParent, [META, 'props', index]) // delete property from the props list
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: setIn(data, parentPath, updatePaths(updatedParent, parentPath)),
|
|
||||||
revert: [{
|
|
||||||
op: 'add',
|
|
||||||
path: compileJSONPointer(path),
|
|
||||||
value,
|
|
||||||
meta: {
|
|
||||||
type: dataValue[META].type,
|
|
||||||
before: nextProp
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {ESON} data
|
|
||||||
* @param {Path} path
|
|
||||||
* @param {ESON} value
|
|
||||||
* @param {{before?: string}} [options]
|
|
||||||
* @return {{data: ESON, revert: ESONPatch}}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
export function add (data, path, value, options) {
|
|
||||||
const parentPath = initial(path)
|
|
||||||
const parent = getIn(data, parentPath)
|
|
||||||
const resolvedPath = resolvePathIndex(data, path)
|
|
||||||
const prop = last(resolvedPath)
|
|
||||||
|
|
||||||
let updatedEson
|
|
||||||
if (parent[META].type === 'Array') {
|
|
||||||
updatedEson = updatePaths(insertAt(data, resolvedPath, value))
|
|
||||||
}
|
|
||||||
else { // parent[META].type === 'Object'
|
|
||||||
updatedEson = updateIn(data, parentPath, (parent) => {
|
|
||||||
const oldValue = getIn(data, path)
|
|
||||||
const props = parent[META].props
|
|
||||||
const existingIndex = props.indexOf(prop)
|
|
||||||
|
|
||||||
if (existingIndex !== -1) {
|
|
||||||
// replace existing item, keep existing id
|
|
||||||
const newValue = setIn(value, [META, 'id'], oldValue[META].id)
|
|
||||||
return setIn(parent, [prop], updatePaths(newValue, path))
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// insert new item
|
|
||||||
const index = (options && typeof options.before === 'string')
|
|
||||||
? props.indexOf(options.before) // insert before
|
|
||||||
: props.length // append
|
|
||||||
|
|
||||||
let updatedKeys = props.slice()
|
|
||||||
updatedKeys.splice(index, 0, prop)
|
|
||||||
const updatedParent = setIn(parent, [prop], updatePaths(value, parentPath.concat(prop)))
|
|
||||||
return setIn(updatedParent, [META, 'props'], updatedKeys)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parent[META].type === 'Object' && pathExists(data, resolvedPath)) {
|
|
||||||
const oldValue = getIn(data, resolvedPath)
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: updatedEson,
|
|
||||||
revert: [{
|
|
||||||
op: 'replace',
|
|
||||||
path: compileJSONPointer(resolvedPath),
|
|
||||||
value: esonToJson(oldValue),
|
|
||||||
meta: { type: oldValue[META].type }
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return {
|
|
||||||
data: updatedEson,
|
|
||||||
revert: [{
|
|
||||||
op: 'remove',
|
|
||||||
path: compileJSONPointer(resolvedPath)
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy a value
|
|
||||||
* @param {ESON} data
|
|
||||||
* @param {Path} path
|
|
||||||
* @param {Path} from
|
|
||||||
* @param {{before?: string}} [options]
|
|
||||||
* @return {{data: ESON, revert: ESONPatch}}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
export function copy (data, path, from, options) {
|
|
||||||
const value = getIn(data, from)
|
|
||||||
|
|
||||||
// create new id for the copied item
|
|
||||||
let updatedValue = cloneWithSymbols(value)
|
|
||||||
updatedValue[META] = setIn(updatedValue[META], ['id'], createId())
|
|
||||||
|
|
||||||
return add(data, path, updatedValue, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move a value
|
|
||||||
* @param {ESON} data
|
|
||||||
* @param {Path} path
|
|
||||||
* @param {Path} from
|
|
||||||
* @param {{before?: string}} [options]
|
|
||||||
* @return {{data: ESON, revert: ESONPatch}}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
export function move (data, path, from, options) {
|
|
||||||
const dataValue = getIn(data, from)
|
|
||||||
|
|
||||||
const parentPathFrom = initial(from)
|
|
||||||
const parent = getIn(data, parentPathFrom)
|
|
||||||
|
|
||||||
const result1 = remove(data, from)
|
|
||||||
const result2 = add(result1.data, path, dataValue, options)
|
|
||||||
|
|
||||||
const before = result1.revert[0].meta.before
|
|
||||||
const beforeNeeded = (parent[META].type === 'Object' && before)
|
|
||||||
|
|
||||||
if (result2.revert[0].op === 'replace') {
|
|
||||||
const value = result2.revert[0].value
|
|
||||||
const type = result2.revert[0].meta.type
|
|
||||||
const options = beforeNeeded ? { type, before } : { type }
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: result2.data,
|
|
||||||
revert: [
|
|
||||||
{ op: 'move', from: compileJSONPointer(path), path: compileJSONPointer(from) },
|
|
||||||
{ op: 'add', path: compileJSONPointer(path), value, meta: options}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else { // result2.revert[0].op === 'remove'
|
|
||||||
return {
|
|
||||||
data: result2.data,
|
|
||||||
revert: beforeNeeded
|
|
||||||
? [{ op: 'move', from: compileJSONPointer(path), path: compileJSONPointer(from), meta: { before } }]
|
|
||||||
: [{ op: 'move', from: compileJSONPointer(path), path: compileJSONPointer(from) }]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test whether the data contains the provided value at the specified path.
|
|
||||||
* Throws an error when the test fails.
|
|
||||||
* @param {ESON} data
|
|
||||||
* @param {Path} path
|
|
||||||
* @param {*} value
|
|
||||||
* @return {null | Error} Returns an error when the tests, returns null otherwise
|
|
||||||
*/
|
|
||||||
export function test (data, path, value) {
|
|
||||||
if (value === undefined) {
|
|
||||||
return new Error('Test failed, no value provided')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pathExists(data, path)) {
|
|
||||||
return new Error('Test failed, path not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
const actualValue = getIn(data, path)
|
|
||||||
if (!isEqual(esonToJson(actualValue), value)) {
|
|
||||||
return new Error('Test failed, value differs')
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,572 +0,0 @@
|
||||||
'use strict'
|
|
||||||
|
|
||||||
import { readFileSync } from 'fs'
|
|
||||||
import { META, jsonToEson, esonToJson, expandOne } from './eson'
|
|
||||||
import { patchEson } from './patchEson'
|
|
||||||
import { assertDeepEqualEson } from './utils/assertDeepEqualEson'
|
|
||||||
|
|
||||||
test('jsonpatch add', () => {
|
|
||||||
const json = {
|
|
||||||
arr: [1,2,3],
|
|
||||||
obj: {a : 2}
|
|
||||||
}
|
|
||||||
|
|
||||||
const patch = [
|
|
||||||
{op: 'add', path: '/obj/b', value: {foo: 'bar'}}
|
|
||||||
]
|
|
||||||
|
|
||||||
const data = jsonToEson(json)
|
|
||||||
const result = patchEson(data, patch)
|
|
||||||
const patchedData = result.data
|
|
||||||
const revert = result.revert
|
|
||||||
|
|
||||||
assertDeepEqualEson(patchedData, jsonToEson({
|
|
||||||
arr: [1,2,3],
|
|
||||||
obj: {a : 2, b: {foo: 'bar'}}
|
|
||||||
}))
|
|
||||||
expect(revert).toEqual([
|
|
||||||
{op: 'remove', path: '/obj/b'}
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('jsonpatch add: insert in matrix', () => {
|
|
||||||
const json = {
|
|
||||||
arr: [1,2,3],
|
|
||||||
obj: {a : 2}
|
|
||||||
}
|
|
||||||
|
|
||||||
const patch = [
|
|
||||||
{op: 'add', path: '/arr/1', value: 4}
|
|
||||||
]
|
|
||||||
|
|
||||||
const data = jsonToEson(json)
|
|
||||||
const result = patchEson(data, patch)
|
|
||||||
const patchedData = result.data
|
|
||||||
const revert = result.revert
|
|
||||||
|
|
||||||
assertDeepEqualEson(patchedData, jsonToEson({
|
|
||||||
arr: [1,4,2,3],
|
|
||||||
obj: {a : 2}
|
|
||||||
}))
|
|
||||||
expect(revert).toEqual([
|
|
||||||
{op: 'remove', path: '/arr/1'}
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('jsonpatch add: append to matrix', () => {
|
|
||||||
const json = {
|
|
||||||
arr: [1,2,3],
|
|
||||||
obj: {a : 2}
|
|
||||||
}
|
|
||||||
|
|
||||||
const patch = [
|
|
||||||
{op: 'add', path: '/arr/-', value: 4}
|
|
||||||
]
|
|
||||||
|
|
||||||
const data = jsonToEson(json)
|
|
||||||
const result = patchEson(data, patch)
|
|
||||||
const patchedData = result.data
|
|
||||||
const revert = result.revert
|
|
||||||
|
|
||||||
assertDeepEqualEson(patchedData, jsonToEson({
|
|
||||||
arr: [1,2,3,4],
|
|
||||||
obj: {a : 2}
|
|
||||||
}))
|
|
||||||
expect(revert).toEqual([
|
|
||||||
{op: 'remove', path: '/arr/3'}
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('jsonpatch add: apply eson state', () => {
|
|
||||||
const json = {
|
|
||||||
a: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
const patch = [
|
|
||||||
{
|
|
||||||
op: 'add',
|
|
||||||
path: '/b',
|
|
||||||
value: {c: {d: 3}},
|
|
||||||
meta: {
|
|
||||||
state: {
|
|
||||||
'': { expanded: true },
|
|
||||||
'/c/d': { expanded: true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const data = jsonToEson(json)
|
|
||||||
const result = patchEson(data, patch)
|
|
||||||
const patchedData = result.data
|
|
||||||
|
|
||||||
let expected = jsonToEson({
|
|
||||||
a: 2,
|
|
||||||
b: {c: {d: 3}}
|
|
||||||
})
|
|
||||||
expected = expandOne(expected, ['b'], true)
|
|
||||||
expected = expandOne(expected, ['b', 'c', 'd'], true)
|
|
||||||
|
|
||||||
assertDeepEqualEson(patchedData, expected)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('jsonpatch remove', () => {
|
|
||||||
const json = {
|
|
||||||
arr: [1,2,3],
|
|
||||||
obj: {a : 4}
|
|
||||||
}
|
|
||||||
|
|
||||||
const patch = [
|
|
||||||
{op: 'remove', path: '/obj/a'},
|
|
||||||
{op: 'remove', path: '/arr/1'},
|
|
||||||
]
|
|
||||||
|
|
||||||
const data = jsonToEson(json)
|
|
||||||
const result = patchEson(data, patch)
|
|
||||||
const patchedData = result.data
|
|
||||||
const revert = result.revert
|
|
||||||
const patchedJson = esonToJson(patchedData)
|
|
||||||
|
|
||||||
assertDeepEqualEson(patchedData, jsonToEson({
|
|
||||||
arr: [1,3],
|
|
||||||
obj: {}
|
|
||||||
}))
|
|
||||||
expect(revert).toEqual([
|
|
||||||
{op: 'add', path: '/arr/1', value: 2, meta: {type: 'value'}},
|
|
||||||
{op: 'add', path: '/obj/a', value: 4, meta: {type: 'value', before: null}}
|
|
||||||
])
|
|
||||||
|
|
||||||
// test revert
|
|
||||||
const data2 = jsonToEson(patchedJson)
|
|
||||||
const result2 = patchEson(data2, revert)
|
|
||||||
const patchedData2 = result2.data
|
|
||||||
const revert2 = result2.revert
|
|
||||||
const patchedJson2 = esonToJson(patchedData2)
|
|
||||||
|
|
||||||
expect(patchedJson2).toEqual(json)
|
|
||||||
expect(revert2).toEqual(patch)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('jsonpatch replace', () => {
|
|
||||||
const json = {
|
|
||||||
arr: [1,2,3],
|
|
||||||
obj: {a : 4}
|
|
||||||
}
|
|
||||||
|
|
||||||
const patch = [
|
|
||||||
{op: 'replace', path: '/obj/a', value: 400},
|
|
||||||
{op: 'replace', path: '/arr/1', value: 200},
|
|
||||||
]
|
|
||||||
|
|
||||||
const data = jsonToEson(json)
|
|
||||||
const result = patchEson(data, patch)
|
|
||||||
const patchedData = result.data
|
|
||||||
const revert = result.revert
|
|
||||||
const patchedJson = esonToJson(patchedData)
|
|
||||||
|
|
||||||
assertDeepEqualEson(patchedData, jsonToEson({
|
|
||||||
arr: [1,200,3],
|
|
||||||
obj: {a: 400}
|
|
||||||
}))
|
|
||||||
expect(revert).toEqual([
|
|
||||||
{op: 'replace', path: '/arr/1', value: 2, meta: {type: 'value'}},
|
|
||||||
{op: 'replace', path: '/obj/a', value: 4, meta: {type: 'value'}}
|
|
||||||
])
|
|
||||||
|
|
||||||
// test revert
|
|
||||||
const data2 = jsonToEson(patchedJson)
|
|
||||||
const result2 = patchEson(data2, revert)
|
|
||||||
const patchedData2 = result2.data
|
|
||||||
const revert2 = result2.revert
|
|
||||||
const patchedJson2 = esonToJson(patchedData2)
|
|
||||||
|
|
||||||
expect(patchedJson2).toEqual(json)
|
|
||||||
expect(revert2).toEqual([
|
|
||||||
{op: 'replace', path: '/obj/a', value: 400, meta: {type: 'value'}},
|
|
||||||
{op: 'replace', path: '/arr/1', value: 200, meta: {type: 'value'}}
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('jsonpatch replace (keep ids intact)', () => {
|
|
||||||
const json = { value: 42 }
|
|
||||||
const patch = [
|
|
||||||
{op: 'replace', path: '/value', value: 100}
|
|
||||||
]
|
|
||||||
|
|
||||||
const data = jsonToEson(json)
|
|
||||||
const valueId = data.value[META].id
|
|
||||||
|
|
||||||
const patchedData = patchEson(data, patch).data
|
|
||||||
const patchedValueId = patchedData.value[META].id
|
|
||||||
|
|
||||||
expect(patchedValueId).toEqual(valueId)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('jsonpatch replace: apply eson state', () => {
|
|
||||||
const json = {
|
|
||||||
a: 2,
|
|
||||||
b: 4
|
|
||||||
}
|
|
||||||
|
|
||||||
const patch = [
|
|
||||||
{
|
|
||||||
op: 'replace',
|
|
||||||
path: '/b',
|
|
||||||
value: {c: {d: 3}},
|
|
||||||
meta: {
|
|
||||||
state: {
|
|
||||||
'': { expanded: true },
|
|
||||||
'/c/d': { expanded: true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const data = jsonToEson(json)
|
|
||||||
const result = patchEson(data, patch)
|
|
||||||
const patchedData = result.data
|
|
||||||
|
|
||||||
let expected = jsonToEson({
|
|
||||||
a: 2,
|
|
||||||
b: {c: {d: 3}}
|
|
||||||
})
|
|
||||||
expected = expandOne(expected, ['b'], true)
|
|
||||||
expected = expandOne(expected, ['b', 'c', 'd'], true)
|
|
||||||
|
|
||||||
assertDeepEqualEson(patchedData, expected)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('jsonpatch copy', () => {
|
|
||||||
const json = {
|
|
||||||
arr: [1,2,3],
|
|
||||||
obj: {a : 4}
|
|
||||||
}
|
|
||||||
|
|
||||||
const patch = [
|
|
||||||
{op: 'copy', from: '/obj', path: '/arr/2'},
|
|
||||||
]
|
|
||||||
|
|
||||||
const data = jsonToEson(json)
|
|
||||||
const result = patchEson(data, patch)
|
|
||||||
const patchedData = result.data
|
|
||||||
const revert = result.revert
|
|
||||||
const patchedJson = esonToJson(patchedData)
|
|
||||||
|
|
||||||
expect(patchedJson).toEqual({
|
|
||||||
arr: [1, 2, {a:4}, 3],
|
|
||||||
obj: {a: 4}
|
|
||||||
})
|
|
||||||
expect(revert).toEqual([
|
|
||||||
{op: 'remove', path: '/arr/2'}
|
|
||||||
])
|
|
||||||
|
|
||||||
// test revert
|
|
||||||
const data2 = jsonToEson(patchedJson)
|
|
||||||
const result2 = patchEson(data2, revert)
|
|
||||||
const patchedData2 = result2.data
|
|
||||||
const revert2 = result2.revert
|
|
||||||
const patchedJson2 = esonToJson(patchedData2)
|
|
||||||
|
|
||||||
expect(patchedJson2).toEqual(json)
|
|
||||||
expect(revert2).toEqual([
|
|
||||||
{op: 'add', path: '/arr/2', value: {a: 4}, meta: {type: 'Object'}}
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('jsonpatch copy (keeps the same ids)', () => {
|
|
||||||
const json = { foo: { bar: 42 } }
|
|
||||||
const patch = [
|
|
||||||
{op: 'copy', from: '/foo', path: '/copied'}
|
|
||||||
]
|
|
||||||
|
|
||||||
const data = jsonToEson(json)
|
|
||||||
const fooId = data.foo[META].id
|
|
||||||
const barId = data.foo.bar[META].id
|
|
||||||
|
|
||||||
const patchedData = patchEson(data, patch).data
|
|
||||||
const patchedFooId = patchedData.foo[META].id
|
|
||||||
const patchedBarId = patchedData.foo.bar[META].id
|
|
||||||
const copiedId = patchedData.copied[META].id
|
|
||||||
const patchedCopiedBarId = patchedData.copied.bar[META].id
|
|
||||||
|
|
||||||
expect(patchedFooId).toEqual(fooId)
|
|
||||||
expect(patchedBarId).toEqual(barId)
|
|
||||||
|
|
||||||
expect(copiedId).not.toEqual(fooId)
|
|
||||||
|
|
||||||
// The id's of the copied childs are the same, that's okish, they will not bite each other
|
|
||||||
expect(patchedCopiedBarId).toEqual(patchedBarId)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('jsonpatch move', () => {
|
|
||||||
const json = {
|
|
||||||
arr: [1,2,3],
|
|
||||||
obj: {a : 4}
|
|
||||||
}
|
|
||||||
|
|
||||||
const patch = [
|
|
||||||
{op: 'move', from: '/obj', path: '/arr/2'},
|
|
||||||
]
|
|
||||||
|
|
||||||
const data = jsonToEson(json)
|
|
||||||
const result = patchEson(data, patch)
|
|
||||||
const patchedData = result.data
|
|
||||||
const revert = result.revert
|
|
||||||
const patchedJson = esonToJson(patchedData)
|
|
||||||
|
|
||||||
expect(result.error).toEqual(null)
|
|
||||||
expect(patchedJson).toEqual({
|
|
||||||
arr: [1, 2, {a:4}, 3]
|
|
||||||
})
|
|
||||||
expect(revert).toEqual([
|
|
||||||
{op: 'move', from: '/arr/2', path: '/obj'}
|
|
||||||
])
|
|
||||||
|
|
||||||
// test revert
|
|
||||||
const data2 = jsonToEson(patchedJson)
|
|
||||||
const result2 = patchEson(data2, revert)
|
|
||||||
const patchedData2 = result2.data
|
|
||||||
const revert2 = result2.revert
|
|
||||||
const patchedJson2 = esonToJson(patchedData2)
|
|
||||||
|
|
||||||
expect(patchedJson2).toEqual(json)
|
|
||||||
expect(revert2).toEqual(patch)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('jsonpatch move before', () => {
|
|
||||||
const json = {
|
|
||||||
arr: [1,2,3],
|
|
||||||
obj: {a : 4},
|
|
||||||
zzz: 'zzz'
|
|
||||||
}
|
|
||||||
|
|
||||||
const patch = [
|
|
||||||
{op: 'move', from: '/obj', path: '/arr/2'},
|
|
||||||
]
|
|
||||||
|
|
||||||
const data = jsonToEson(json)
|
|
||||||
const result = patchEson(data, patch)
|
|
||||||
const patchedData = result.data
|
|
||||||
const revert = result.revert
|
|
||||||
const patchedJson = esonToJson(patchedData)
|
|
||||||
|
|
||||||
expect(result.error).toEqual(null)
|
|
||||||
expect(patchedJson).toEqual({
|
|
||||||
arr: [1, 2, {a:4}, 3],
|
|
||||||
zzz: 'zzz'
|
|
||||||
})
|
|
||||||
expect(revert).toEqual([
|
|
||||||
{op: 'move', from: '/arr/2', path: '/obj', meta: {before: 'zzz'}}
|
|
||||||
])
|
|
||||||
|
|
||||||
// test revert
|
|
||||||
const data2 = jsonToEson(patchedJson)
|
|
||||||
const result2 = patchEson(data2, revert)
|
|
||||||
const patchedData2 = result2.data
|
|
||||||
const revert2 = result2.revert
|
|
||||||
const patchedJson2 = esonToJson(patchedData2)
|
|
||||||
|
|
||||||
expect(patchedJson2).toEqual(json)
|
|
||||||
expect(revert2).toEqual(patch)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('jsonpatch move and replace', () => {
|
|
||||||
const json = { a: 2, b: 3 }
|
|
||||||
|
|
||||||
const patch = [
|
|
||||||
{op: 'move', from: '/a', path: '/b'},
|
|
||||||
]
|
|
||||||
|
|
||||||
const data = jsonToEson(json)
|
|
||||||
|
|
||||||
const result = patchEson(data, patch)
|
|
||||||
const patchedData = result.data
|
|
||||||
const revert = result.revert
|
|
||||||
const patchedJson = esonToJson(patchedData)
|
|
||||||
|
|
||||||
// id of the replaced B must be kept intact
|
|
||||||
expect(patchedData.b[META].id).toEqual(data.b[META].id)
|
|
||||||
|
|
||||||
assertDeepEqualEson(patchedData, jsonToEson({b: 2}))
|
|
||||||
expect(patchedJson).toEqual({ b : 2 })
|
|
||||||
expect(revert).toEqual([
|
|
||||||
{op:'move', from: '/b', path: '/a'},
|
|
||||||
{op:'add', path:'/b', value: 3, meta: {type: 'value', before: 'b'}}
|
|
||||||
])
|
|
||||||
|
|
||||||
// test revert
|
|
||||||
const data2 = jsonToEson(patchedJson)
|
|
||||||
const result2 = patchEson(data2, revert)
|
|
||||||
const patchedData2 = result2.data
|
|
||||||
const revert2 = result2.revert
|
|
||||||
const patchedJson2 = esonToJson(patchedData2)
|
|
||||||
|
|
||||||
expect(patchedJson2).toEqual(json)
|
|
||||||
expect(revert2).toEqual([
|
|
||||||
{op: 'remove', path: '/b'},
|
|
||||||
{op: 'move', from: '/a', path: '/b'}
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('jsonpatch move and replace (nested)', () => {
|
|
||||||
const json = {
|
|
||||||
arr: [1,2,3],
|
|
||||||
obj: {a : 4}
|
|
||||||
}
|
|
||||||
|
|
||||||
const patch = [
|
|
||||||
{op: 'move', from: '/obj', path: '/arr'},
|
|
||||||
]
|
|
||||||
|
|
||||||
const data = jsonToEson(json)
|
|
||||||
const result = patchEson(data, patch)
|
|
||||||
const patchedData = result.data
|
|
||||||
const revert = result.revert
|
|
||||||
const patchedJson = esonToJson(patchedData)
|
|
||||||
|
|
||||||
expect(patchedJson).toEqual({
|
|
||||||
arr: {a:4}
|
|
||||||
})
|
|
||||||
expect(revert).toEqual([
|
|
||||||
{op:'move', from: '/arr', path: '/obj'},
|
|
||||||
{op:'add', path:'/arr', value: [1,2,3], meta: {type: 'Array'}}
|
|
||||||
])
|
|
||||||
|
|
||||||
// test revert
|
|
||||||
const data2 = jsonToEson(patchedJson)
|
|
||||||
const result2 = patchEson(data2, revert)
|
|
||||||
const patchedData2 = result2.data
|
|
||||||
const revert2 = result2.revert
|
|
||||||
const patchedJson2 = esonToJson(patchedData2)
|
|
||||||
|
|
||||||
expect(patchedJson2).toEqual(json)
|
|
||||||
expect(revert2).toEqual([
|
|
||||||
{op: 'remove', path: '/arr'},
|
|
||||||
{op: 'move', from: '/obj', path: '/arr'}
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('jsonpatch move (keep id intact)', () => {
|
|
||||||
const json = { value: 42 }
|
|
||||||
const patch = [
|
|
||||||
{op: 'move', from: '/value', path: '/moved'}
|
|
||||||
]
|
|
||||||
|
|
||||||
const data = jsonToEson(json)
|
|
||||||
const valueId = data.value[META].id
|
|
||||||
|
|
||||||
const patchedData = patchEson(data, patch).data
|
|
||||||
const patchedValueId = patchedData.moved[META].id
|
|
||||||
|
|
||||||
expect(patchedValueId).toEqual(valueId)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('jsonpatch move and replace (keep ids intact)', () => {
|
|
||||||
const json = { a: 2, b: 3 }
|
|
||||||
const patch = [
|
|
||||||
{op: 'move', from: '/a', path: '/b'}
|
|
||||||
]
|
|
||||||
|
|
||||||
const data = jsonToEson(json)
|
|
||||||
const bId = data.b[META].id
|
|
||||||
|
|
||||||
expect(data[META].props).toEqual(['a', 'b'])
|
|
||||||
|
|
||||||
const patchedData = patchEson(data, patch).data
|
|
||||||
|
|
||||||
expect(patchedData.b[META].id).toEqual(bId)
|
|
||||||
expect(patchedData[META].props).toEqual(['b'])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('jsonpatch test (ok)', () => {
|
|
||||||
const json = {
|
|
||||||
arr: [1,2,3],
|
|
||||||
obj: {a : 4}
|
|
||||||
}
|
|
||||||
|
|
||||||
const patch = [
|
|
||||||
{op: 'test', path: '/arr', value: [1,2,3]},
|
|
||||||
{op: 'add', path: '/added', value: 'ok'}
|
|
||||||
]
|
|
||||||
|
|
||||||
const data = jsonToEson(json)
|
|
||||||
const result = patchEson(data, patch)
|
|
||||||
const patchedData = result.data
|
|
||||||
const revert = result.revert
|
|
||||||
const patchedJson = esonToJson(patchedData)
|
|
||||||
|
|
||||||
expect(patchedJson).toEqual({
|
|
||||||
arr: [1,2,3],
|
|
||||||
obj: {a : 4},
|
|
||||||
added: 'ok'
|
|
||||||
})
|
|
||||||
expect(revert).toEqual([
|
|
||||||
{op: 'remove', path: '/added'}
|
|
||||||
])
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
test('jsonpatch test (fail: path not found)', () => {
|
|
||||||
const json = {
|
|
||||||
arr: [1,2,3],
|
|
||||||
obj: {a : 4}
|
|
||||||
}
|
|
||||||
|
|
||||||
const patch = [
|
|
||||||
{op: 'test', path: '/arr/5', value: [1,2,3]},
|
|
||||||
{op: 'add', path: '/added', value: 'ok'}
|
|
||||||
]
|
|
||||||
|
|
||||||
const data = jsonToEson(json)
|
|
||||||
const result = patchEson(data, patch)
|
|
||||||
const patchedData = result.data
|
|
||||||
const revert = result.revert
|
|
||||||
const patchedJson = esonToJson(patchedData)
|
|
||||||
|
|
||||||
// patch shouldn't be applied
|
|
||||||
expect(patchedJson).toEqual({
|
|
||||||
arr: [1,2,3],
|
|
||||||
obj: {a : 4}
|
|
||||||
})
|
|
||||||
expect(revert).toEqual([])
|
|
||||||
expect(result.error.toString()).toEqual('Error: Test failed, path not found')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('jsonpatch test (fail: value not equal)', () => {
|
|
||||||
const json = {
|
|
||||||
arr: [1,2,3],
|
|
||||||
obj: {a : 4}
|
|
||||||
}
|
|
||||||
|
|
||||||
const patch = [
|
|
||||||
{op: 'test', path: '/obj', value: {a:4, b: 6}},
|
|
||||||
{op: 'add', path: '/added', value: 'ok'}
|
|
||||||
]
|
|
||||||
|
|
||||||
const data = jsonToEson(json)
|
|
||||||
const result = patchEson(data, patch)
|
|
||||||
const patchedData = result.data
|
|
||||||
const revert = result.revert
|
|
||||||
const patchedJson = esonToJson(patchedData)
|
|
||||||
|
|
||||||
// patch shouldn't be applied
|
|
||||||
expect(patchedJson).toEqual({
|
|
||||||
arr: [1,2,3],
|
|
||||||
obj: {a : 4}
|
|
||||||
})
|
|
||||||
expect(revert).toEqual([])
|
|
||||||
expect(result.error.toString()).toEqual('Error: Test failed, value differs')
|
|
||||||
})
|
|
||||||
|
|
||||||
// helper function to print JSON in the console
|
|
||||||
function printJSON (json, message = null) {
|
|
||||||
if (message) {
|
|
||||||
console.log(message)
|
|
||||||
}
|
|
||||||
console.log(JSON.stringify(json, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
// helper function to load a JSON file
|
|
||||||
function loadJSON (filename) {
|
|
||||||
return JSON.parse(readFileSync(__dirname + '/' + filename, 'utf-8'))
|
|
||||||
}
|
|
|
@ -57,7 +57,20 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {'Object' | 'Array' | 'value' | 'string'} ESONType
|
* @typedef {'object' | 'array' | 'value' | 'string'} ESONType
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {{
|
||||||
|
* op: 'add' | 'remove' | 'replace' | 'copy' | 'move' | 'test',
|
||||||
|
* path: string,
|
||||||
|
* from?: string,
|
||||||
|
* value?: *
|
||||||
|
* }} JSONPatchAction
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {JSONPatchAction[]} JSONPatch
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
import {META} from "../eson"
|
|
||||||
import lodashTransform from "lodash/transform"
|
|
||||||
|
|
||||||
export function assertDeepEqualEson (actual, expected, path = [], ignoreIds = true) {
|
|
||||||
if (expected === undefined) {
|
|
||||||
throw new Error('Argument "expected" is undefined')
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log('assertDeepEqualEson', actual, expected)
|
|
||||||
|
|
||||||
const actualMeta = ignoreIds ? normalizeMetaIds(actual[META]) : actual[META]
|
|
||||||
const expectedMeta = ignoreIds ? normalizeMetaIds(expected[META]) : expected[META]
|
|
||||||
|
|
||||||
expect(actualMeta).toEqual(expectedMeta) // `Meta data not equal, path=[${path.join(', ')}], actual[META]=${JSON.stringify(actualMeta)}, expected[META]=${JSON.stringify(expectedMeta)}`
|
|
||||||
|
|
||||||
if (actualMeta.type === 'Array') {
|
|
||||||
expect(actual.length).toEqual(expected.length) // 'Actual lengths of arrays should be equal, path=[${path.join(\', \')}]'
|
|
||||||
actual.forEach((item, index) => assertDeepEqualEson(actual[index], expected[index], path.concat(index)), ignoreIds)
|
|
||||||
}
|
|
||||||
else if (actualMeta.type === 'Object') {
|
|
||||||
expect(Object.keys(actual).sort()).toEqual(Object.keys(expected).sort()) // 'Actual properties should be equal, path=[${path.join(\', \')}]'
|
|
||||||
actualMeta.props.forEach(key => assertDeepEqualEson(actual[key], expected[key], path.concat(key)), ignoreIds)
|
|
||||||
}
|
|
||||||
else { // actual[META].type === 'value'
|
|
||||||
expect(Object.keys(actual)).toEqual([]) // 'Value should not contain additional properties, path=[${path.join(\', \')}]'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeMetaIds (meta) {
|
|
||||||
return lodashTransform(meta, (result, value, key) => {
|
|
||||||
if (key === 'id') {
|
|
||||||
result[key] = '[ID]'
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
result[key] = value
|
|
||||||
}
|
|
||||||
}, {})
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { ID, TYPE, VALUE } from '../eson'
|
||||||
|
import uniq from 'lodash/uniq'
|
||||||
|
import each from 'lodash/each'
|
||||||
|
|
||||||
|
export function createAssertEqualEson(expect) {
|
||||||
|
|
||||||
|
function assertEqualEson (actual, expected, ignoreIds = true) {
|
||||||
|
if (expected === undefined) {
|
||||||
|
throw new Error('Argument "expected" is undefined')
|
||||||
|
}
|
||||||
|
|
||||||
|
// regular deep equal
|
||||||
|
expect(actual).toEqual(expected)
|
||||||
|
|
||||||
|
assertEqualEsonKeys(actual, expected, ignoreIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertEqualEsonKeys (actual, expected, ignoreIds = true) {
|
||||||
|
// collect all symbols
|
||||||
|
const symbols = uniq(Object.getOwnPropertySymbols(actual)
|
||||||
|
.concat(Object.getOwnPropertySymbols(expected)))
|
||||||
|
|
||||||
|
// test whether all meta data is the same
|
||||||
|
symbols
|
||||||
|
.filter(symbol => symbol !== ID || ignoreIds)
|
||||||
|
.forEach(symbol => expect(actual[symbol]).toEqual(expected[symbol]))
|
||||||
|
|
||||||
|
if (actual[TYPE] === 'array') {
|
||||||
|
each(expected, (item, index) => assertEqualEsonKeys(actual[index], expected[index], ignoreIds))
|
||||||
|
}
|
||||||
|
else if (actual[TYPE] === 'object') {
|
||||||
|
each(actual, (value, key) => assertEqualEsonKeys(actual[key], expected[key]), ignoreIds)
|
||||||
|
}
|
||||||
|
else { // actual[TYPE] === 'value'
|
||||||
|
expect(actual[VALUE]).toEqual(expected[VALUE])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return assertEqualEson
|
||||||
|
}
|
|
@ -17,7 +17,7 @@ import { isObjectOrArray } from './typeUtils'
|
||||||
* @param {*} value
|
* @param {*} value
|
||||||
* @return {*}
|
* @return {*}
|
||||||
*/
|
*/
|
||||||
export function cloneWithSymbols (value) {
|
export function shallowCloneWithSymbols (value) {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
// copy array items
|
// copy array items
|
||||||
let arr = value.slice()
|
let arr = value.slice()
|
||||||
|
@ -97,7 +97,7 @@ export function setIn (object, path, value) {
|
||||||
return object
|
return object
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const updatedObject = cloneWithSymbols(object)
|
const updatedObject = shallowCloneWithSymbols(object)
|
||||||
updatedObject[key] = updatedValue
|
updatedObject[key] = updatedValue
|
||||||
return updatedObject
|
return updatedObject
|
||||||
}
|
}
|
||||||
|
@ -129,7 +129,7 @@ export function updateIn (object, path, callback) {
|
||||||
return object
|
return object
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const updatedObject = cloneWithSymbols(object)
|
const updatedObject = shallowCloneWithSymbols(object)
|
||||||
updatedObject[key] = updatedValue
|
updatedObject[key] = updatedValue
|
||||||
return updatedObject
|
return updatedObject
|
||||||
}
|
}
|
||||||
|
@ -159,7 +159,7 @@ export function deleteIn (object, path) {
|
||||||
return object
|
return object
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const updatedObject = cloneWithSymbols(object)
|
const updatedObject = shallowCloneWithSymbols(object)
|
||||||
|
|
||||||
if (Array.isArray(updatedObject)) {
|
if (Array.isArray(updatedObject)) {
|
||||||
updatedObject.splice(key, 1)
|
updatedObject.splice(key, 1)
|
||||||
|
@ -179,7 +179,7 @@ export function deleteIn (object, path) {
|
||||||
return object
|
return object
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const updatedObject = cloneWithSymbols(object)
|
const updatedObject = shallowCloneWithSymbols(object)
|
||||||
updatedObject[key] = updatedValue
|
updatedObject[key] = updatedValue
|
||||||
return updatedObject
|
return updatedObject
|
||||||
}
|
}
|
||||||
|
@ -205,9 +205,89 @@ export function insertAt (object, path, value) {
|
||||||
throw new TypeError('Array expected at path ' + JSON.stringify(parentPath))
|
throw new TypeError('Array expected at path ' + JSON.stringify(parentPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedItems = cloneWithSymbols(items)
|
const updatedItems = shallowCloneWithSymbols(items)
|
||||||
updatedItems.splice(index, 0, value)
|
updatedItems.splice(index, 0, value)
|
||||||
|
|
||||||
return updatedItems
|
return updatedItems
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform a JSON object, traverse over the whole object,
|
||||||
|
* and allow replacing Objects/Arrays/values.
|
||||||
|
* Does not iterate over symbols.
|
||||||
|
* @param {JSON} json
|
||||||
|
* @param {function (json: JSON, path: Path) : JSON} callback
|
||||||
|
* @param {Path} [path]
|
||||||
|
* @return {JSON}
|
||||||
|
*/
|
||||||
|
export function transform (json, callback, path = []) {
|
||||||
|
const updated1 = callback(json, path)
|
||||||
|
|
||||||
|
if (Array.isArray(json)) { // array
|
||||||
|
let updated2 = undefined
|
||||||
|
|
||||||
|
for (let i = 0; i < updated1.length; i++) {
|
||||||
|
const before = updated1[i]
|
||||||
|
// we stringify the index here, so the Path only contains strings and can be safely
|
||||||
|
// stringified/parsed to JSONPointer without loosing information.
|
||||||
|
// We do not want to rely on path keys being numeric/string.
|
||||||
|
const after = transform(before, callback, path.concat(i + ''))
|
||||||
|
if (after !== before) {
|
||||||
|
if (!updated2) {
|
||||||
|
updated2 = shallowCloneWithSymbols(updated1)
|
||||||
|
}
|
||||||
|
updated2[i] = after
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated2 ? updated2 : updated1
|
||||||
|
}
|
||||||
|
else if (json && typeof json === 'object') { // object
|
||||||
|
let updated2 = undefined
|
||||||
|
|
||||||
|
for (let key in updated1) {
|
||||||
|
if (updated1.hasOwnProperty(key)) {
|
||||||
|
const before = updated1[key]
|
||||||
|
const after = transform(before, callback, path.concat(key))
|
||||||
|
if (after !== before) {
|
||||||
|
if (!updated2) {
|
||||||
|
updated2 = shallowCloneWithSymbols(updated1)
|
||||||
|
}
|
||||||
|
updated2[key] = after
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated2 ? updated2 : updated1
|
||||||
|
}
|
||||||
|
else { // number, string, boolean, null
|
||||||
|
return updated1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test whether a path exists in a JSON object
|
||||||
|
* @param {ESON} json
|
||||||
|
* @param {Path} path
|
||||||
|
* @return {boolean} Returns true if the path exists, else returns false
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export function existsIn (json, path) {
|
||||||
|
if (json === undefined) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.length === 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(json)) {
|
||||||
|
// index of an array
|
||||||
|
return existsIn(json[parseInt(path[0], 10)], path.slice(1))
|
||||||
|
}
|
||||||
|
else { // Object
|
||||||
|
// object property. find the index of this property
|
||||||
|
return existsIn(json[path[0]], path.slice(1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { getIn, setIn, updateIn, deleteIn, insertAt } from './immutabilityHelpers'
|
import { deleteIn, existsIn, getIn, insertAt, setIn, transform, updateIn } from './immutabilityHelpers'
|
||||||
|
|
||||||
test('getIn', () => {
|
test('getIn', () => {
|
||||||
const obj = {
|
const obj = {
|
||||||
|
@ -274,3 +274,47 @@ test('insertAt', () => {
|
||||||
const updated = insertAt(obj, ['a', '2'], 8)
|
const updated = insertAt(obj, ['a', '2'], 8)
|
||||||
expect(updated).toEqual({a: [1,2,8,3]})
|
expect(updated).toEqual({a: [1,2,8,3]})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('transform (no change)', () => {
|
||||||
|
const eson = {a: [1,2,3], b: {c: 4}}
|
||||||
|
const updated = transform(eson, (value, path) => value)
|
||||||
|
expect(updated).toBe(eson)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('transform (change based on value)', () => {
|
||||||
|
const eson = {a: [1,2,3], b: {c: 4}}
|
||||||
|
|
||||||
|
const updated = transform(eson,
|
||||||
|
(value, path) => value === 2 ? 20 : value)
|
||||||
|
const expected = {a: [1,20,3], b: {c: 4}}
|
||||||
|
|
||||||
|
expect(updated).toEqual(expected)
|
||||||
|
expect(updated.b).toBe(eson.b) // should not have replaced b
|
||||||
|
})
|
||||||
|
|
||||||
|
test('transform (change based on path)', () => {
|
||||||
|
const eson = {a: [1,2,3], b: {c: 4}}
|
||||||
|
|
||||||
|
const updated = transform(eson,
|
||||||
|
(value, path) => path.join('.') === 'a.1' ? 20 : value)
|
||||||
|
const expected = {a: [1,20,3], b: {c: 4}}
|
||||||
|
|
||||||
|
expect(updated).toEqual(expected)
|
||||||
|
expect(updated.b).toBe(eson.b) // should not have replaced b
|
||||||
|
})
|
||||||
|
|
||||||
|
test('existsIn', () => {
|
||||||
|
const json = {
|
||||||
|
"obj": {
|
||||||
|
"arr": [1,2, {"first":3,"last":4}]
|
||||||
|
},
|
||||||
|
"str": "hello world",
|
||||||
|
"nill": null,
|
||||||
|
"bool": false
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(existsIn(json, ['obj', 'arr', 2, 'first'])).toEqual(true)
|
||||||
|
expect(existsIn(json, ['obj', 'foo'])).toEqual(false)
|
||||||
|
expect(existsIn(json, ['obj', 'foo', 'bar'])).toEqual(false)
|
||||||
|
expect(existsIn(json, [])).toEqual(true)
|
||||||
|
})
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
const map = new WeakMap()
|
||||||
|
let counter = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a unique key for an object or array (by object reference)
|
||||||
|
* @param {Object | Array} item
|
||||||
|
* @returns {string | null}
|
||||||
|
* Returns the generated key.
|
||||||
|
* If the item is no Object or Array, null is returned
|
||||||
|
*/
|
||||||
|
export function weakKey(item) {
|
||||||
|
if (!item || (!Array.isArray(item) && typeof item !== 'object')) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let k = map.get(item)
|
||||||
|
|
||||||
|
if (!k) {
|
||||||
|
k = 'key-' + counter
|
||||||
|
counter++
|
||||||
|
map.set(item, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
return k
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { weakKey } from './reactUtils'
|
||||||
|
|
||||||
|
test('weakKey should keep the key the same for objects and arrays', () => {
|
||||||
|
const a = {x: 1}
|
||||||
|
const b = {x: 1}
|
||||||
|
const c = b
|
||||||
|
const d = [1, 2, 3]
|
||||||
|
const e = [1, 2, 3]
|
||||||
|
|
||||||
|
expect(weakKey(a)).toEqual(weakKey(a))
|
||||||
|
expect(weakKey(b)).toEqual(weakKey(b))
|
||||||
|
expect(weakKey(a)).not.toEqual(weakKey(b))
|
||||||
|
expect(weakKey(b)).toEqual(weakKey(c))
|
||||||
|
expect(weakKey(d)).toEqual(weakKey(d))
|
||||||
|
expect(weakKey(e)).toEqual(weakKey(e))
|
||||||
|
expect(weakKey(d)).not.toEqual(weakKey(e))
|
||||||
|
expect(weakKey(d)).not.toEqual(weakKey(e))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('weakKey should return null for non-object and non-array items', () => {
|
||||||
|
expect(weakKey('foo')).toBeNull()
|
||||||
|
expect(weakKey(123)).toBeNull()
|
||||||
|
expect(weakKey(null)).toBeNull()
|
||||||
|
expect(weakKey(undefined)).toBeNull()
|
||||||
|
expect(weakKey(true)).toBeNull()
|
||||||
|
})
|
|
@ -98,13 +98,13 @@ export function escapeJSON (text) {
|
||||||
* Find a unique name. Suffix the name with ' (copy)', '(copy 2)', etc
|
* Find a unique name. Suffix the name with ' (copy)', '(copy 2)', etc
|
||||||
* until a unique name is found
|
* until a unique name is found
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @param {Array.<string>} invalidNames
|
* @param {Object} existingProps Object with existing props
|
||||||
*/
|
*/
|
||||||
export function findUniqueName (name, invalidNames) {
|
export function findUniqueName (name, existingProps) {
|
||||||
let validName = name
|
let validName = name
|
||||||
let i = 1
|
let i = 1
|
||||||
|
|
||||||
while (invalidNames.includes(validName)) {
|
while (validName in existingProps) {
|
||||||
const copy = 'copy' + (i > 1 ? (' ' + i) : '')
|
const copy = 'copy' + (i > 1 ? (' ' + i) : '')
|
||||||
validName = `${name} (${copy})`
|
validName = `${name} (${copy})`
|
||||||
i++
|
i++
|
||||||
|
|
|
@ -18,8 +18,8 @@ test('unescapeHTML', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('findUniqueName', () => {
|
test('findUniqueName', () => {
|
||||||
expect(findUniqueName('other', ['a', 'b', 'c'])).toEqual('other')
|
expect(findUniqueName('other', {'a': true, 'b': true, 'c': true})).toEqual('other')
|
||||||
expect(findUniqueName('b', ['a', 'b', 'c'])).toEqual('b (copy)')
|
expect(findUniqueName('b', {'a': true, 'b': true, 'c': true})).toEqual('b (copy)')
|
||||||
expect(findUniqueName('b', ['a', 'b', 'c', 'b (copy)'])).toEqual('b (copy 2)')
|
expect(findUniqueName('b', {'a': true, 'b': true, 'c': true, 'b (copy)': true})).toEqual('b (copy 2)')
|
||||||
expect(findUniqueName('b', ['a', 'b', 'c', 'b (copy)', 'b (copy 2)'])).toEqual('b (copy 3)')
|
expect(findUniqueName('b', {'a': true, 'b': true, 'c': true, 'b (copy)': true, 'b (copy 2)': true})).toEqual('b (copy 3)')
|
||||||
})
|
})
|
||||||
|
|
|
@ -62,10 +62,10 @@ export function valueType(value) {
|
||||||
return 'regexp'
|
return 'regexp'
|
||||||
}
|
}
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return 'Array'
|
return 'array'
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'Object'
|
return 'object'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue