Implemented support for selection in eson

This commit is contained in:
jos 2017-09-22 11:38:23 +02:00
parent f3313158db
commit 943d721d84
6 changed files with 580 additions and 109 deletions

View File

@ -52,7 +52,7 @@ export default class JSONNode extends Component {
if (data.expanded) {
if (data.props.length > 0) {
const props = data.props.map(prop => {
return h('li', {key: prop.id},
return h('li', { key: prop.id, className: (prop.value.selected ? ' jsoneditor-selected' : '') },
h(this.constructor, {
path: this.props.path.concat(prop.name),
prop,
@ -98,7 +98,7 @@ export default class JSONNode extends Component {
if (data.expanded) {
if (data.items.length > 0) {
const items = data.items.map((item, index) => {
return h('li', {key : item.id},
return h('li', { key : item.id, className: (item.value.selected ? ' jsoneditor-selected' : '')},
h(this.constructor, {
path: this.props.path.concat(String(index)),
index,

View File

@ -11,7 +11,8 @@ import { enrichSchemaError } from '../utils/schemaUtils'
import {
jsonToEson, esonToJson, toEsonPath, pathExists,
expand, expandPath, addErrors,
search, addSearchResults, nextSearchResult, previousSearchResult,
search, applySearchResults, nextSearchResult, previousSearchResult,
applySelection,
compileJSONPointer
} from '../eson'
import { patchEson } from '../patchEson'
@ -93,6 +94,11 @@ export default class TreeMode extends Component {
search: {
text: '',
active: null // active search result
},
selection: {
start: null, // ESONPointer
end: null, // ESONPointer
}
}
}
@ -162,7 +168,10 @@ export default class TreeMode extends Component {
// TODO: performance improvements in search would be nice though it's acceptable right now
const searchResults = this.state.search.text ? search(data, this.state.search.text) : null
if (searchResults) {
data = addSearchResults(data, searchResults, this.state.search.active)
data = applySearchResults(data, searchResults, this.state.search.active)
}
if (this.state.selection) {
data = applySelection(data, this.state.selection)
}
return h('div', {
@ -178,7 +187,7 @@ export default class TreeMode extends Component {
className: 'jsoneditor-contents jsoneditor-tree-contents',
id: this.id
},
h('ul', {className: 'jsoneditor-list jsoneditor-root'},
h('ul', {className: 'jsoneditor-list jsoneditor-root' + (data.selected ? ' jsoneditor-selected' : '')},
h(Node, {
data,
events: state.events,

View File

@ -5,13 +5,13 @@
* All functions are pure and don't mutate the ESON.
*/
import { setIn, getIn } from './utils/immutabilityHelpers'
import { setIn, getIn, updateIn } from './utils/immutabilityHelpers'
import { isObject } from './utils/typeUtils'
import { last, allButLast } from './utils/arrayUtils'
import isEqual from 'lodash/isEqual'
import type {
ESON, ESONObject, ESONArrayItem, ESONPointer, ESONType, ESONPath,
ESON, ESONObject, ESONArrayItem, ESONPointer, ESONSelection, ESONType, ESONPath,
Path,
JSONPath, JSONType
} from './types'
@ -222,7 +222,7 @@ export function search (eson: ESON, text: string): ESONPointer[] {
const parentPath = allButLast(path)
const parent = getIn(eson, toEsonPath(eson, parentPath))
if (parent.type === 'Object') {
results.push({path, type: 'property'})
results.push({path, field: 'property'})
}
}
}
@ -230,7 +230,7 @@ export function search (eson: ESON, text: string): ESONPointer[] {
// check value
if (value.type === 'value') {
if (containsCaseInsensitive(value.value, text)) {
results.push({path, type: 'value'})
results.push({path, field: 'value'})
}
}
})
@ -290,17 +290,17 @@ export function previousSearchResult (searchResults: ESONPointer[], current: ESO
/**
* Merge searchResults into the eson object
*/
export function addSearchResults (eson: ESON, searchResults: ESONPointer[], activeSearchResult: ESONPointer) {
export function applySearchResults (eson: ESON, searchResults: ESONPointer[], activeSearchResult: ESONPointer) {
let updatedEson = eson
searchResults.forEach(function (searchResult) {
if (searchResult.type === 'value') {
if (searchResult.field === 'value') {
const esonPath = toEsonPath(updatedEson, searchResult.path).concat('searchResult')
const value = isEqual(searchResult, activeSearchResult) ? 'active' : 'normal'
updatedEson = setIn(updatedEson, esonPath, value)
}
if (searchResult.type === 'property') {
if (searchResult.field === 'property') {
const esonPath = toEsonPath(updatedEson, searchResult.path)
const propertyPath = allButLast(esonPath).concat('searchResult')
const value = isEqual(searchResult, activeSearchResult) ? 'active' : 'normal'
@ -312,13 +312,65 @@ export function addSearchResults (eson: ESON, searchResults: ESONPointer[], acti
}
/**
* Do a case insensitive search for a search text in a text
* @param {String} text
* @param {String} search
* @return {boolean} Returns true if `search` is found in `text`
* Merge searchResults into the eson object
*/
export function containsCaseInsensitive (text: string, search: string): boolean {
return String(text).toLowerCase().indexOf(search.toLowerCase()) !== -1
export function applySelection (eson: ESON, selection: ESONSelection) {
if (!selection || !selection.start || !selection.end) {
return eson
}
// find the parent node shared by both start and end of the selection
const rootPath = findSharedPath(selection.start.path, selection.end.path)
const rootEsonPath = toEsonPath(eson, rootPath)
if (rootPath.length === selection.start.path.length || rootPath.length === selection.end.path.length) {
// select the root itself
return setIn(eson, rootEsonPath.concat(['selected']), true)
}
else {
// select multiple childs of an object or array
return updateIn(eson, rootEsonPath, (root) => {
if (root.type === 'Object') {
const startIndex = findPropertyIndex(root, selection.start.path[rootPath.length])
const endIndex = findPropertyIndex(root, selection.end.path[rootPath.length])
const minIndex = Math.min(startIndex, endIndex)
const maxIndex = Math.max(startIndex, endIndex) + 1 // include max index itself
const propsBefore = root.props.slice(0, minIndex)
const propsUpdated = root.props.slice(minIndex, maxIndex)
.map((prop, index) => setIn(prop, ['value', 'selected'], true))
const propsAfter = root.props.slice(maxIndex)
return setIn(root, ['props'], propsBefore.concat(propsUpdated, propsAfter))
}
else if (root.type === 'Array') {
const startIndex = parseInt(selection.start.path[rootPath.length])
const endIndex = parseInt(selection.end.path[rootPath.length])
const minIndex = Math.min(startIndex, endIndex)
const maxIndex = Math.max(startIndex, endIndex) + 1 // include max index itself
const itemsBefore = root.items.slice(0, minIndex)
const itemsUpdated = root.items.slice(minIndex, maxIndex)
.map((item, index) => setIn(item, ['value', 'selected'], true))
const itemsAfter = root.items.slice(maxIndex)
return setIn(root, ['items'], itemsBefore.concat(itemsUpdated, itemsAfter))
}
})
}
}
/**
* Find the common path of two paths.
* For example findCommonRoot(['arr', '1', 'name'], ['arr', '1', 'address', 'contact']) returns ['arr', '1']
*/
function findSharedPath (path1: JSONPath, path2: JSONPath): JSONPath {
let i = 0;
while (i < path1.length && path1[i] === path2[i]) {
i++;
}
return path1.slice(0, i)
}
/**
@ -520,6 +572,16 @@ export function compileJSONPointer (path: Path) {
// TODO: move getId and createUniqueId to a separate file
/**
* Do a case insensitive search for a search text in a text
* @param {String} text
* @param {String} search
* @return {boolean} Returns true if `search` is found in `text`
*/
export function containsCaseInsensitive (text: string, search: string): boolean {
return String(text).toLowerCase().indexOf(search.toLowerCase()) !== -1
}
/**
* Get a new "unique" id. Id's are created from an incremental counter.
* @return {number}

View File

@ -256,6 +256,10 @@ div.jsoneditor-value.jsoneditor-empty::after {
content: 'value';
}
.jsoneditor-selected {
background-color: #f0f0f0;
}
.jsoneditor-highlight {
background-color: yellow;
}

View File

@ -33,7 +33,7 @@ export type JSONArrayType = JSONType[]
/********************** TYPES FOR THE ESON OBJECT MODEL *************************/
export type SearchResultStatus = 'normal' | 'active'
export type ESONPointerType = 'value' | 'property'
export type ESONPointerField = 'value' | 'property'
export type ESONObjectProperty = {
id: number,
@ -50,18 +50,21 @@ export type ESONArrayItem = {
export type ESONObject = {
type: 'Object',
expanded?: boolean,
selected?: boolean,
props: ESONObjectProperty[]
}
export type ESONArray = {
type: 'Array',
expanded?: boolean,
selected?: boolean,
items: ESONArrayItem[]
}
export type ESONValue = {
type: 'value' | 'string',
value?: any,
selected?: boolean,
searchResult?: SearchResultStatus
}
@ -74,8 +77,13 @@ export type JSONPath = string[]
export type ESONPath = string[]
export type ESONPointer = {
path: ESONPath,
type: ESONPointerType
path: JSONPath, // TODO: change path to an ESONPath?
field?: ESONPointerField
}
export type ESONSelection = {
start: ESONPointer,
end: ESONPointer
}
// TODO: ESONPointer.path is an array, JSONSchemaError.path is a string -> make this consistent

View File

@ -2,10 +2,13 @@ import test from 'ava';
import {
jsonToEson, esonToJson, pathExists, transform, traverse,
parseJSONPointer, compileJSONPointer,
expand, addErrors, search, addSearchResults, nextSearchResult, previousSearchResult
expand, addErrors, search, applySearchResults, nextSearchResult, previousSearchResult,
applySelection
} from '../src/eson'
// TODO: move all JSON documents in separate json files to keep the test readable?
const JSON_EXAMPLE = {
obj: {
arr: [1,2, {first:3,last:4}]
@ -15,7 +18,7 @@ const JSON_EXAMPLE = {
bool: false
}
const JSON_DATA_EXAMPLE = {
const ESON = {
type: 'Object',
expanded: true,
props: [
@ -105,34 +108,9 @@ const JSON_DATA_EXAMPLE = {
]
}
const JSON_DUPLICATE_PROPERTY_EXAMPLE = {
type: 'Object',
expanded: true,
props: [
{
id: '[ID]',
name: 'name',
value: {
type: 'value',
expanded: true,
value: 'Joe'
}
},
{
id: '[ID]',
name: 'name',
value: {
type: 'value',
expanded: true,
value: 'Joe'
}
}
]
}
// TODO: instead of all slightly different copies of ESON, built them up via setIn, updateIn based on ESON
// TODO: instead of all slightly different copies of JSON_DATA_EXAMPLE, built them up via setIn, updateIn based on JSON_DATA_EXAMPLE
const JSON_DATA_EXAMPLE_COLLAPSED_1 = {
const ESON_COLLAPSED_1 = {
type: 'Object',
expanded: true,
props: [
@ -223,7 +201,7 @@ const JSON_DATA_EXAMPLE_COLLAPSED_1 = {
]
}
const JSON_DATA_EXAMPLE_COLLAPSED_2 = {
const ESON_COLLAPSED_2 = {
type: 'Object',
expanded: true,
props: [
@ -314,7 +292,7 @@ const JSON_DATA_EXAMPLE_COLLAPSED_2 = {
}
// after search for 'L' (case insensitive)
const JSON_DATA_EXAMPLE_SEARCH_L = {
const ESON_SEARCH_L = {
type: 'Object',
expanded: true,
props: [
@ -410,7 +388,374 @@ const JSON_DATA_EXAMPLE_SEARCH_L = {
]
}
const JSON_DATA_SMALL = {
const ESON_SELECTED_OBJECT = {
type: 'Object',
expanded: true,
props: [
{
id: '[ID]',
name: 'obj',
value: {
type: 'Object',
expanded: true,
selected: true,
props: [
{
id: '[ID]',
name: 'arr',
value: {
type: 'Array',
expanded: true,
items: [
{
id: '[ID]',
value: {
type: 'value',
value: 1
}
},
{
id: '[ID]',
value: {
type: 'value',
value: 2
}
},
{
id: '[ID]',
value: {
type: 'Object',
expanded: true,
props: [
{
id: '[ID]',
name: 'first',
value: {
type: 'value',
value: 3
}
},
{
id: '[ID]',
name: 'last',
value: {
type: 'value',
value: 4
}
}
]
}
}
]
}
}
]
}
},
{
id: '[ID]',
name: 'str',
value: {
type: 'value',
value: 'hello world',
selected: true
}
},
{
id: '[ID]',
name: 'nill',
value: {
type: 'value',
value: null,
selected: true
}
},
{
id: '[ID]',
name: 'bool',
value: {
type: 'value',
value: false
}
}
]
}
const ESON_SELECTED_ARRAY = {
type: 'Object',
expanded: true,
props: [
{
id: '[ID]',
name: 'obj',
value: {
type: 'Object',
expanded: true,
props: [
{
id: '[ID]',
name: 'arr',
value: {
type: 'Array',
expanded: true,
items: [
{
id: '[ID]',
value: {
type: 'value',
selected: true,
value: 1
}
},
{
id: '[ID]',
value: {
type: 'value',
selected: true,
value: 2
}
},
{
id: '[ID]',
value: {
type: 'Object',
expanded: true,
props: [
{
id: '[ID]',
name: 'first',
value: {
type: 'value',
value: 3
}
},
{
id: '[ID]',
name: 'last',
value: {
type: 'value',
value: 4
}
}
]
}
}
]
}
}
]
}
},
{
id: '[ID]',
name: 'str',
value: {
type: 'value',
value: 'hello world'
}
},
{
id: '[ID]',
name: 'nill',
value: {
type: 'value',
value: null
}
},
{
id: '[ID]',
name: 'bool',
value: {
type: 'value',
value: false
}
}
]
}
const ESON_SELECTED_VALUE = {
type: 'Object',
expanded: true,
props: [
{
id: '[ID]',
name: 'obj',
value: {
type: 'Object',
expanded: true,
props: [
{
id: '[ID]',
name: 'arr',
value: {
type: 'Array',
expanded: true,
items: [
{
id: '[ID]',
value: {
type: 'value',
value: 1
}
},
{
id: '[ID]',
value: {
type: 'value',
value: 2
}
},
{
id: '[ID]',
value: {
type: 'Object',
expanded: true,
props: [
{
id: '[ID]',
name: 'first',
value: {
type: 'value',
selected: true,
value: 3
}
},
{
id: '[ID]',
name: 'last',
value: {
type: 'value',
value: 4
}
}
]
}
}
]
}
}
]
}
},
{
id: '[ID]',
name: 'str',
value: {
type: 'value',
value: 'hello world'
}
},
{
id: '[ID]',
name: 'nill',
value: {
type: 'value',
value: null
}
},
{
id: '[ID]',
name: 'bool',
value: {
type: 'value',
value: false
}
}
]
}
const ESON_SELECTED_PARENT = {
type: 'Object',
expanded: true,
props: [
{
id: '[ID]',
name: 'obj',
value: {
type: 'Object',
expanded: true,
props: [
{
id: '[ID]',
name: 'arr',
value: {
type: 'Array',
expanded: true,
items: [
{
id: '[ID]',
value: {
type: 'value',
value: 1
}
},
{
id: '[ID]',
value: {
type: 'value',
value: 2
}
},
{
id: '[ID]',
value: {
type: 'Object',
expanded: true,
selected: true,
props: [
{
id: '[ID]',
name: 'first',
value: {
type: 'value',
value: 3
}
},
{
id: '[ID]',
name: 'last',
value: {
type: 'value',
value: 4
}
}
]
}
}
]
}
}
]
}
},
{
id: '[ID]',
name: 'str',
value: {
type: 'value',
value: 'hello world'
}
},
{
id: '[ID]',
name: 'nill',
value: {
type: 'value',
value: null
}
},
{
id: '[ID]',
name: 'bool',
value: {
type: 'value',
value: false
}
}
]
}
const ESON_SMALL = {
type: 'Object',
props: [
{
@ -455,7 +800,7 @@ const JSON_SCHEMA_ERRORS = [
{dataPath: '/nill', message: 'Null expected'}
]
const JSON_DATA_EXAMPLE_ERRORS = {
const ESON_ERRORS = {
type: 'Object',
expanded: true,
props: [
@ -555,17 +900,17 @@ test('jsonToEson', t => {
const ESON = jsonToEson(JSON_EXAMPLE, expand, [])
replaceIds(ESON)
t.deepEqual(ESON, JSON_DATA_EXAMPLE)
t.deepEqual(ESON, ESON)
})
test('esonToJson', t => {
t.deepEqual(esonToJson(JSON_DATA_EXAMPLE), JSON_EXAMPLE)
t.deepEqual(esonToJson(ESON), JSON_EXAMPLE)
})
test('expand a single path', t => {
const collapsed = expand(JSON_DATA_EXAMPLE, ['obj', 'arr', 2], false)
const collapsed = expand(ESON, ['obj', 'arr', 2], false)
t.deepEqual(collapsed, JSON_DATA_EXAMPLE_COLLAPSED_1)
t.deepEqual(collapsed, ESON_COLLAPSED_1)
})
test('expand a callback', t => {
@ -573,9 +918,9 @@ test('expand a callback', t => {
return path.length >= 1
}
const expanded = false
const collapsed = expand(JSON_DATA_EXAMPLE, callback, expanded)
const collapsed = expand(ESON, callback, expanded)
t.deepEqual(collapsed, JSON_DATA_EXAMPLE_COLLAPSED_2)
t.deepEqual(collapsed, ESON_COLLAPSED_2)
})
test('expand a callback should not change the object when nothing happens', t => {
@ -583,16 +928,16 @@ test('expand a callback should not change the object when nothing happens', t =>
return false
}
const expanded = false
const collapsed = expand(JSON_DATA_EXAMPLE, callback, expanded)
const collapsed = expand(ESON, callback, expanded)
t.is(collapsed, JSON_DATA_EXAMPLE)
t.is(collapsed, ESON)
})
test('pathExists', t => {
t.is(pathExists(JSON_DATA_EXAMPLE, ['obj', 'arr', 2, 'first']), true)
t.is(pathExists(JSON_DATA_EXAMPLE, ['obj', 'foo']), false)
t.is(pathExists(JSON_DATA_EXAMPLE, ['obj', 'foo', 'bar']), false)
t.is(pathExists(JSON_DATA_EXAMPLE, []), true)
t.is(pathExists(ESON, ['obj', 'arr', 2, 'first']), true)
t.is(pathExists(ESON, ['obj', 'foo']), false)
t.is(pathExists(ESON, ['obj', 'foo', 'bar']), false)
t.is(pathExists(ESON, []), true)
})
test('parseJSONPointer', t => {
@ -612,16 +957,16 @@ test('compileJSONPointer', t => {
})
test('add and remove errors', t => {
const dataWithErrors = addErrors(JSON_DATA_EXAMPLE, JSON_SCHEMA_ERRORS)
t.deepEqual(dataWithErrors, JSON_DATA_EXAMPLE_ERRORS)
const dataWithErrors = addErrors(ESON, JSON_SCHEMA_ERRORS)
t.deepEqual(dataWithErrors, ESON_ERRORS)
})
test('transform', t => {
// {obj: {a: 2}, arr: [3]}
let log = []
const transformed = transform(JSON_DATA_SMALL, function (value, path, root) {
t.is(root, JSON_DATA_SMALL)
const transformed = transform(ESON_SMALL, function (value, path, root) {
t.is(root, ESON_SMALL)
log.push([value, path, root])
@ -637,22 +982,22 @@ test('transform', t => {
// console.log('transformed', JSON.stringify(transformed, null, 2))
const EXPECTED_LOG = [
[JSON_DATA_SMALL, [], JSON_DATA_SMALL],
[JSON_DATA_SMALL.props[0].value, ['obj'], JSON_DATA_SMALL],
[JSON_DATA_SMALL.props[0].value.props[0].value, ['obj', 'a'], JSON_DATA_SMALL],
[JSON_DATA_SMALL.props[1].value, ['arr'], JSON_DATA_SMALL],
[JSON_DATA_SMALL.props[1].value.items[0].value, ['arr', '0'], JSON_DATA_SMALL],
[ESON_SMALL, [], ESON_SMALL],
[ESON_SMALL.props[0].value, ['obj'], ESON_SMALL],
[ESON_SMALL.props[0].value.props[0].value, ['obj', 'a'], ESON_SMALL],
[ESON_SMALL.props[1].value, ['arr'], ESON_SMALL],
[ESON_SMALL.props[1].value.items[0].value, ['arr', '0'], ESON_SMALL],
]
log.forEach((row, index) => {
t.deepEqual(log[index], EXPECTED_LOG[index], 'should have equal log at index ' + index )
})
t.deepEqual(log, EXPECTED_LOG)
t.not(transformed, JSON_DATA_SMALL)
t.not(transformed.props[0].value, JSON_DATA_SMALL.props[0].value)
t.not(transformed.props[0].value.props[0].value, JSON_DATA_SMALL.props[0].value.props[0].value)
t.is(transformed.props[1].value, JSON_DATA_SMALL.props[1].value)
t.is(transformed.props[1].value.items[0].value, JSON_DATA_SMALL.props[1].value.items[0].value)
t.not(transformed, ESON_SMALL)
t.not(transformed.props[0].value, ESON_SMALL.props[0].value)
t.not(transformed.props[0].value.props[0].value, ESON_SMALL.props[0].value.props[0].value)
t.is(transformed.props[1].value, ESON_SMALL.props[1].value)
t.is(transformed.props[1].value.items[0].value, ESON_SMALL.props[1].value.items[0].value)
})
@ -660,8 +1005,8 @@ test('traverse', t => {
// {obj: {a: 2}, arr: [3]}
let log = []
const returnValue = traverse(JSON_DATA_SMALL, function (value, path, root) {
t.is(root, JSON_DATA_SMALL)
const returnValue = traverse(ESON_SMALL, function (value, path, root) {
t.is(root, ESON_SMALL)
log.push([value, path, root])
})
@ -669,11 +1014,11 @@ test('traverse', t => {
t.is(returnValue, undefined)
const EXPECTED_LOG = [
[JSON_DATA_SMALL, [], JSON_DATA_SMALL],
[JSON_DATA_SMALL.props[0].value, ['obj'], JSON_DATA_SMALL],
[JSON_DATA_SMALL.props[0].value.props[0].value, ['obj', 'a'], JSON_DATA_SMALL],
[JSON_DATA_SMALL.props[1].value, ['arr'], JSON_DATA_SMALL],
[JSON_DATA_SMALL.props[1].value.items[0].value, ['arr', '0'], JSON_DATA_SMALL],
[ESON_SMALL, [], ESON_SMALL],
[ESON_SMALL.props[0].value, ['obj'], ESON_SMALL],
[ESON_SMALL.props[0].value.props[0].value, ['obj', 'a'], ESON_SMALL],
[ESON_SMALL.props[1].value, ['arr'], ESON_SMALL],
[ESON_SMALL.props[1].value.items[0].value, ['arr', '0'], ESON_SMALL],
]
log.forEach((row, index) => {
@ -684,51 +1029,51 @@ test('traverse', t => {
test('search', t => {
const searchResults = search(JSON_DATA_EXAMPLE, 'L')
const searchResults = search(ESON, 'L')
// printJSON(searchResults)
t.deepEqual(searchResults, [
{path: ['obj', 'arr', '2', 'last'], type: 'property'},
{path: ['str'], type: 'value'},
{path: ['nill'], type: 'property'},
{path: ['nill'], type: 'value'},
{path: ['bool'], type: 'property'},
{path: ['bool'], type: 'value'}
{path: ['obj', 'arr', '2', 'last'], field: 'property'},
{path: ['str'], field: 'value'},
{path: ['nill'], field: 'property'},
{path: ['nill'], field: 'value'},
{path: ['bool'], field: 'property'},
{path: ['bool'], field: 'value'}
])
const activeSearchResult = searchResults[0]
const updatedData = addSearchResults(JSON_DATA_EXAMPLE, searchResults, activeSearchResult)
const updatedData = applySearchResults(ESON, searchResults, activeSearchResult)
// printJSON(updatedData)
t.deepEqual(updatedData, JSON_DATA_EXAMPLE_SEARCH_L)
t.deepEqual(updatedData, ESON_SEARCH_L)
})
test('nextSearchResult', t => {
const searchResults = [
{path: ['obj', 'arr', '2', 'last'], type: 'property'},
{path: ['str'], type: 'value'},
{path: ['nill'], type: 'property'},
{path: ['nill'], type: 'value'},
{path: ['bool'], type: 'property'},
{path: ['bool'], type: 'value'}
{path: ['obj', 'arr', '2', 'last'], field: 'property'},
{path: ['str'], field: 'value'},
{path: ['nill'], field: 'property'},
{path: ['nill'], field: 'value'},
{path: ['bool'], field: 'property'},
{path: ['bool'], field: 'value'}
]
t.deepEqual(nextSearchResult(searchResults,
{path: ['nill'], type: 'property'}),
{path: ['nill'], type: 'value'})
{path: ['nill'], field: 'property'}),
{path: ['nill'], field: 'value'})
// wrap around
t.deepEqual(nextSearchResult(searchResults,
{path: ['bool'], type: 'value'}),
{path: ['obj', 'arr', '2', 'last'], type: 'property'})
{path: ['bool'], field: 'value'}),
{path: ['obj', 'arr', '2', 'last'], field: 'property'})
// return first when current is not found
t.deepEqual(nextSearchResult(searchResults,
{path: ['non', 'existing'], type: 'value'}),
{path: ['obj', 'arr', '2', 'last'], type: 'property'})
{path: ['non', 'existing'], field: 'value'}),
{path: ['obj', 'arr', '2', 'last'], field: 'property'})
// return null when searchResults are empty
t.deepEqual(nextSearchResult([], {path: ['non', 'existing'], type: 'value'}), null)
t.deepEqual(nextSearchResult([], {path: ['non', 'existing'], field: 'value'}), null)
})
test('previousSearchResult', t => {
@ -759,6 +1104,49 @@ test('previousSearchResult', t => {
t.deepEqual(previousSearchResult([], {path: ['non', 'existing'], type: 'value'}), null)
})
test('selection (object)', t => {
const selection = {
start: {path: ['obj', 'arr', '2', 'last']},
end: {path: ['nill']}
}
const result = applySelection(ESON, selection)
t.deepEqual(result, ESON_SELECTED_OBJECT)
})
test('selection (array)', t => {
const selection = {
start: {path: ['obj', 'arr', '1']},
end: {path: ['obj', 'arr', '0']} // note the "wrong" order of start and end
}
const result = applySelection(ESON, selection)
t.deepEqual(result, ESON_SELECTED_ARRAY)
})
test('selection (value)', t => {
const selection = {
start: {path: ['obj', 'arr', '2', 'first']},
end: {path: ['obj', 'arr', '2', 'first']}
}
const result = applySelection(ESON, selection)
t.deepEqual(result, ESON_SELECTED_VALUE)
})
test('selection (single parent)', t => {
const selection = {
start: {path: ['obj', 'arr', '2']},
end: {path: ['obj', 'arr', '2']}
}
const result = applySelection(ESON, selection)
t.deepEqual(result, ESON_SELECTED_PARENT)
})
// helper function to replace all id properties with a constant value
function replaceIds (data, value = '[ID]') {
if (data.type === 'Object') {