Many patch action working again in UI
This commit is contained in:
parent
156f330e4e
commit
1a6661fbb5
|
@ -1,9 +1,11 @@
|
|||
import last from 'lodash/last'
|
||||
import initial from 'lodash/initial'
|
||||
import {
|
||||
compileJSONPointer, getInEson, esonToJson, findNextProp,
|
||||
META,
|
||||
compileJSONPointer, esonToJson, findNextProp,
|
||||
pathsFromSelection, findRootPath, findSelectionIndices
|
||||
} from './eson'
|
||||
import { cloneWithSymbols, getIn, setIn } from './utils/immutabilityHelpers'
|
||||
import { findUniqueName } from './utils/stringUtils'
|
||||
import { isObject, stringConvert } from './utils/typeUtils'
|
||||
import { compareAsc, compareDesc, strictShallowEqual } from './utils/arrayUtils'
|
||||
|
@ -11,14 +13,14 @@ import { compareAsc, compareDesc, strictShallowEqual } from './utils/arrayUtils'
|
|||
|
||||
/**
|
||||
* Create a JSONPatch to change the value of a property or item
|
||||
* @param {ESON} data
|
||||
* @param {ESON} eson
|
||||
* @param {Path} path
|
||||
* @param {*} value
|
||||
* @return {Array}
|
||||
*/
|
||||
export function changeValue (data, path, value) {
|
||||
export function changeValue (eson, path, value) {
|
||||
// console.log('changeValue', data, value)
|
||||
const oldDataValue = getInEson(data, path)
|
||||
const oldDataValue = getIn(eson, path)
|
||||
|
||||
return [{
|
||||
op: 'replace',
|
||||
|
@ -32,18 +34,18 @@ export function changeValue (data, path, value) {
|
|||
|
||||
/**
|
||||
* Create a JSONPatch to change a property name
|
||||
* @param {ESON} data
|
||||
* @param {ESON} eson
|
||||
* @param {Path} parentPath
|
||||
* @param {string} oldProp
|
||||
* @param {string} newProp
|
||||
* @return {Array}
|
||||
*/
|
||||
export function changeProperty (data, parentPath, oldProp, newProp) {
|
||||
export function changeProperty (eson, parentPath, oldProp, newProp) {
|
||||
// console.log('changeProperty', parentPath, oldProp, newProp)
|
||||
const parent = getInEson(data, parentPath)
|
||||
const parent = getIn(eson, parentPath)
|
||||
|
||||
// prevent duplicate property names
|
||||
const uniqueNewProp = findUniqueName(newProp, parent.props.map(p => p.name))
|
||||
const uniqueNewProp = findUniqueName(newProp, parent[META].keys)
|
||||
|
||||
return [{
|
||||
op: 'move',
|
||||
|
@ -57,13 +59,13 @@ export function changeProperty (data, parentPath, oldProp, newProp) {
|
|||
|
||||
/**
|
||||
* Create a JSONPatch to change the type of a property or item
|
||||
* @param {ESON} data
|
||||
* @param {ESON} eson
|
||||
* @param {Path} path
|
||||
* @param {ESONType} type
|
||||
* @return {Array}
|
||||
*/
|
||||
export function changeType (data, path, type) {
|
||||
const oldValue = esonToJson(getInEson(data, path))
|
||||
export function changeType (eson, path, type) {
|
||||
const oldValue = esonToJson(getIn(eson, path))
|
||||
const newValue = convertType(oldValue, type)
|
||||
|
||||
// console.log('changeType', path, type, oldValue, newValue)
|
||||
|
@ -85,20 +87,20 @@ export function changeType (data, path, type) {
|
|||
* a unique property name for the duplicated node in case of duplicating
|
||||
* and object property
|
||||
*
|
||||
* @param {ESON} data
|
||||
* @param {ESON} eson
|
||||
* @param {Selection} selection
|
||||
* @return {Array}
|
||||
*/
|
||||
export function duplicate (data, selection) {
|
||||
export function duplicate (eson, selection) {
|
||||
// console.log('duplicate', path)
|
||||
if (!selection.start || !selection.end) {
|
||||
return []
|
||||
}
|
||||
|
||||
const rootPath = findRootPath(selection)
|
||||
const root = getInEson(data, rootPath)
|
||||
const root = getIn(eson, rootPath)
|
||||
const { maxIndex } = findSelectionIndices(root, rootPath, selection)
|
||||
const paths = pathsFromSelection(data, selection)
|
||||
const paths = pathsFromSelection(eson, selection)
|
||||
|
||||
if (root.type === 'Array') {
|
||||
return paths.map((path, offset) => ({
|
||||
|
@ -108,12 +110,11 @@ export function duplicate (data, selection) {
|
|||
}))
|
||||
}
|
||||
else { // object.type === 'Object'
|
||||
const nextProp = root.props && root.props[maxIndex]
|
||||
const before = nextProp ? nextProp.name : null
|
||||
const before = root[META].keys[maxIndex] || null
|
||||
|
||||
return paths.map(path => {
|
||||
const prop = last(path)
|
||||
const newProp = findUniqueName(prop, root.props.map(p => p.name))
|
||||
const newProp = findUniqueName(prop, root[META].keys)
|
||||
|
||||
return {
|
||||
op: 'copy',
|
||||
|
@ -134,14 +135,14 @@ export function duplicate (data, selection) {
|
|||
* a unique property name for the inserted node in case of duplicating
|
||||
* and object property
|
||||
*
|
||||
* @param {ESON} data
|
||||
* @param {ESON} eson
|
||||
* @param {Path} path
|
||||
* @param {Array.<{name?: string, value: JSONType, type?: ESONType}>} values
|
||||
* @return {Array}
|
||||
*/
|
||||
export function insertBefore (data, path, values) { // TODO: find a better name and define datastructure for values
|
||||
export function insertBefore (eson, path, values) { // TODO: find a better name and define datastructure for values
|
||||
const parentPath = initial(path)
|
||||
const parent = getInEson(data, parentPath)
|
||||
const parent = getIn(eson, parentPath)
|
||||
|
||||
if (parent.type === 'Array') {
|
||||
const startIndex = parseInt(last(path))
|
||||
|
@ -157,7 +158,7 @@ export function insertBefore (data, path, values) { // TODO: find a better name
|
|||
else { // object.type === 'Object'
|
||||
const before = last(path)
|
||||
return values.map(entry => {
|
||||
const newProp = findUniqueName(entry.name, parent.props.map(p => p.name))
|
||||
const newProp = findUniqueName(entry.name, parent[META].keys)
|
||||
return {
|
||||
op: 'add',
|
||||
path: compileJSONPointer(parentPath.concat(newProp)),
|
||||
|
@ -178,18 +179,18 @@ export function insertBefore (data, path, values) { // TODO: find a better name
|
|||
* a unique property name for the inserted node in case of duplicating
|
||||
* and object property
|
||||
*
|
||||
* @param {ESON} data
|
||||
* @param {ESON} eson
|
||||
* @param {Selection} selection
|
||||
* @param {Array.<{name?: string, value: JSONType, type?: ESONType}>} values
|
||||
* @return {Array}
|
||||
*/
|
||||
export function replace (data, selection, values) { // TODO: find a better name and define datastructure for values
|
||||
export function replace (eson, selection, values) { // TODO: find a better name and define datastructure for values
|
||||
const rootPath = findRootPath(selection)
|
||||
const root = getInEson(data, rootPath)
|
||||
const root = getIn(eson, rootPath)
|
||||
const { minIndex, maxIndex } = findSelectionIndices(root, rootPath, selection)
|
||||
|
||||
if (root.type === 'Array') {
|
||||
const removeActions = removeAll(pathsFromSelection(data, selection))
|
||||
const removeActions = removeAll(pathsFromSelection(eson, selection))
|
||||
const insertActions = values.map((entry, offset) => ({
|
||||
op: 'add',
|
||||
path: compileJSONPointer(rootPath.concat(minIndex + offset)),
|
||||
|
@ -202,12 +203,11 @@ export function replace (data, selection, values) { // TODO: find a better name
|
|||
return removeActions.concat(insertActions)
|
||||
}
|
||||
else { // object.type === 'Object'
|
||||
const nextProp = root.props && root.props[maxIndex]
|
||||
const before = nextProp ? nextProp.name : null
|
||||
const before = root[META].keys[maxIndex] || null
|
||||
|
||||
const removeActions = removeAll(pathsFromSelection(data, selection))
|
||||
const removeActions = removeAll(pathsFromSelection(eson, selection))
|
||||
const insertActions = values.map(entry => {
|
||||
const newProp = findUniqueName(entry.name, root.props.map(p => p.name))
|
||||
const newProp = findUniqueName(entry.name, root[META].keys)
|
||||
return {
|
||||
op: 'add',
|
||||
path: compileJSONPointer(rootPath.concat(newProp)),
|
||||
|
@ -230,15 +230,15 @@ export function replace (data, selection, values) { // TODO: find a better name
|
|||
* a unique property name for the inserted node in case of duplicating
|
||||
* and object property
|
||||
*
|
||||
* @param {ESON} data
|
||||
* @param {ESON} eson
|
||||
* @param {Path} parentPath
|
||||
* @param {ESONType} type
|
||||
* @return {Array}
|
||||
*/
|
||||
export function append (data, parentPath, type) {
|
||||
export function append (eson, parentPath, type) {
|
||||
// console.log('append', parentPath, value)
|
||||
|
||||
const parent = getInEson(data, parentPath)
|
||||
const parent = getIn(eson, parentPath)
|
||||
const value = createEntry(type)
|
||||
|
||||
if (parent.type === 'Array') {
|
||||
|
@ -252,7 +252,7 @@ export function append (data, parentPath, type) {
|
|||
}]
|
||||
}
|
||||
else { // object.type === 'Object'
|
||||
const newProp = findUniqueName('', parent.props.map(p => p.name))
|
||||
const newProp = findUniqueName('', parent[META].keys)
|
||||
|
||||
return [{
|
||||
op: 'add',
|
||||
|
@ -295,50 +295,49 @@ export function removeAll (paths) {
|
|||
/**
|
||||
* Create a JSONPatch to order the items of an array or the properties of an object in ascending
|
||||
* or descending order
|
||||
* @param {ESON} data
|
||||
* @param {ESON} eson
|
||||
* @param {Path} path
|
||||
* @param {'asc' | 'desc' | null} [order=null] If not provided, will toggle current ordering
|
||||
* @return {Array}
|
||||
*/
|
||||
export function sort (data, path, order = null) {
|
||||
export function sort (eson, path, order = null) {
|
||||
// console.log('sort', path, order)
|
||||
|
||||
const compare = order === 'desc' ? compareDesc : compareAsc
|
||||
const object = getInEson(data, path)
|
||||
const object = getIn(eson, path)
|
||||
|
||||
if (object.type === 'Array') {
|
||||
const orderedItems = object.items.slice(0)
|
||||
const orderedItems = object.slice()
|
||||
|
||||
// order the items by value
|
||||
orderedItems.sort((a, b) => compare(a.value, b.value))
|
||||
|
||||
// when no order is provided, test whether ordering ascending
|
||||
// changed anything. If not, sort descending
|
||||
if (!order && strictShallowEqual(object.items, orderedItems)) {
|
||||
if (!order && strictShallowEqual(object, orderedItems)) {
|
||||
orderedItems.reverse()
|
||||
}
|
||||
|
||||
return [{
|
||||
op: 'replace',
|
||||
path: compileJSONPointer(path),
|
||||
value: esonToJson({
|
||||
type: 'Array',
|
||||
items: orderedItems
|
||||
})
|
||||
value: orderedItems
|
||||
}]
|
||||
}
|
||||
else { // object.type === 'Object'
|
||||
const orderedProps = object.props.slice(0)
|
||||
|
||||
// order the properties by key
|
||||
orderedProps.sort((a, b) => compare(a.name, b.name))
|
||||
const orderedKeys = object[META].keys.slice().sort((a, b) => compare(a.name, b.name))
|
||||
|
||||
// when no order is provided, test whether ordering ascending
|
||||
// changed anything. If not, sort descending
|
||||
if (!order && strictShallowEqual(object.props, orderedProps)) {
|
||||
orderedProps.reverse()
|
||||
if (!order && strictShallowEqual(object[META].keys, orderedKeys)) {
|
||||
orderedKeys.reverse()
|
||||
}
|
||||
|
||||
const orderedProps = cloneWithSymbols(object)
|
||||
orderedProps[META] = setIn(object[META], ['keys'], orderedKeys)
|
||||
|
||||
return [{
|
||||
op: 'replace',
|
||||
path: compileJSONPointer(path),
|
||||
|
|
|
@ -5,7 +5,7 @@ import Ajv from 'ajv'
|
|||
import { parseJSON } from '../utils/jsonUtils'
|
||||
import { escapeUnicodeChars } from '../utils/stringUtils'
|
||||
import { enrichSchemaError, limitErrors } from '../utils/schemaUtils'
|
||||
import { jsonToEsonOld, esonToJson } from '../eson'
|
||||
import { jsonToEson, esonToJson } from '../eson'
|
||||
import { patchEson } from '../patchEson'
|
||||
import { createFindKeyBinding } from '../utils/keyBindings'
|
||||
import { KEY_BINDINGS } from '../constants'
|
||||
|
@ -334,7 +334,7 @@ export default class TextMode extends Component {
|
|||
patch (actions) {
|
||||
const json = this.get()
|
||||
|
||||
const data = jsonToEsonOld(json)
|
||||
const data = jsonToEson(json)
|
||||
const result = patchEson(data, actions)
|
||||
|
||||
this.set(esonToJson(result.data))
|
||||
|
|
|
@ -336,19 +336,19 @@ export default class TreeMode extends Component {
|
|||
}
|
||||
|
||||
handleChangeValue = (path, value) => {
|
||||
this.handlePatch(changeValue(this.state.data, path, value))
|
||||
this.handlePatch(changeValue(this.state.eson, path, value))
|
||||
}
|
||||
|
||||
handleChangeProperty = (parentPath, oldProp, newProp) => {
|
||||
this.handlePatch(changeProperty(this.state.data, parentPath, oldProp, newProp))
|
||||
this.handlePatch(changeProperty(this.state.eson, parentPath, oldProp, newProp))
|
||||
}
|
||||
|
||||
handleChangeType = (path, type) => {
|
||||
this.handlePatch(changeType(this.state.data, path, type))
|
||||
this.handlePatch(changeType(this.state.eson, path, type))
|
||||
}
|
||||
|
||||
handleInsert = (path, type) => {
|
||||
this.handlePatch(insertBefore(this.state.data, path, [{
|
||||
this.handlePatch(insertBefore(this.state.eson, path, [{
|
||||
type,
|
||||
name: '',
|
||||
value: createEntry(type)
|
||||
|
@ -367,7 +367,7 @@ export default class TreeMode extends Component {
|
|||
}
|
||||
|
||||
handleAppend = (parentPath, type) => {
|
||||
this.handlePatch(append(this.state.data, parentPath, type))
|
||||
this.handlePatch(append(this.state.eson, parentPath, type))
|
||||
|
||||
// apply focus to new node
|
||||
this.focusToNext(parentPath)
|
||||
|
@ -375,7 +375,7 @@ export default class TreeMode extends Component {
|
|||
|
||||
handleDuplicate = () => {
|
||||
if (this.state.selection) {
|
||||
this.handlePatch(duplicate(this.state.data, this.state.selection))
|
||||
this.handlePatch(duplicate(this.state.eson, this.state.selection))
|
||||
// TODO: focus to duplicated selection
|
||||
}
|
||||
}
|
||||
|
@ -395,7 +395,7 @@ export default class TreeMode extends Component {
|
|||
else if (this.state.selection) {
|
||||
// remove selection
|
||||
// TODO: select next property? (same as when removing a path?)
|
||||
const paths = pathsFromSelection(this.state.data, this.state.selection)
|
||||
const paths = pathsFromSelection(this.state.eson, this.state.selection)
|
||||
this.setState({ selection: null })
|
||||
this.handlePatch(removeAll(paths))
|
||||
}
|
||||
|
@ -446,13 +446,13 @@ export default class TreeMode extends Component {
|
|||
}
|
||||
|
||||
handleKeyDownPaste = (event) => {
|
||||
const { clipboard, data } = this.state
|
||||
const { clipboard, eson } = this.state
|
||||
|
||||
if (clipboard && clipboard.length > 0) {
|
||||
event.preventDefault()
|
||||
|
||||
const path = this.findDataPathFromElement(event.target)
|
||||
this.handlePatch(insertBefore(data, path, clipboard))
|
||||
this.handlePatch(insertBefore(eson, path, clipboard))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -460,7 +460,7 @@ export default class TreeMode extends Component {
|
|||
const path = this.findDataPathFromElement(event.target)
|
||||
if (path) {
|
||||
const selection = { start: path, end: path }
|
||||
this.handlePatch(duplicate(this.state.data, selection))
|
||||
this.handlePatch(duplicate(this.state.eson, selection))
|
||||
|
||||
// apply focus to the duplicated node
|
||||
this.focusToNext(path)
|
||||
|
@ -470,9 +470,9 @@ export default class TreeMode extends Component {
|
|||
handleCut = () => {
|
||||
const selection = this.state.selection
|
||||
if (selection && selection.start && selection.end) {
|
||||
const data = this.state.data
|
||||
const paths = pathsFromSelection(data, selection)
|
||||
const clipboard = contentsFromPaths(data, paths)
|
||||
const eson = this.state.eson
|
||||
const paths = pathsFromSelection(eson, selection)
|
||||
const clipboard = contentsFromPaths(eson, paths)
|
||||
|
||||
this.setState({ clipboard, selection: null })
|
||||
|
||||
|
@ -490,9 +490,9 @@ export default class TreeMode extends Component {
|
|||
handleCopy = () => {
|
||||
const selection = this.state.selection
|
||||
if (selection && selection.start && selection.end) {
|
||||
const data = this.state.data
|
||||
const paths = pathsFromSelection(data, selection)
|
||||
const clipboard = contentsFromPaths(data, paths)
|
||||
const eson = this.state.eson
|
||||
const paths = pathsFromSelection(eson, selection)
|
||||
const clipboard = contentsFromPaths(eson, paths)
|
||||
|
||||
this.setState({ clipboard })
|
||||
}
|
||||
|
@ -503,11 +503,11 @@ export default class TreeMode extends Component {
|
|||
}
|
||||
|
||||
handlePaste = () => {
|
||||
const { data, selection, clipboard } = this.state
|
||||
const { eson, selection, clipboard } = this.state
|
||||
|
||||
if (selection && clipboard && clipboard.length > 0) {
|
||||
this.setState({ selection: null })
|
||||
this.handlePatch(replace(data, selection, clipboard))
|
||||
this.handlePatch(replace(eson, selection, clipboard))
|
||||
// TODO: select the pasted contents
|
||||
}
|
||||
}
|
||||
|
@ -539,7 +539,7 @@ export default class TreeMode extends Component {
|
|||
}
|
||||
|
||||
handleSort = (path, order = null) => {
|
||||
this.handlePatch(sort(this.state.data, path, order))
|
||||
this.handlePatch(sort(this.state.eson, path, order))
|
||||
}
|
||||
|
||||
handleSelect = (selection: Selection) => {
|
||||
|
@ -753,13 +753,13 @@ export default class TreeMode extends Component {
|
|||
* Emit an onChange event when there is a listener for it.
|
||||
* @private
|
||||
*/
|
||||
emitOnChange (patch: ESONPatch, revert: ESONPatch, data: ESON) {
|
||||
emitOnChange (patch: ESONPatch, revert: ESONPatch, eson: ESON) {
|
||||
if (this.props.onPatch) {
|
||||
this.props.onPatch(patch, revert)
|
||||
}
|
||||
|
||||
if (this.props.onChange || this.props.onChangeText) {
|
||||
const json = esonToJson(data)
|
||||
const json = esonToJson(eson)
|
||||
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(json)
|
||||
|
@ -798,10 +798,10 @@ export default class TreeMode extends Component {
|
|||
const historyIndex = this.state.historyIndex
|
||||
const historyItem = history[historyIndex]
|
||||
|
||||
const result = patchEson(this.state.data, historyItem.undo)
|
||||
const result = patchEson(this.state.eson, historyItem.undo)
|
||||
|
||||
this.setState({
|
||||
data: result.data,
|
||||
eson: result.data,
|
||||
history,
|
||||
historyIndex: historyIndex + 1
|
||||
})
|
||||
|
@ -816,10 +816,10 @@ export default class TreeMode extends Component {
|
|||
const historyIndex = this.state.historyIndex - 1
|
||||
const historyItem = history[historyIndex]
|
||||
|
||||
const result = patchEson(this.state.data, historyItem.redo)
|
||||
const result = patchEson(this.state.eson, historyItem.redo)
|
||||
|
||||
this.setState({
|
||||
data: result.data,
|
||||
eson: result.data,
|
||||
history,
|
||||
historyIndex
|
||||
})
|
||||
|
@ -845,10 +845,10 @@ export default class TreeMode extends Component {
|
|||
}
|
||||
|
||||
const expand = options.expand || (path => this.expandKeepOrExpandAll(path))
|
||||
const result = patchEson(this.state.data, actions, expand)
|
||||
const data = result.data
|
||||
const result = patchEson(this.state.eson, actions, expand)
|
||||
const eson = result.data
|
||||
|
||||
if (this.props.history != false) {
|
||||
if (this.props.history !== false) {
|
||||
// update data and store history
|
||||
const historyItem = {
|
||||
redo: actions,
|
||||
|
@ -860,21 +860,21 @@ export default class TreeMode extends Component {
|
|||
.slice(0, MAX_HISTORY_ITEMS)
|
||||
|
||||
this.setState({
|
||||
data,
|
||||
eson,
|
||||
history,
|
||||
historyIndex: 0
|
||||
})
|
||||
}
|
||||
else {
|
||||
// update data and don't store history
|
||||
this.setState({ data })
|
||||
this.setState({ eson })
|
||||
}
|
||||
|
||||
return {
|
||||
patch: actions,
|
||||
revert: result.revert,
|
||||
error: result.error,
|
||||
data // FIXME: shouldn't pass data here
|
||||
data: eson // FIXME: shouldn't pass data here
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -973,7 +973,7 @@ export default class TreeMode extends Component {
|
|||
* @param {Path} path
|
||||
*/
|
||||
exists (path) {
|
||||
return pathExists(this.state.data, path)
|
||||
return pathExists(this.state.eson, path)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
276
src/eson.js
276
src/eson.js
|
@ -15,7 +15,7 @@ import initial from 'lodash/initial'
|
|||
import last from 'lodash/last'
|
||||
|
||||
import type {
|
||||
ESON, ESONObject, ESONArrayItem, ESONPointer, Selection, ESONType, ESONPath,
|
||||
ESON, ESONObject, ESONArrayItem, ESONPointer, Selection, ESONPath,
|
||||
Path,
|
||||
JSONPath, JSONType
|
||||
} from './types'
|
||||
|
@ -29,6 +29,15 @@ export const SELECTED_AFTER = 4
|
|||
|
||||
export const META = Symbol('meta')
|
||||
|
||||
/**
|
||||
* Expand function which will expand all nodes
|
||||
* @param {Path} path
|
||||
* @return {boolean}
|
||||
*/
|
||||
export function expandAll (path) {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {JSONType} json
|
||||
|
@ -58,57 +67,6 @@ export function jsonToEson (json, path = []) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand function which will expand all nodes
|
||||
* @param {Path} path
|
||||
* @return {boolean}
|
||||
*/
|
||||
export function expandAll (path) {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a JSON object into ESON
|
||||
* @param {Object | Array | string | number | boolean | null} json
|
||||
* @param {function(path: JSONPath)} [expand]
|
||||
* @param {JSONPath} [path=[]]
|
||||
* @param {ESONType} [type='value'] Optional eson type for the created value
|
||||
* @return {ESON}
|
||||
*/
|
||||
export function jsonToEsonOld (json, expand = expandAll, path: JSONPath = [], type: ESONType = 'value') : ESON {
|
||||
if (Array.isArray(json)) {
|
||||
return {
|
||||
type: 'Array',
|
||||
expanded: expand(path),
|
||||
items: json.map((child, index) => {
|
||||
return {
|
||||
id: createId(), // TODO: use id based on index (only has to be unique within this array)
|
||||
value: jsonToEsonOld(child, expand, path.concat(index))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
else if (isObject(json)) {
|
||||
return {
|
||||
type: 'Object',
|
||||
expanded: expand(path),
|
||||
props: Object.keys(json).map((name, index) => {
|
||||
return {
|
||||
id: createId(), // TODO: use id based on index (only has to be unique within this array)
|
||||
name,
|
||||
value: jsonToEsonOld(json[name], expand, path.concat(name))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
else { // value
|
||||
return {
|
||||
type: (type === 'string') ? 'string' : 'value',
|
||||
value: json
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an ESON object to a JSON object
|
||||
* @param {ESON} eson
|
||||
|
@ -133,103 +91,6 @@ export function esonToJson (eson: ESON) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a path of a JSON object into a path in the corresponding ESON object
|
||||
* @param {ESON} eson
|
||||
* @param {JSONPath} path
|
||||
* @return {ESONPath} esonPath
|
||||
* @private
|
||||
*/
|
||||
export function toEsonPath (eson: ESON, path: JSONPath) : ESONPath {
|
||||
if (path.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (eson.type === 'Array') {
|
||||
// index of an array
|
||||
const index = path[0]
|
||||
const item = eson.items[parseInt(index)]
|
||||
if (!item) {
|
||||
throw new Error('Array item "' + index + '" not found')
|
||||
}
|
||||
|
||||
return ['items', String(index), 'value'].concat(toEsonPath(item.value, path.slice(1)))
|
||||
}
|
||||
else if (eson.type === 'Object') {
|
||||
// object property. find the index of this property
|
||||
const index = findPropertyIndex(eson, path[0])
|
||||
const prop = eson.props[index]
|
||||
if (!prop) {
|
||||
throw new Error('Object property "' + path[0] + '" not found')
|
||||
}
|
||||
|
||||
return ['props', String(index), 'value']
|
||||
.concat(toEsonPath(prop.value, path.slice(1)))
|
||||
}
|
||||
else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an ESON object to a JSON object
|
||||
* @param {ESON} eson
|
||||
* @param {ESONPath} esonPath
|
||||
* @return {JSONPath} path
|
||||
*/
|
||||
export function toJsonPath (eson: ESON, esonPath: ESONPath) : JSONPath {
|
||||
if (esonPath.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (eson.type === 'Array') {
|
||||
// index of an array
|
||||
const index = esonPath[1]
|
||||
const item = eson.items[parseInt(index)]
|
||||
if (!item) {
|
||||
throw new Error('Array item "' + index + '" not found')
|
||||
}
|
||||
|
||||
return [index].concat(toJsonPath(item.value, esonPath.slice(3)))
|
||||
}
|
||||
else if (eson.type === 'Object') {
|
||||
// object property. find the index of this property
|
||||
const index = esonPath[1]
|
||||
const prop = eson.props[parseInt(index)]
|
||||
if (!prop) {
|
||||
throw new Error('Object property "' + esonPath[1] + '" not found')
|
||||
}
|
||||
|
||||
return [prop.name].concat(toJsonPath(prop.value, esonPath.slice(3)))
|
||||
}
|
||||
else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a nested property from an ESON object using a JSON path
|
||||
*/
|
||||
export function getInEson (eson: ESON, jsonPath: JSONPath) {
|
||||
return getIn(eson, toEsonPath(eson, jsonPath))
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of a nested property in an ESON object using a JSON path
|
||||
*/
|
||||
export function setInEson (eson: ESON, jsonPath: JSONPath, value: JSONType) {
|
||||
return setIn(eson, toEsonPath(eson, jsonPath), value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of a nested property in an ESON object using a JSON path
|
||||
*/
|
||||
export function deleteInEson (eson: ESON, jsonPath: JSONPath) : JSONType {
|
||||
// with initial we remove the 'value' property,
|
||||
// we want to remove the whole item from the items array
|
||||
return deleteIn(eson, initial(toEsonPath(eson, jsonPath)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform an eson object, traverse over the whole object (excluding the _meta)
|
||||
* objects, and allow replacing Objects/Arrays/values
|
||||
|
@ -569,14 +430,19 @@ export function applySelection (eson, selection) {
|
|||
* Find the min and max index of a start and end child.
|
||||
* Start and end can be a property name in case of an Object,
|
||||
* or a matrix index (string with a number) in case of an Array.
|
||||
*
|
||||
* @param {ESON} root
|
||||
* @param {Path} rootPath
|
||||
* @param {Selection} selection
|
||||
* @return {{minIndex: number, maxIndex: number}}
|
||||
*/
|
||||
export function findSelectionIndices (root: ESON, rootPath: JSONPath, selection: Selection) : { minIndex: number, maxIndex: number } {
|
||||
export function findSelectionIndices (root, rootPath, selection) {
|
||||
const start = (selection.after || selection.before || selection.start)[rootPath.length]
|
||||
const end = (selection.after || selection.before || selection.end)[rootPath.length]
|
||||
|
||||
// if no object we assume it's an Array
|
||||
const startIndex = root.type === 'Object' ? findPropertyIndex(root, start) : parseInt(start)
|
||||
const endIndex = root.type === 'Object' ? findPropertyIndex(root, end) : parseInt(end)
|
||||
const startIndex = root[META].type === 'Object' ? root[META].keys.indexOf(start) : parseInt(start)
|
||||
const endIndex = root[META].type === 'Object' ? root[META].keys.indexOf(end) : parseInt(end)
|
||||
|
||||
const minIndex = Math.min(startIndex, endIndex)
|
||||
const maxIndex = Math.max(startIndex, endIndex) +
|
||||
|
@ -588,15 +454,15 @@ export function findSelectionIndices (root: ESON, rootPath: JSONPath, selection:
|
|||
/**
|
||||
* Get the JSON paths from a selection, sorted from first to last
|
||||
*/
|
||||
export function pathsFromSelection (eson: ESON, selection: Selection): JSONPath[] {
|
||||
export function pathsFromSelection (eson, selection: Selection): JSONPath[] {
|
||||
// find the parent node shared by both start and end of the selection
|
||||
const rootPath = findRootPath(selection)
|
||||
const root = getInEson(eson, rootPath)
|
||||
const root = getIn(eson, rootPath)
|
||||
|
||||
const { minIndex, maxIndex } = findSelectionIndices(root, rootPath, selection)
|
||||
|
||||
if (root.type === 'Object') {
|
||||
return times(maxIndex - minIndex, i => rootPath.concat(root.props[i + minIndex].name))
|
||||
if (root[META].type === 'Object') {
|
||||
return times(maxIndex - minIndex, i => rootPath.concat(root[META].keys[i + minIndex]))
|
||||
}
|
||||
else { // root.type === 'Array'
|
||||
return times(maxIndex - minIndex, i => rootPath.concat(String(i + minIndex)))
|
||||
|
@ -611,10 +477,9 @@ export function pathsFromSelection (eson: ESON, selection: Selection): JSONPath[
|
|||
*/
|
||||
export function contentsFromPaths (data: ESON, paths: JSONPath[]) {
|
||||
return paths.map(path => {
|
||||
const esonPath = toEsonPath(data, path)
|
||||
return {
|
||||
name: getIn(data, initial(esonPath).concat('name')) || String(esonPath[esonPath.length - 2]),
|
||||
value: esonToJson(getIn(data, esonPath))
|
||||
name: getIn(data, last(path)),
|
||||
value: esonToJson(getIn(data, path))
|
||||
// FIXME: also store the type and expanded state
|
||||
}
|
||||
})
|
||||
|
@ -659,93 +524,6 @@ function findSharedPath (path1: JSONPath, path2: JSONPath): JSONPath {
|
|||
|
||||
return path1.slice(0, i)
|
||||
}
|
||||
//
|
||||
// /**
|
||||
// * Recursively transform ESON: a recursive "map" function
|
||||
// * @param {ESON} eson
|
||||
// * @param {function(value: ESON, path: Path, root: ESON)} callback
|
||||
// * @return {ESON} Returns the transformed eson object
|
||||
// */
|
||||
// export function transform (eson: ESON, callback: RecurseCallback) : ESON {
|
||||
// return recurseTransform (eson, [], eson, callback)
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Recursively transform ESON
|
||||
// * @param {ESON} value
|
||||
// * @param {JSONPath} path
|
||||
// * @param {ESON} root The root object, object at path=[]
|
||||
// * @param {function(value: ESON, path: Path, root: ESON)} callback
|
||||
// * @return {ESON} Returns the transformed eson object
|
||||
// */
|
||||
// function recurseTransform (value: ESON, path: JSONPath, root: ESON, callback: RecurseCallback) : ESON {
|
||||
// let updatedValue: ESON = callback(value, path, root)
|
||||
//
|
||||
// if (value.type === 'Array') {
|
||||
// let updatedItems = updatedValue.items
|
||||
//
|
||||
// updatedValue.items.forEach((item, index) => {
|
||||
// const updatedItem = recurseTransform(item.value, path.concat(String(index)), root, callback)
|
||||
// updatedItems = setIn(updatedItems, [index, 'value'], updatedItem)
|
||||
// })
|
||||
//
|
||||
// updatedValue = setIn(updatedValue, ['items'], updatedItems)
|
||||
// }
|
||||
//
|
||||
// if (value.type === 'Object') {
|
||||
// let updatedProps = updatedValue.props
|
||||
//
|
||||
// updatedValue.props.forEach((prop, index) => {
|
||||
// const updatedItem = recurseTransform(prop.value, path.concat(prop.name), root, callback)
|
||||
// updatedProps = setIn(updatedProps, [index, 'value'], updatedItem)
|
||||
// })
|
||||
//
|
||||
// updatedValue = setIn(updatedValue, ['props'], updatedProps)
|
||||
// }
|
||||
//
|
||||
// // (for type 'string' or 'value' there are no childs to traverse)
|
||||
//
|
||||
// return updatedValue
|
||||
// }
|
||||
|
||||
/**
|
||||
* Recursively loop over a ESON object: a recursive "forEach" function.
|
||||
* @param {ESON} eson
|
||||
* @param {function(value: ESON, path: JSONPath, root: ESON)} callback
|
||||
*/
|
||||
export function traverse (eson: ESON, callback: RecurseCallback) {
|
||||
return recurseTraverse (eson, [], eson, callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively traverse a ESON object
|
||||
* @param {ESON} value
|
||||
* @param {JSONPath} path
|
||||
* @param {ESON | null} root The root object, object at path=[]
|
||||
* @param {function(value: ESON, path: Path, root: ESON)} callback
|
||||
*/
|
||||
function recurseTraverse (value: ESON, path: JSONPath, root: ESON, callback: RecurseCallback) {
|
||||
callback(value, path, root)
|
||||
|
||||
switch (value.type) {
|
||||
case 'Array': {
|
||||
value.items.forEach((item: ESONArrayItem, index) => {
|
||||
recurseTraverse(item.value, path.concat(String(index)), root, callback)
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'Object': {
|
||||
value.props.forEach((prop) => {
|
||||
recurseTraverse(prop.value, path.concat(prop.name), root, callback)
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
default: // type 'string' or 'value'
|
||||
// no childs to traverse
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether a path exists in the eson object
|
||||
|
@ -808,12 +586,12 @@ export function findNextProp (parent, prop) {
|
|||
|
||||
/**
|
||||
* Find the index of a property
|
||||
* @param {ESONObject} object
|
||||
* @param {ESON} object
|
||||
* @param {string} prop
|
||||
* @return {number} Returns the index when found, -1 when not found
|
||||
*/
|
||||
export function findPropertyIndex (object: ESONObject, prop: string) {
|
||||
return object.props.findIndex(p => p.name === prop)
|
||||
export function findPropertyIndex (object, prop) {
|
||||
return object[META].keys.indexOf(prop)
|
||||
}
|
||||
|
||||
// TODO: move parseJSONPointer and compileJSONPointer to a separate file
|
||||
|
|
|
@ -3,7 +3,7 @@ import test from 'ava'
|
|||
import { setIn, getIn, deleteIn } from '../src/utils/immutabilityHelpers'
|
||||
import {
|
||||
META,
|
||||
esonToJson, toEsonPath, toJsonPath, pathExists, transform, traverse,
|
||||
esonToJson, pathExists, transform,
|
||||
parseJSONPointer, compileJSONPointer,
|
||||
jsonToEson,
|
||||
expand, expandOne, expandPath, applyErrors, search, nextSearchResult,
|
||||
|
@ -15,8 +15,6 @@ import 'console.table'
|
|||
import repeat from 'lodash/repeat'
|
||||
import { assertDeepEqualEson } from './utils/assertDeepEqualEson'
|
||||
|
||||
const JSON1 = loadJSON('./resources/json1.json')
|
||||
const ESON1 = loadJSON('./resources/eson1.json')
|
||||
const ESON2 = loadJSON('./resources/eson2.json')
|
||||
|
||||
test('jsonToEson', t => {
|
||||
|
@ -236,33 +234,6 @@ test('add and remove errors', t => {
|
|||
t.is(actual3.str, eson.str) // shouldn't have touched values not affected by the errors
|
||||
})
|
||||
|
||||
test('traverse', t => {
|
||||
// {obj: {a: 2}, arr: [3]}
|
||||
|
||||
let log = []
|
||||
const returnValue = traverse(ESON2, function (value, path, root) {
|
||||
t.is(root, ESON2)
|
||||
|
||||
log.push([value, path, root])
|
||||
})
|
||||
|
||||
t.is(returnValue, undefined)
|
||||
|
||||
const EXPECTED_LOG = [
|
||||
[ESON2, [], ESON2],
|
||||
[ESON2.props[0].value, ['obj'], ESON2],
|
||||
[ESON2.props[0].value.props[0].value, ['obj', 'a'], ESON2],
|
||||
[ESON2.props[1].value, ['arr'], ESON2],
|
||||
[ESON2.props[1].value.items[0].value, ['arr', '0'], ESON2],
|
||||
]
|
||||
|
||||
log.forEach((row, index) => {
|
||||
t.deepEqual(log[index], EXPECTED_LOG[index], 'should have equal log at index ' + index )
|
||||
})
|
||||
t.deepEqual(log, EXPECTED_LOG)
|
||||
})
|
||||
|
||||
|
||||
test('search', t => {
|
||||
const eson = jsonToEson({
|
||||
"obj": {
|
||||
|
@ -476,12 +447,20 @@ test('selection (node)', t => {
|
|||
})
|
||||
|
||||
test('pathsFromSelection (object)', t => {
|
||||
const eson = jsonToEson({
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
"str": "hello world",
|
||||
"nill": null,
|
||||
"bool": false
|
||||
})
|
||||
const selection = {
|
||||
start: ['obj', 'arr', '2', 'last'],
|
||||
end: ['nill']
|
||||
}
|
||||
|
||||
t.deepEqual(pathsFromSelection(ESON1, selection), [
|
||||
t.deepEqual(pathsFromSelection(eson, selection), [
|
||||
['obj'],
|
||||
['str'],
|
||||
['nill']
|
||||
|
@ -489,42 +468,74 @@ test('pathsFromSelection (object)', t => {
|
|||
})
|
||||
|
||||
test('pathsFromSelection (array)', t => {
|
||||
const eson = jsonToEson({
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
"str": "hello world",
|
||||
"nill": null,
|
||||
"bool": false
|
||||
})
|
||||
const selection = {
|
||||
start: ['obj', 'arr', '1'],
|
||||
end: ['obj', 'arr', '0'] // note the "wrong" order of start and end
|
||||
}
|
||||
|
||||
t.deepEqual(pathsFromSelection(ESON1, selection), [
|
||||
t.deepEqual(pathsFromSelection(eson, selection), [
|
||||
['obj', 'arr', '0'],
|
||||
['obj', 'arr', '1']
|
||||
])
|
||||
})
|
||||
|
||||
test('pathsFromSelection (value)', t => {
|
||||
const eson = jsonToEson({
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
"str": "hello world",
|
||||
"nill": null,
|
||||
"bool": false
|
||||
})
|
||||
const selection = {
|
||||
start: ['obj', 'arr', '2', 'first'],
|
||||
end: ['obj', 'arr', '2', 'first']
|
||||
}
|
||||
|
||||
t.deepEqual(pathsFromSelection(ESON1, selection), [
|
||||
t.deepEqual(pathsFromSelection(eson, selection), [
|
||||
['obj', 'arr', '2', 'first'],
|
||||
])
|
||||
})
|
||||
|
||||
test('pathsFromSelection (before)', t => {
|
||||
const eson = jsonToEson({
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
"str": "hello world",
|
||||
"nill": null,
|
||||
"bool": false
|
||||
})
|
||||
const selection = {
|
||||
before: ['obj', 'arr', '2', 'first']
|
||||
}
|
||||
|
||||
t.deepEqual(pathsFromSelection(ESON1, selection), [])
|
||||
t.deepEqual(pathsFromSelection(eson, selection), [])
|
||||
})
|
||||
|
||||
test('pathsFromSelection (after)', t => {
|
||||
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']
|
||||
}
|
||||
|
||||
t.deepEqual(pathsFromSelection(ESON1, selection), [])
|
||||
t.deepEqual(pathsFromSelection(eson, selection), [])
|
||||
})
|
||||
|
||||
// helper function to print JSON in the console
|
||||
|
|
Loading…
Reference in New Issue