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.expanded) {
if (data.props.length > 0) { if (data.props.length > 0) {
const props = data.props.map(prop => { 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, { h(this.constructor, {
path: this.props.path.concat(prop.name), path: this.props.path.concat(prop.name),
prop, prop,
@ -98,7 +98,7 @@ export default class JSONNode extends Component {
if (data.expanded) { if (data.expanded) {
if (data.items.length > 0) { if (data.items.length > 0) {
const items = data.items.map((item, index) => { 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, { h(this.constructor, {
path: this.props.path.concat(String(index)), path: this.props.path.concat(String(index)),
index, index,

View File

@ -11,7 +11,8 @@ import { enrichSchemaError } from '../utils/schemaUtils'
import { import {
jsonToEson, esonToJson, toEsonPath, pathExists, jsonToEson, esonToJson, toEsonPath, pathExists,
expand, expandPath, addErrors, expand, expandPath, addErrors,
search, addSearchResults, nextSearchResult, previousSearchResult, search, applySearchResults, nextSearchResult, previousSearchResult,
applySelection,
compileJSONPointer compileJSONPointer
} from '../eson' } from '../eson'
import { patchEson } from '../patchEson' import { patchEson } from '../patchEson'
@ -93,6 +94,11 @@ export default class TreeMode extends Component {
search: { search: {
text: '', text: '',
active: null // active search result 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 // 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 const searchResults = this.state.search.text ? search(data, this.state.search.text) : null
if (searchResults) { 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', { return h('div', {
@ -178,7 +187,7 @@ export default class TreeMode extends Component {
className: 'jsoneditor-contents jsoneditor-tree-contents', className: 'jsoneditor-contents jsoneditor-tree-contents',
id: this.id id: this.id
}, },
h('ul', {className: 'jsoneditor-list jsoneditor-root'}, h('ul', {className: 'jsoneditor-list jsoneditor-root' + (data.selected ? ' jsoneditor-selected' : '')},
h(Node, { h(Node, {
data, data,
events: state.events, events: state.events,

View File

@ -5,13 +5,13 @@
* All functions are pure and don't mutate the ESON. * 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 { isObject } from './utils/typeUtils'
import { last, allButLast } from './utils/arrayUtils' import { last, allButLast } from './utils/arrayUtils'
import isEqual from 'lodash/isEqual' import isEqual from 'lodash/isEqual'
import type { import type {
ESON, ESONObject, ESONArrayItem, ESONPointer, ESONType, ESONPath, ESON, ESONObject, ESONArrayItem, ESONPointer, ESONSelection, ESONType, ESONPath,
Path, Path,
JSONPath, JSONType JSONPath, JSONType
} from './types' } from './types'
@ -222,7 +222,7 @@ export function search (eson: ESON, text: string): ESONPointer[] {
const parentPath = allButLast(path) const parentPath = allButLast(path)
const parent = getIn(eson, toEsonPath(eson, parentPath)) const parent = getIn(eson, toEsonPath(eson, parentPath))
if (parent.type === 'Object') { 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 // check value
if (value.type === 'value') { if (value.type === 'value') {
if (containsCaseInsensitive(value.value, text)) { 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 * 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 let updatedEson = eson
searchResults.forEach(function (searchResult) { searchResults.forEach(function (searchResult) {
if (searchResult.type === 'value') { if (searchResult.field === 'value') {
const esonPath = toEsonPath(updatedEson, searchResult.path).concat('searchResult') const esonPath = toEsonPath(updatedEson, searchResult.path).concat('searchResult')
const value = isEqual(searchResult, activeSearchResult) ? 'active' : 'normal' const value = isEqual(searchResult, activeSearchResult) ? 'active' : 'normal'
updatedEson = setIn(updatedEson, esonPath, value) updatedEson = setIn(updatedEson, esonPath, value)
} }
if (searchResult.type === 'property') { if (searchResult.field === 'property') {
const esonPath = toEsonPath(updatedEson, searchResult.path) const esonPath = toEsonPath(updatedEson, searchResult.path)
const propertyPath = allButLast(esonPath).concat('searchResult') const propertyPath = allButLast(esonPath).concat('searchResult')
const value = isEqual(searchResult, activeSearchResult) ? 'active' : 'normal' 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 * Merge searchResults into the eson object
* @param {String} text
* @param {String} search
* @return {boolean} Returns true if `search` is found in `text`
*/ */
export function containsCaseInsensitive (text: string, search: string): boolean { export function applySelection (eson: ESON, selection: ESONSelection) {
return String(text).toLowerCase().indexOf(search.toLowerCase()) !== -1 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 // 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. * Get a new "unique" id. Id's are created from an incremental counter.
* @return {number} * @return {number}

View File

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

View File

@ -33,7 +33,7 @@ export type JSONArrayType = JSONType[]
/********************** TYPES FOR THE ESON OBJECT MODEL *************************/ /********************** TYPES FOR THE ESON OBJECT MODEL *************************/
export type SearchResultStatus = 'normal' | 'active' export type SearchResultStatus = 'normal' | 'active'
export type ESONPointerType = 'value' | 'property' export type ESONPointerField = 'value' | 'property'
export type ESONObjectProperty = { export type ESONObjectProperty = {
id: number, id: number,
@ -50,18 +50,21 @@ export type ESONArrayItem = {
export type ESONObject = { export type ESONObject = {
type: 'Object', type: 'Object',
expanded?: boolean, expanded?: boolean,
selected?: boolean,
props: ESONObjectProperty[] props: ESONObjectProperty[]
} }
export type ESONArray = { export type ESONArray = {
type: 'Array', type: 'Array',
expanded?: boolean, expanded?: boolean,
selected?: boolean,
items: ESONArrayItem[] items: ESONArrayItem[]
} }
export type ESONValue = { export type ESONValue = {
type: 'value' | 'string', type: 'value' | 'string',
value?: any, value?: any,
selected?: boolean,
searchResult?: SearchResultStatus searchResult?: SearchResultStatus
} }
@ -74,8 +77,13 @@ export type JSONPath = string[]
export type ESONPath = string[] export type ESONPath = string[]
export type ESONPointer = { export type ESONPointer = {
path: ESONPath, path: JSONPath, // TODO: change path to an ESONPath?
type: ESONPointerType field?: ESONPointerField
}
export type ESONSelection = {
start: ESONPointer,
end: ESONPointer
} }
// TODO: ESONPointer.path is an array, JSONSchemaError.path is a string -> make this consistent // 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 { import {
jsonToEson, esonToJson, pathExists, transform, traverse, jsonToEson, esonToJson, pathExists, transform, traverse,
parseJSONPointer, compileJSONPointer, parseJSONPointer, compileJSONPointer,
expand, addErrors, search, addSearchResults, nextSearchResult, previousSearchResult expand, addErrors, search, applySearchResults, nextSearchResult, previousSearchResult,
applySelection
} from '../src/eson' } from '../src/eson'
// TODO: move all JSON documents in separate json files to keep the test readable?
const JSON_EXAMPLE = { const JSON_EXAMPLE = {
obj: { obj: {
arr: [1,2, {first:3,last:4}] arr: [1,2, {first:3,last:4}]
@ -15,7 +18,7 @@ const JSON_EXAMPLE = {
bool: false bool: false
} }
const JSON_DATA_EXAMPLE = { const ESON = {
type: 'Object', type: 'Object',
expanded: true, expanded: true,
props: [ props: [
@ -105,34 +108,9 @@ const JSON_DATA_EXAMPLE = {
] ]
} }
const JSON_DUPLICATE_PROPERTY_EXAMPLE = { // TODO: instead of all slightly different copies of ESON, built them up via setIn, updateIn based on ESON
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 JSON_DATA_EXAMPLE, built them up via setIn, updateIn based on JSON_DATA_EXAMPLE const ESON_COLLAPSED_1 = {
const JSON_DATA_EXAMPLE_COLLAPSED_1 = {
type: 'Object', type: 'Object',
expanded: true, expanded: true,
props: [ props: [
@ -223,7 +201,7 @@ const JSON_DATA_EXAMPLE_COLLAPSED_1 = {
] ]
} }
const JSON_DATA_EXAMPLE_COLLAPSED_2 = { const ESON_COLLAPSED_2 = {
type: 'Object', type: 'Object',
expanded: true, expanded: true,
props: [ props: [
@ -314,7 +292,7 @@ const JSON_DATA_EXAMPLE_COLLAPSED_2 = {
} }
// after search for 'L' (case insensitive) // after search for 'L' (case insensitive)
const JSON_DATA_EXAMPLE_SEARCH_L = { const ESON_SEARCH_L = {
type: 'Object', type: 'Object',
expanded: true, expanded: true,
props: [ 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', type: 'Object',
props: [ props: [
{ {
@ -455,7 +800,7 @@ const JSON_SCHEMA_ERRORS = [
{dataPath: '/nill', message: 'Null expected'} {dataPath: '/nill', message: 'Null expected'}
] ]
const JSON_DATA_EXAMPLE_ERRORS = { const ESON_ERRORS = {
type: 'Object', type: 'Object',
expanded: true, expanded: true,
props: [ props: [
@ -555,17 +900,17 @@ test('jsonToEson', t => {
const ESON = jsonToEson(JSON_EXAMPLE, expand, []) const ESON = jsonToEson(JSON_EXAMPLE, expand, [])
replaceIds(ESON) replaceIds(ESON)
t.deepEqual(ESON, JSON_DATA_EXAMPLE) t.deepEqual(ESON, ESON)
}) })
test('esonToJson', t => { test('esonToJson', t => {
t.deepEqual(esonToJson(JSON_DATA_EXAMPLE), JSON_EXAMPLE) t.deepEqual(esonToJson(ESON), JSON_EXAMPLE)
}) })
test('expand a single path', t => { 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 => { test('expand a callback', t => {
@ -573,9 +918,9 @@ test('expand a callback', t => {
return path.length >= 1 return path.length >= 1
} }
const expanded = false 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 => { 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 return false
} }
const expanded = 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 => { test('pathExists', t => {
t.is(pathExists(JSON_DATA_EXAMPLE, ['obj', 'arr', 2, 'first']), true) t.is(pathExists(ESON, ['obj', 'arr', 2, 'first']), true)
t.is(pathExists(JSON_DATA_EXAMPLE, ['obj', 'foo']), false) t.is(pathExists(ESON, ['obj', 'foo']), false)
t.is(pathExists(JSON_DATA_EXAMPLE, ['obj', 'foo', 'bar']), false) t.is(pathExists(ESON, ['obj', 'foo', 'bar']), false)
t.is(pathExists(JSON_DATA_EXAMPLE, []), true) t.is(pathExists(ESON, []), true)
}) })
test('parseJSONPointer', t => { test('parseJSONPointer', t => {
@ -612,16 +957,16 @@ test('compileJSONPointer', t => {
}) })
test('add and remove errors', t => { test('add and remove errors', t => {
const dataWithErrors = addErrors(JSON_DATA_EXAMPLE, JSON_SCHEMA_ERRORS) const dataWithErrors = addErrors(ESON, JSON_SCHEMA_ERRORS)
t.deepEqual(dataWithErrors, JSON_DATA_EXAMPLE_ERRORS) t.deepEqual(dataWithErrors, ESON_ERRORS)
}) })
test('transform', t => { test('transform', t => {
// {obj: {a: 2}, arr: [3]} // {obj: {a: 2}, arr: [3]}
let log = [] let log = []
const transformed = transform(JSON_DATA_SMALL, function (value, path, root) { const transformed = transform(ESON_SMALL, function (value, path, root) {
t.is(root, JSON_DATA_SMALL) t.is(root, ESON_SMALL)
log.push([value, path, root]) log.push([value, path, root])
@ -637,22 +982,22 @@ test('transform', t => {
// console.log('transformed', JSON.stringify(transformed, null, 2)) // console.log('transformed', JSON.stringify(transformed, null, 2))
const EXPECTED_LOG = [ const EXPECTED_LOG = [
[JSON_DATA_SMALL, [], JSON_DATA_SMALL], [ESON_SMALL, [], ESON_SMALL],
[JSON_DATA_SMALL.props[0].value, ['obj'], JSON_DATA_SMALL], [ESON_SMALL.props[0].value, ['obj'], ESON_SMALL],
[JSON_DATA_SMALL.props[0].value.props[0].value, ['obj', 'a'], JSON_DATA_SMALL], [ESON_SMALL.props[0].value.props[0].value, ['obj', 'a'], ESON_SMALL],
[JSON_DATA_SMALL.props[1].value, ['arr'], JSON_DATA_SMALL], [ESON_SMALL.props[1].value, ['arr'], ESON_SMALL],
[JSON_DATA_SMALL.props[1].value.items[0].value, ['arr', '0'], JSON_DATA_SMALL], [ESON_SMALL.props[1].value.items[0].value, ['arr', '0'], ESON_SMALL],
] ]
log.forEach((row, index) => { log.forEach((row, index) => {
t.deepEqual(log[index], EXPECTED_LOG[index], 'should have equal log at index ' + index ) t.deepEqual(log[index], EXPECTED_LOG[index], 'should have equal log at index ' + index )
}) })
t.deepEqual(log, EXPECTED_LOG) t.deepEqual(log, EXPECTED_LOG)
t.not(transformed, JSON_DATA_SMALL) t.not(transformed, ESON_SMALL)
t.not(transformed.props[0].value, JSON_DATA_SMALL.props[0].value) t.not(transformed.props[0].value, ESON_SMALL.props[0].value)
t.not(transformed.props[0].value.props[0].value, JSON_DATA_SMALL.props[0].value.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, JSON_DATA_SMALL.props[1].value) t.is(transformed.props[1].value, ESON_SMALL.props[1].value)
t.is(transformed.props[1].value.items[0].value, JSON_DATA_SMALL.props[1].value.items[0].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]} // {obj: {a: 2}, arr: [3]}
let log = [] let log = []
const returnValue = traverse(JSON_DATA_SMALL, function (value, path, root) { const returnValue = traverse(ESON_SMALL, function (value, path, root) {
t.is(root, JSON_DATA_SMALL) t.is(root, ESON_SMALL)
log.push([value, path, root]) log.push([value, path, root])
}) })
@ -669,11 +1014,11 @@ test('traverse', t => {
t.is(returnValue, undefined) t.is(returnValue, undefined)
const EXPECTED_LOG = [ const EXPECTED_LOG = [
[JSON_DATA_SMALL, [], JSON_DATA_SMALL], [ESON_SMALL, [], ESON_SMALL],
[JSON_DATA_SMALL.props[0].value, ['obj'], JSON_DATA_SMALL], [ESON_SMALL.props[0].value, ['obj'], ESON_SMALL],
[JSON_DATA_SMALL.props[0].value.props[0].value, ['obj', 'a'], JSON_DATA_SMALL], [ESON_SMALL.props[0].value.props[0].value, ['obj', 'a'], ESON_SMALL],
[JSON_DATA_SMALL.props[1].value, ['arr'], JSON_DATA_SMALL], [ESON_SMALL.props[1].value, ['arr'], ESON_SMALL],
[JSON_DATA_SMALL.props[1].value.items[0].value, ['arr', '0'], JSON_DATA_SMALL], [ESON_SMALL.props[1].value.items[0].value, ['arr', '0'], ESON_SMALL],
] ]
log.forEach((row, index) => { log.forEach((row, index) => {
@ -684,51 +1029,51 @@ test('traverse', t => {
test('search', t => { test('search', t => {
const searchResults = search(JSON_DATA_EXAMPLE, 'L') const searchResults = search(ESON, 'L')
// printJSON(searchResults) // printJSON(searchResults)
t.deepEqual(searchResults, [ t.deepEqual(searchResults, [
{path: ['obj', 'arr', '2', 'last'], type: 'property'}, {path: ['obj', 'arr', '2', 'last'], field: 'property'},
{path: ['str'], type: 'value'}, {path: ['str'], field: 'value'},
{path: ['nill'], type: 'property'}, {path: ['nill'], field: 'property'},
{path: ['nill'], type: 'value'}, {path: ['nill'], field: 'value'},
{path: ['bool'], type: 'property'}, {path: ['bool'], field: 'property'},
{path: ['bool'], type: 'value'} {path: ['bool'], field: 'value'}
]) ])
const activeSearchResult = searchResults[0] const activeSearchResult = searchResults[0]
const updatedData = addSearchResults(JSON_DATA_EXAMPLE, searchResults, activeSearchResult) const updatedData = applySearchResults(ESON, searchResults, activeSearchResult)
// printJSON(updatedData) // printJSON(updatedData)
t.deepEqual(updatedData, JSON_DATA_EXAMPLE_SEARCH_L) t.deepEqual(updatedData, ESON_SEARCH_L)
}) })
test('nextSearchResult', t => { test('nextSearchResult', t => {
const searchResults = [ const searchResults = [
{path: ['obj', 'arr', '2', 'last'], type: 'property'}, {path: ['obj', 'arr', '2', 'last'], field: 'property'},
{path: ['str'], type: 'value'}, {path: ['str'], field: 'value'},
{path: ['nill'], type: 'property'}, {path: ['nill'], field: 'property'},
{path: ['nill'], type: 'value'}, {path: ['nill'], field: 'value'},
{path: ['bool'], type: 'property'}, {path: ['bool'], field: 'property'},
{path: ['bool'], type: 'value'} {path: ['bool'], field: 'value'}
] ]
t.deepEqual(nextSearchResult(searchResults, t.deepEqual(nextSearchResult(searchResults,
{path: ['nill'], type: 'property'}), {path: ['nill'], field: 'property'}),
{path: ['nill'], type: 'value'}) {path: ['nill'], field: 'value'})
// wrap around // wrap around
t.deepEqual(nextSearchResult(searchResults, t.deepEqual(nextSearchResult(searchResults,
{path: ['bool'], type: 'value'}), {path: ['bool'], field: 'value'}),
{path: ['obj', 'arr', '2', 'last'], type: 'property'}) {path: ['obj', 'arr', '2', 'last'], field: 'property'})
// return first when current is not found // return first when current is not found
t.deepEqual(nextSearchResult(searchResults, t.deepEqual(nextSearchResult(searchResults,
{path: ['non', 'existing'], type: 'value'}), {path: ['non', 'existing'], field: 'value'}),
{path: ['obj', 'arr', '2', 'last'], type: 'property'}) {path: ['obj', 'arr', '2', 'last'], field: 'property'})
// return null when searchResults are empty // 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 => { test('previousSearchResult', t => {
@ -759,6 +1104,49 @@ test('previousSearchResult', t => {
t.deepEqual(previousSearchResult([], {path: ['non', 'existing'], type: 'value'}), null) 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 // helper function to replace all id properties with a constant value
function replaceIds (data, value = '[ID]') { function replaceIds (data, value = '[ID]') {
if (data.type === 'Object') { if (data.type === 'Object') {