Selection working again

This commit is contained in:
jos 2017-12-14 16:40:55 +01:00
parent 378c8ef250
commit f491a00575
4 changed files with 151 additions and 86 deletions

View File

@ -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
}
/**

View File

@ -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 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)
})
}
const esonWithErrors = errors.reduce((eson, error) => {
const path = parseJSONPointer(error.dataPath)
// TODO: do we want to be able to store multiple errors per item?
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)

View File

@ -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
}

View File

@ -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)
})