Selection working again
This commit is contained in:
parent
378c8ef250
commit
f491a00575
|
@ -8,13 +8,13 @@ import Hammer from 'react-hammerjs'
|
|||
import jump from '../assets/jump.js/src/jump'
|
||||
import Ajv from 'ajv'
|
||||
|
||||
import { setIn, updateIn } from '../utils/immutabilityHelpers'
|
||||
import { getIn, setIn, updateIn } from '../utils/immutabilityHelpers'
|
||||
import { parseJSON } from '../utils/jsonUtils'
|
||||
import { enrichSchemaError } from '../utils/schemaUtils'
|
||||
import {
|
||||
jsonToEson, esonToJson, getInEson, updateInEson, pathExists,
|
||||
expand, expandOne, expandPath, updateErrors,
|
||||
search, applySearchResults, nextSearchResult, previousSearchResult,
|
||||
jsonToEson, esonToJson, pathExists,
|
||||
expand, expandOne, expandPath, applyErrors,
|
||||
search, nextSearchResult, previousSearchResult,
|
||||
applySelection, pathsFromSelection, contentsFromPaths,
|
||||
compileJSONPointer, parseJSONPointer
|
||||
} from '../eson'
|
||||
|
@ -195,9 +195,10 @@ export default class TreeMode extends Component {
|
|||
|
||||
let eson = state.eson
|
||||
|
||||
// enrich the data with JSON Schema errors
|
||||
// TODO: for optimization, we can apply errors only when the eson is changed? (a wrapper around setState or something?) Takes about 7ms in large documents
|
||||
eson = updateErrors(eson, this.getErrors())
|
||||
// enrich the eson with selection and JSON Schema errors
|
||||
// TODO: for optimization, we can apply errors only when the eson is changed? (a wrapper around setState or something?)
|
||||
eson = applyErrors(eson, this.getErrors())
|
||||
eson = applySelection(eson, this.state.selection)
|
||||
|
||||
return h('div', {
|
||||
className: `jsoneditor jsoneditor-mode-${props.mode}`,
|
||||
|
@ -981,7 +982,7 @@ export default class TreeMode extends Component {
|
|||
* @return {boolean} Returns true when expanded, false otherwise
|
||||
*/
|
||||
isExpanded (path) {
|
||||
return getInEson(this.state.data, path).expanded
|
||||
return getIn(this.state.eson, path)._meta.expanded
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
135
src/eson.js
135
src/eson.js
|
@ -9,6 +9,7 @@ import { setIn, getIn, updateIn, deleteIn } from './utils/immutabilityHelpers'
|
|||
import { isObject } from './utils/typeUtils'
|
||||
import isEqual from 'lodash/isEqual'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import range from 'lodash/range'
|
||||
import times from 'lodash/times'
|
||||
import initial from 'lodash/initial'
|
||||
import last from 'lodash/last'
|
||||
|
@ -337,25 +338,38 @@ export function expandPath (eson, path, expanded = true) {
|
|||
* @param {JSONSchemaError[]} errors
|
||||
* @return {ESON}
|
||||
*/
|
||||
export function updateErrors (eson, errors = []) {
|
||||
let updatedEson = eson
|
||||
export function applyErrors (eson, errors = []) {
|
||||
const errorPaths = errors.map(error => error.dataPath)
|
||||
|
||||
if (!isEmpty(errors)) {
|
||||
errors.forEach(error => {
|
||||
const esonWithErrors = errors.reduce((eson, error) => {
|
||||
const path = parseJSONPointer(error.dataPath)
|
||||
// TODO: do we want to be able to store multiple errors per item?
|
||||
updatedEson = setIn(updatedEson, path.concat(['_meta', 'error']), error)
|
||||
})
|
||||
}
|
||||
return setIn(eson, path.concat(['_meta', 'error']), error)
|
||||
}, eson)
|
||||
|
||||
// cleanup any old error messages
|
||||
updatedEson = transform(updatedEson, function (value, path) {
|
||||
return (value._meta.error && !contains(errors, value._meta.error))
|
||||
? deleteIn(value, ['_meta', 'error'])
|
||||
: value
|
||||
return cleanupMetaData(esonWithErrors, 'error', errorPaths)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup meta data from an eson object
|
||||
* @param {ESON} eson Object to be cleaned up
|
||||
* @param {String} field Field name, for example 'error' or 'selected'
|
||||
* @param {String[] | JSONPath[]} [ignorePaths=[]] An optional array with paths to be ignored
|
||||
* @return {ESON}
|
||||
*/
|
||||
export function cleanupMetaData(eson, field, ignorePaths = []) {
|
||||
const pathsMap = {}
|
||||
ignorePaths.forEach(path => {
|
||||
const pathString = (typeof path === 'string') ? path : compileJSONPointer(path)
|
||||
pathsMap[pathString] = true
|
||||
})
|
||||
|
||||
return updatedEson
|
||||
return transform(eson, function (value, path) {
|
||||
return (value._meta[field] && !pathsMap[compileJSONPointer(path)])
|
||||
? deleteIn(value, ['_meta', field])
|
||||
: value
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -472,61 +486,70 @@ function setSearchStatus (eson, esonPointer, searchStatus) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Merge searchResults into the eson object
|
||||
* Merge selection status into the eson object, cleanup previous selection
|
||||
* @param {ESON} eson
|
||||
* @param {Selection} selection
|
||||
* @return {ESON} Returns updated eson object
|
||||
*/
|
||||
export function applySearchResults (eson: ESON, searchResults: ESONPointer[], activeSearchResult: ESONPointer) {
|
||||
let updatedEson = eson
|
||||
|
||||
searchResults.forEach(function (searchResult) {
|
||||
if (searchResult.area === 'value') {
|
||||
const esonPath = toEsonPath(updatedEson, searchResult.path).concat('searchResult')
|
||||
const value = isEqual(searchResult, activeSearchResult) ? 'active' : 'normal'
|
||||
updatedEson = setIn(updatedEson, esonPath, value)
|
||||
}
|
||||
|
||||
if (searchResult.area === 'property') {
|
||||
const esonPath = toEsonPath(updatedEson, searchResult.path)
|
||||
const propertyPath = initial(esonPath).concat('searchResult')
|
||||
const value = isEqual(searchResult, activeSearchResult) ? 'active' : 'normal'
|
||||
updatedEson = setIn(updatedEson, propertyPath, value)
|
||||
}
|
||||
})
|
||||
|
||||
return updatedEson
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge searchResults into the eson object
|
||||
*/
|
||||
export function applySelection (eson: ESON, selection: Selection) {
|
||||
export function applySelection (eson, selection) {
|
||||
if (!selection) {
|
||||
return eson
|
||||
return cleanupMetaData(eson, 'selected')
|
||||
}
|
||||
|
||||
if (selection.before) {
|
||||
const esonPath = toEsonPath(eson, selection.before)
|
||||
return setIn(eson, esonPath.concat('selected'), SELECTED_BEFORE)
|
||||
else if (selection.before) {
|
||||
const updatedEson = setIn(eson, selection.before.concat(['_meta', 'selected']), SELECTED_BEFORE)
|
||||
return cleanupMetaData(updatedEson, 'selected', [selection.before])
|
||||
}
|
||||
else if (selection.after) {
|
||||
const esonPath = toEsonPath(eson, selection.after)
|
||||
return setIn(eson, esonPath.concat('selected'), SELECTED_AFTER)
|
||||
const updatedEson = setIn(eson, selection.after.concat(['_meta', 'selected']), SELECTED_AFTER)
|
||||
return cleanupMetaData(updatedEson, 'selected', [selection.after])
|
||||
}
|
||||
else { // selection.start and selection.end
|
||||
// find the parent node shared by both start and end of the selection
|
||||
const rootPath = findRootPath(selection)
|
||||
let selectedPaths = null
|
||||
|
||||
return updateInEson(eson, rootPath, (root) => {
|
||||
const { minIndex, maxIndex } = findSelectionIndices(root, rootPath, selection)
|
||||
const updatedEson = updateIn(eson, rootPath, (root) => {
|
||||
const start = selection.start[rootPath.length]
|
||||
const end = selection.end[rootPath.length]
|
||||
|
||||
const childsKey = (root.type === 'Object') ? 'props' : 'items' // property name of the array with props/items
|
||||
const childsBefore = root[childsKey].slice(0, minIndex)
|
||||
const childsUpdated = root[childsKey].slice(minIndex, maxIndex)
|
||||
.map((child, index) => setIn(child, ['value', 'selected'], index === 0 ? SELECTED_END : SELECTED))
|
||||
const childsAfter = root[childsKey].slice(maxIndex)
|
||||
// FIXME: actually mark the end index as SELECTED_END, currently we select the first index
|
||||
// TODO: simplify the update function. Use pathsFromSelection ?
|
||||
|
||||
return setIn(root, [childsKey], childsBefore.concat(childsUpdated, childsAfter))
|
||||
if (root._meta.type === 'Object') {
|
||||
const startIndex = root._meta.keys.indexOf(start)
|
||||
const endIndex = root._meta.keys.indexOf(end)
|
||||
|
||||
const minIndex = Math.min(startIndex, endIndex)
|
||||
const maxIndex = Math.max(startIndex, endIndex) + 1 // include max index itself
|
||||
|
||||
const selectedProps = root._meta.keys.slice(minIndex, maxIndex)
|
||||
selectedPaths = selectedProps.map(prop => rootPath.concat(prop))
|
||||
let updatedRoot = Object.assign({}, root)
|
||||
selectedProps.forEach(prop => {
|
||||
updatedRoot[prop] = setIn(updatedRoot[prop], ['_meta', 'selected'], prop === end ? SELECTED_END : SELECTED)
|
||||
})
|
||||
|
||||
return updatedRoot
|
||||
}
|
||||
else { // root._meta.type === 'Array'
|
||||
const startIndex = parseInt(start)
|
||||
const endIndex = parseInt(end)
|
||||
|
||||
const minIndex = Math.min(startIndex, endIndex)
|
||||
const maxIndex = Math.max(startIndex, endIndex) + 1 // include max index itself
|
||||
|
||||
const selectedIndices = range(minIndex, maxIndex)
|
||||
selectedPaths = selectedIndices.map(index => rootPath.concat(index))
|
||||
|
||||
let updatedRoot = Object.assign({}, root)
|
||||
selectedIndices.forEach(index => {
|
||||
updatedRoot[index] = setIn(updatedRoot[index], ['_meta', 'selected'], index === endIndex ? SELECTED_END : SELECTED)
|
||||
})
|
||||
|
||||
return updatedRoot
|
||||
}
|
||||
})
|
||||
|
||||
return cleanupMetaData(updatedEson, 'selected', selectedPaths)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -598,10 +621,10 @@ export function findRootPath(selection) {
|
|||
else if (selection.after) {
|
||||
return initial(selection.after)
|
||||
}
|
||||
else { // .start and .end
|
||||
else { // selection.start and selection.end
|
||||
const sharedPath = findSharedPath(selection.start, selection.end)
|
||||
|
||||
if (sharedPath.length === selection.start.length &&
|
||||
if (sharedPath.length === selection.start.length ||
|
||||
sharedPath.length === selection.end.length) {
|
||||
// there is just one node selected, return it's parent
|
||||
return initial(sharedPath)
|
||||
|
|
|
@ -134,6 +134,6 @@ export type ESONPatchResult = {
|
|||
}
|
||||
|
||||
export type JSONSchemaError = {
|
||||
path: string, // TODO: change type to JSONPath
|
||||
dataPath: string, // TODO: change type to JSONPath
|
||||
message: string
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
esonToJson, toEsonPath, toJsonPath, pathExists, transform, traverse,
|
||||
parseJSONPointer, compileJSONPointer,
|
||||
jsonToEson,
|
||||
expand, expandOne, expandPath, updateErrors, search, nextSearchResult, previousSearchResult,
|
||||
expand, expandOne, expandPath, applyErrors, search, nextSearchResult, previousSearchResult,
|
||||
applySelection, pathsFromSelection,
|
||||
SELECTED, SELECTED_END
|
||||
} from '../src/eson'
|
||||
|
@ -222,7 +222,7 @@ test('add and remove errors', t => {
|
|||
{dataPath: '/nill', message: 'Null expected'}
|
||||
]
|
||||
|
||||
const actual1 = updateErrors(eson, jsonSchemaErrors)
|
||||
const actual1 = applyErrors(eson, jsonSchemaErrors)
|
||||
|
||||
let expected = eson
|
||||
expected = setIn(expected, ['obj', 'arr', '2', 'last', '_meta', 'error'], jsonSchemaErrors[0])
|
||||
|
@ -230,11 +230,11 @@ test('add and remove errors', t => {
|
|||
t.deepEqual(actual1, expected)
|
||||
|
||||
// re-applying the same errors should not change eson
|
||||
const actual2 = updateErrors(actual1, jsonSchemaErrors)
|
||||
const actual2 = applyErrors(actual1, jsonSchemaErrors)
|
||||
t.is(actual2, actual1)
|
||||
|
||||
// clear errors
|
||||
const actual3 = updateErrors(actual2, [])
|
||||
const actual3 = applyErrors(actual2, [])
|
||||
t.deepEqual(actual3, eson)
|
||||
t.is(actual3.str, eson.str) // shouldn't have touched values not affected by the errors
|
||||
})
|
||||
|
@ -384,56 +384,97 @@ test('previousSearchResult', t => {
|
|||
})
|
||||
|
||||
test('selection (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']
|
||||
}
|
||||
|
||||
const actual = applySelection(ESON1, selection)
|
||||
|
||||
let expected = ESON1
|
||||
expected = setIn(expected, toEsonPath(ESON1, ['obj']).concat(['selected']), SELECTED_END)
|
||||
expected = setIn(expected, toEsonPath(ESON1, ['str']).concat(['selected']), SELECTED)
|
||||
expected = setIn(expected, toEsonPath(ESON1, ['nill']).concat(['selected']), SELECTED)
|
||||
const actual = applySelection(eson, selection)
|
||||
|
||||
let expected = eson
|
||||
expected = setIn(expected, ['obj', '_meta', 'selected'], SELECTED)
|
||||
expected = setIn(expected, ['str', '_meta', 'selected'], SELECTED)
|
||||
expected = setIn(expected, ['nill', '_meta', 'selected'], SELECTED_END)
|
||||
t.deepEqual(actual, expected)
|
||||
|
||||
// test whether old selection results are cleaned up
|
||||
const selection2 = {
|
||||
start: ['nill'],
|
||||
end: ['bool']
|
||||
}
|
||||
const actual2 = applySelection(actual, selection2)
|
||||
let expected2 = eson
|
||||
expected2 = setIn(expected2, ['nill', '_meta', 'selected'], SELECTED)
|
||||
expected2 = setIn(expected2, ['bool', '_meta', 'selected'], SELECTED_END)
|
||||
t.deepEqual(actual2, expected2)
|
||||
})
|
||||
|
||||
test('selection (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
|
||||
}
|
||||
|
||||
const actual = applySelection(ESON1, selection)
|
||||
const actual = applySelection(eson, selection)
|
||||
|
||||
// FIXME: SELECTE_END should be selection.start, not the first
|
||||
let expected = ESON1
|
||||
expected = setIn(expected, toEsonPath(ESON1, ['obj', 'arr', '0']).concat(['selected']), SELECTED_END)
|
||||
expected = setIn(expected, toEsonPath(ESON1, ['obj', 'arr', '1']).concat(['selected']), SELECTED)
|
||||
let expected = eson
|
||||
expected = setIn(expected, ['obj', 'arr', '0', '_meta', 'selected'], SELECTED_END)
|
||||
expected = setIn(expected, ['obj', 'arr', '1', '_meta', 'selected'], SELECTED)
|
||||
|
||||
t.deepEqual(actual, expected)
|
||||
})
|
||||
|
||||
test('selection (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']
|
||||
}
|
||||
|
||||
const actual = applySelection(ESON1, selection)
|
||||
const expected = setIn(ESON1, toEsonPath(ESON1, ['obj', 'arr', '2', 'first']).concat(['selected']), SELECTED_END)
|
||||
const actual = applySelection(eson, selection)
|
||||
const expected = setIn(eson, ['obj', 'arr', '2', 'first', '_meta', 'selected'], SELECTED_END)
|
||||
t.deepEqual(actual, expected)
|
||||
})
|
||||
|
||||
test('selection (node)', t => {
|
||||
const eson = jsonToEson({
|
||||
"obj": {
|
||||
"arr": [1,2, {"first":3,"last":4}]
|
||||
},
|
||||
"str": "hello world",
|
||||
"nill": null,
|
||||
"bool": false
|
||||
})
|
||||
const selection = {
|
||||
start: ['obj', 'arr'],
|
||||
end: ['obj', 'arr']
|
||||
}
|
||||
|
||||
const actual = applySelection(ESON1, selection)
|
||||
const expected = setIn(ESON1, toEsonPath(ESON1, ['obj', 'arr']).concat(['selected']), SELECTED_END)
|
||||
const actual = applySelection(eson, selection)
|
||||
const expected = setIn(eson, ['obj', 'arr', '_meta', 'selected'], SELECTED_END)
|
||||
t.deepEqual(actual, expected)
|
||||
})
|
||||
|
||||
|
|
Loading…
Reference in New Issue