Refactored `search` to return an array with results, implemented `addSearchResults`

This commit is contained in:
jos 2016-11-27 21:16:17 +01:00
parent 939ad792d6
commit 37f2f77124
6 changed files with 196 additions and 163 deletions

View File

@ -36,13 +36,13 @@ export default class JSONNode extends Component {
}
}
renderJSONObject ({prop, data, search, options, events}) {
renderJSONObject ({prop, data, options, events}) {
const childCount = data.props.length
const contents = [
h('div', {class: 'jsoneditor-node jsoneditor-object'}, [
this.renderExpandButton(),
this.renderActionMenuButton(),
this.renderProperty(prop, data, search, options),
this.renderProperty(prop, data, options),
this.renderReadonly(`{${childCount}}`, `Array containing ${childCount} items`),
this.renderError(data.error)
])
@ -56,7 +56,6 @@ export default class JSONNode extends Component {
parent: this,
prop: prop.name,
data: prop.value,
search: prop.search,
options,
events
})
@ -74,13 +73,13 @@ export default class JSONNode extends Component {
return h('li', {}, contents)
}
renderJSONArray ({prop, data, search, options, events}) {
renderJSONArray ({prop, data, options, events}) {
const childCount = data.items.length
const contents = [
h('div', {class: 'jsoneditor-node jsoneditor-array'}, [
this.renderExpandButton(),
this.renderActionMenuButton(),
this.renderProperty(prop, data, search, options),
this.renderProperty(prop, data, options),
this.renderReadonly(`[${childCount}]`, `Array containing ${childCount} items`),
this.renderError(data.error)
])
@ -110,14 +109,14 @@ export default class JSONNode extends Component {
return h('li', {}, contents)
}
renderJSONValue ({prop, data, search, options}) {
renderJSONValue ({prop, data, options}) {
return h('li', {}, [
h('div', {class: 'jsoneditor-node'}, [
this.renderPlaceholder(),
this.renderActionMenuButton(),
this.renderProperty(prop, data, search, options),
this.renderProperty(prop, data, options),
this.renderSeparator(),
this.renderValue(data.value, data.search, options),
this.renderValue(data.value, data.searchValue, options),
this.renderError(data.error)
])
])
@ -146,7 +145,7 @@ export default class JSONNode extends Component {
return h('div', {class: 'jsoneditor-readonly', title}, text)
}
renderProperty (prop, data, search, options) {
renderProperty (prop, data, options) {
if (prop === null) {
// root node
const rootName = JSONNode.getRootName(data, options)
@ -162,7 +161,7 @@ export default class JSONNode extends Component {
const editable = !isIndex && (!options.isPropertyEditable || options.isPropertyEditable(this.getPath()))
const emptyClassName = (prop.length === 0 ? ' jsoneditor-empty' : '')
const searchClassName = search ? ' jsoneditor-highlight': '';
const searchClassName = data.searchProperty ? ' jsoneditor-highlight': '';
if (editable) {
const escapedProp = escapeHTML(prop, options.escapeUnicode)
@ -186,7 +185,7 @@ export default class JSONNode extends Component {
return h('div', {class: 'jsoneditor-separator'}, ':')
}
renderValue (value, searchResult, options) {
renderValue (value, searchValue, options) {
const escapedValue = escapeHTML(value, options.escapeUnicode)
const type = valueType (value)
const itsAnUrl = isUrl(value)
@ -195,7 +194,7 @@ export default class JSONNode extends Component {
const editable = !options.isValueEditable || options.isValueEditable(this.getPath())
if (editable) {
return h('div', {
class: JSONNode.getValueClass(type, itsAnUrl, isEmpty, searchResult),
class: JSONNode.getValueClass(type, itsAnUrl, isEmpty, searchValue),
contentEditable: 'true',
spellCheck: 'false',
onBlur: this.handleChangeValue,
@ -286,17 +285,16 @@ export default class JSONNode extends Component {
* @param {string} type
* @param {boolean} isUrl
* @param {boolean} isEmpty
* @param {boolean | 'selected'} [searchResult]
* @param {boolean} [searchValue]
* @return {string}
* @public
*/
static getValueClass (type, isUrl, isEmpty, searchResult) {
static getValueClass (type, isUrl, isEmpty, searchValue) {
return 'jsoneditor-value ' +
'jsoneditor-' + type +
(isUrl ? ' jsoneditor-url' : '') +
(isEmpty ? ' jsoneditor-empty' : '') +
(searchResult === 'selected' ? ' jsoneditor-highlight-primary' :
searchResult ? ' jsoneditor-highlight' : '')
(searchValue ? ' jsoneditor-highlight' : '')
}
/**

View File

@ -6,7 +6,7 @@ import { parseJSON } from '../utils/jsonUtils'
import { enrichSchemaError } from '../utils/schemaUtils'
import {
jsonToData, dataToJson, toDataPath, patchData, pathExists,
expand, addErrors, search
expand, addErrors, search, addSearchResults
} from '../jsonData'
import {
duplicate, insert, append, remove,
@ -66,16 +66,19 @@ export default class TreeMode extends Component {
? JSONNodeForm
: JSONNode
// enrich the data with JSON Schema errors and search results
// enrich the data with JSON Schema errors
let data = state.data
const errors = this.getErrors()
if (errors.length) {
data = addErrors(data, this.getErrors())
}
if (this.state.search.text) {
data = search(data, this.state.search.text)
console.log('data', data)
// enrich the data with search results
const searchResults = this.state.search.text ? search(data, this.state.search.text) : null
if (searchResults) {
data = addSearchResults(data, searchResults)
}
// TODO: pass number of search results to search box in top menu
return h('div', {
class: `jsoneditor jsoneditor-mode-${props.mode}`,

View File

@ -15,7 +15,7 @@ export default class Search extends Component {
// TODO: show number of search results left from the input box
// TODO: prev/next
// TODO: focus on search results
// TODO: expand next search result if not expanded
// TODO: expand the focused search result if not expanded
return h('div', {class: 'jsoneditor-search'},
h('input', {type: 'text', value: state.text, onInput: this.handleChange})

View File

@ -117,7 +117,7 @@ export function toDataPath (data, path) {
* @param {Array} patch A JSON patch
* @param {function(path: Path)} [expand] Optional function to determine
* what nodes must be expanded
* @return {{data: JSONData, revert: Array.<Object>, error: Error | null}}
* @return {{data: JSONData, revert: Object[], error: Error | null}}
*/
export function patchData (data, patch, expand = expandAll) {
let updatedData = data
@ -486,7 +486,7 @@ export function expand (data, callback, expanded) {
* into the data
*
* @param {JSONData} data
* @param {Array.<JSONSchemaError>} errors
* @param {JSONSchemaError[]} errors
*/
export function addErrors (data, errors) {
let updatedData = data
@ -507,38 +507,61 @@ export function addErrors (data, errors) {
*
* @param {JSONData} data
* @param {string} text
* @return {JSONData} Returns an updated `data` object containing the search results
* @return {SearchResult[]} Returns a list with search results
*/
// TODO: change search to return an array with paths, create a separate method addSearch similar to addErrors
export function search (data, text) {
return transform(data, function (value) {
// search in values
if (value.type === 'value') {
if (containsCaseInsensitive(value.value, text)) {
return setIn(value, ['search'], true)
}
else {
return deleteIn(value, ['search'])
}
}
let results = []
// search object property names
if (value.type === 'Object') {
let updatedProps = value.props
updatedProps.forEach((prop, index) => {
if (containsCaseInsensitive(prop.name, text)) {
updatedProps = setIn(updatedProps, [index, 'search'], true)
traverse(data, function (value, path) {
// search in values
if (value.type === 'value') {
if (containsCaseInsensitive(value.value, text)) {
results.push({
dataPath: path,
value: true
})
}
else {
updatedProps = deleteIn(updatedProps, [index, 'search'])
}
})
}
return setIn(value, ['props'], updatedProps)
}
return value
// search object property names
if (value.type === 'Object') {
value.props.forEach((prop) => {
if (containsCaseInsensitive(prop.name, text)) {
results.push({
dataPath: path.concat(prop.name),
property: true
})
}
})
}
})
return results
}
/**
* Merge searchResults into the data
*
* @param {JSONData} data
* @param {SearchResult[]} searchResults
*/
export function addSearchResults (data, searchResults) {
let updatedData = data
if (searchResults) {
searchResults.forEach(function (searchResult) {
if (searchResult.value) {
const dataPath = toDataPath(data, searchResult.dataPath).concat('searchValue')
updatedData = setIn(updatedData, dataPath, true)
}
if (searchResult.property) {
const dataPath = toDataPath(data, searchResult.dataPath).concat('searchProperty')
updatedData = setIn(updatedData, dataPath, true)
}
})
}
return updatedData
}
/**
@ -552,7 +575,7 @@ export function containsCaseInsensitive (text, search) {
}
/**
*
* Recursively transform JSONData: a recursive "map" function
* @param {JSONData} data
* @param {function(value: JSONData, path: Path, root: JSONData)} callback
* @return {JSONData} Returns the transformed data
@ -600,12 +623,51 @@ function recurseTransform (value, path, root, callback) {
}
default: // type 'string' or 'value'
// don't do anything: a value can't be expanded, only arrays and objects can
// no childs to traverse
}
return updatedValue
}
/**
* Recursively loop over a JSONData object: a recursive "forEach" function.
* @param {JSONData} data
* @param {function(value: JSONData, path: Path, root: JSONData)} callback
*/
export function traverse (data, callback) {
return recurseTraverse (data, [], data, callback)
}
/**
* Recursively traverse a JSONData object
* @param {JSONData} value
* @param {Path} path
* @param {JSONData | null} root The root object, object at path=[]
* @param {function(value: JSONData, path: Path, root: JSONData)} callback
*/
function recurseTraverse (value, path, root, callback) {
callback(value, path, root)
switch (value.type) {
case 'Array': {
value.items.forEach((item, index) => {
recurseTraverse(item, 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 json data
* @param {JSONData} data

View File

@ -59,4 +59,11 @@
* @typedef {{
* expand: function (path: Path)?
* }} PatchOptions
*
* @typedef {{
* dataPath: Path,
* property: boolean?,
* value: boolean?
* }} SearchResult
* // TODO: SearchResult.dataPath is an array, JSONSchemaError.dataPath is a string -> make this consistent
*/

View File

@ -1,14 +1,14 @@
import test from 'ava';
import {
jsonToData, dataToJson, patchData, pathExists, transform, search,
jsonToData, dataToJson, patchData, pathExists, transform, traverse,
parseJSONPointer, compileJSONPointer,
expand, addErrors
expand, addErrors, search, addSearchResults
} from '../src/jsonData'
const JSON_EXAMPLE = {
obj: {
arr: [1,2, {a:3,b:4}]
arr: [1,2, {first:3,last:4}]
},
str: 'hello world',
nill: null,
@ -44,14 +44,14 @@ const JSON_DATA_EXAMPLE = {
expanded: true,
props: [
{
name: 'a',
name: 'first',
value: {
type: 'value',
value: 3
}
},
{
name: 'b',
name: 'last',
value: {
type: 'value',
value: 4
@ -118,14 +118,14 @@ const JSON_DATA_EXAMPLE_COLLAPSED_1 = {
expanded: false,
props: [
{
name: 'a',
name: 'first',
value: {
type: 'value',
value: 3
}
},
{
name: 'b',
name: 'last',
value: {
type: 'value',
value: 4
@ -192,14 +192,14 @@ const JSON_DATA_EXAMPLE_COLLAPSED_2 = {
expanded: false,
props: [
{
name: 'a',
name: 'first',
value: {
type: 'value',
value: 3
}
},
{
name: 'b',
name: 'last',
value: {
type: 'value',
value: 4
@ -237,14 +237,13 @@ const JSON_DATA_EXAMPLE_COLLAPSED_2 = {
]
}
// after search for 'O' (case insensitive)
const JSON_DATA_EXAMPLE_SEARCH_1 = {
// after search for 'L' (case insensitive)
const JSON_DATA_EXAMPLE_SEARCH_L = {
type: 'Object',
expanded: true,
props: [
{
name: 'obj',
search: true,
value: {
type: 'Object',
expanded: true,
@ -268,21 +267,22 @@ const JSON_DATA_EXAMPLE_SEARCH_1 = {
expanded: true,
props: [
{
name: 'a',
name: 'first',
value: {
type: 'value',
value: 3
}
},
{
name: 'b',
name: 'last',
value: {
type: 'value',
value: 4
value: 4,
searchProperty: true
}
}
]
},
}
]
}
}
@ -294,98 +294,25 @@ const JSON_DATA_EXAMPLE_SEARCH_1 = {
value: {
type: 'value',
value: 'hello world',
search: true
searchValue: true
}
},
{
name: 'nill',
value: {
type: 'value',
value: null
}
},
{
name: 'bool',
search: true,
value: {
type: 'value',
value: false
}
}
]
}
// after search for '2'
const JSON_DATA_EXAMPLE_SEARCH_2 = {
type: 'Object',
expanded: true,
props: [
{
name: 'obj',
value: {
type: 'Object',
expanded: true,
props: [
{
name: 'arr',
value: {
type: 'Array',
expanded: true,
items: [
{
type: 'value',
value: 1
},
{
type: 'value',
value: 2,
search: true
},
{
type: 'Object',
expanded: true,
props: [
{
name: 'a',
value: {
type: 'value',
value: 3
}
},
{
name: 'b',
value: {
type: 'value',
value: 4
}
}
]
},
]
}
}
]
}
},
{
name: 'str',
value: {
type: 'value',
value: 'hello world'
}
},
{
name: 'nill',
value: {
type: 'value',
value: null
value: null,
searchProperty: true,
searchValue: true
}
},
{
name: 'bool',
value: {
type: 'value',
value: false
value: false,
searchProperty: true,
searchValue: true
}
}
]
@ -426,7 +353,7 @@ const JSON_DATA_SMALL = {
const JSON_SCHEMA_ERRORS = [
{dataPath: '/obj/arr/2/b', message: 'String expected'},
{dataPath: '/obj/arr/2/last', message: 'String expected'},
{dataPath: '/nill', message: 'Null expected'}
]
@ -459,14 +386,14 @@ const JSON_DATA_EXAMPLE_ERRORS = {
expanded: true,
props: [
{
name: 'a',
name: 'first',
value: {
type: 'value',
value: 3
}
},
{
name: 'b',
name: 'last',
value: {
type: 'value',
value: 4,
@ -545,7 +472,7 @@ test('expand a callback should not change the object when nothing happens', t =>
})
test('pathExists', t => {
t.is(pathExists(JSON_DATA_EXAMPLE, ['obj', 'arr', 2, 'a']), true)
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)
@ -931,7 +858,7 @@ test('transform', t => {
let log = []
const transformed = transform(JSON_DATA_SMALL, function (value, path, root) {
t.truthy(root === JSON_DATA_SMALL)
t.is(root, JSON_DATA_SMALL)
log.push([value, path, root])
@ -958,20 +885,56 @@ test('transform', t => {
// t.deepEqual(log[index], EXPECTED_LOG[index], 'should have equal log at index ' + index )
// })
t.deepEqual(log, EXPECTED_LOG)
t.truthy(transformed !== JSON_DATA_SMALL)
t.truthy(transformed.props[0].value !== JSON_DATA_SMALL.props[0].value)
t.truthy(transformed.props[0].value.props[0].value !== JSON_DATA_SMALL.props[0].value.props[0].value)
t.truthy(JSON_DATA_SMALL.props[1].value === JSON_DATA_SMALL.props[1].value)
t.truthy(JSON_DATA_SMALL.props[1].value.items[0] === JSON_DATA_SMALL.props[1].value.items[0])
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], JSON_DATA_SMALL.props[1].value.items[0])
})
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)
log.push([value, path, root])
})
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], ['arr', '0'], JSON_DATA_SMALL],
]
// 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 result1 = search(JSON_DATA_EXAMPLE, 'O')
t.deepEqual(result1, JSON_DATA_EXAMPLE_SEARCH_1)
const searchResults = search(JSON_DATA_EXAMPLE, 'L')
// console.log(searchResults)
// search for something else. Should clean up earlier search results
const result2 = search(result1, '2')
t.deepEqual(result2, JSON_DATA_EXAMPLE_SEARCH_2)
t.deepEqual(searchResults, [
{dataPath: ['nill'], property: true},
{dataPath: ['bool'], property: true},
{dataPath: ['obj', 'arr', '2', 'last'], property: true},
{dataPath: ['str'], value: true},
{dataPath: ['nill'], value: true},
{dataPath: ['bool'], value: true}
])
const updatedData = addSearchResults(JSON_DATA_EXAMPLE, searchResults)
// console.log(JSON.stringify(updatedData, null, 2))
t.deepEqual(updatedData, JSON_DATA_EXAMPLE_SEARCH_L)
})