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

View File

@ -6,7 +6,7 @@ import { parseJSON } from '../utils/jsonUtils'
import { enrichSchemaError } from '../utils/schemaUtils' import { enrichSchemaError } from '../utils/schemaUtils'
import { import {
jsonToData, dataToJson, toDataPath, patchData, pathExists, jsonToData, dataToJson, toDataPath, patchData, pathExists,
expand, addErrors, search expand, addErrors, search, addSearchResults
} from '../jsonData' } from '../jsonData'
import { import {
duplicate, insert, append, remove, duplicate, insert, append, remove,
@ -66,16 +66,19 @@ export default class TreeMode extends Component {
? JSONNodeForm ? JSONNodeForm
: JSONNode : JSONNode
// enrich the data with JSON Schema errors and search results // enrich the data with JSON Schema errors
let data = state.data let data = state.data
const errors = this.getErrors() const errors = this.getErrors()
if (errors.length) { if (errors.length) {
data = addErrors(data, this.getErrors()) data = addErrors(data, this.getErrors())
} }
if (this.state.search.text) {
data = search(data, this.state.search.text) // enrich the data with search results
console.log('data', data) 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', { return h('div', {
class: `jsoneditor jsoneditor-mode-${props.mode}`, 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: show number of search results left from the input box
// TODO: prev/next // TODO: prev/next
// TODO: focus on search results // 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'}, return h('div', {class: 'jsoneditor-search'},
h('input', {type: 'text', value: state.text, onInput: this.handleChange}) 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 {Array} patch A JSON patch
* @param {function(path: Path)} [expand] Optional function to determine * @param {function(path: Path)} [expand] Optional function to determine
* what nodes must be expanded * 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) { export function patchData (data, patch, expand = expandAll) {
let updatedData = data let updatedData = data
@ -486,7 +486,7 @@ export function expand (data, callback, expanded) {
* into the data * into the data
* *
* @param {JSONData} data * @param {JSONData} data
* @param {Array.<JSONSchemaError>} errors * @param {JSONSchemaError[]} errors
*/ */
export function addErrors (data, errors) { export function addErrors (data, errors) {
let updatedData = data let updatedData = data
@ -507,40 +507,63 @@ export function addErrors (data, errors) {
* *
* @param {JSONData} data * @param {JSONData} data
* @param {string} text * @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) { export function search (data, text) {
return transform(data, function (value) { let results = []
traverse(data, function (value, path) {
// search in values // search in values
if (value.type === 'value') { if (value.type === 'value') {
if (containsCaseInsensitive(value.value, text)) { if (containsCaseInsensitive(value.value, text)) {
return setIn(value, ['search'], true) results.push({
} dataPath: path,
else { value: true
return deleteIn(value, ['search']) })
} }
} }
// search object property names // search object property names
if (value.type === 'Object') { if (value.type === 'Object') {
let updatedProps = value.props value.props.forEach((prop) => {
updatedProps.forEach((prop, index) => {
if (containsCaseInsensitive(prop.name, text)) { if (containsCaseInsensitive(prop.name, text)) {
updatedProps = setIn(updatedProps, [index, 'search'], true) results.push({
dataPath: path.concat(prop.name),
property: true
})
} }
else { })
updatedProps = deleteIn(updatedProps, [index, 'search'])
} }
}) })
return setIn(value, ['props'], updatedProps) return results
} }
return value /**
* 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
}
/** /**
* Do a case insensitive search for a search text in a text * Do a case insensitive search for a search text in a text
* @param {String} text * @param {String} text
@ -552,7 +575,7 @@ export function containsCaseInsensitive (text, search) {
} }
/** /**
* * Recursively transform JSONData: a recursive "map" function
* @param {JSONData} data * @param {JSONData} data
* @param {function(value: JSONData, path: Path, root: JSONData)} callback * @param {function(value: JSONData, path: Path, root: JSONData)} callback
* @return {JSONData} Returns the transformed data * @return {JSONData} Returns the transformed data
@ -600,12 +623,51 @@ function recurseTransform (value, path, root, callback) {
} }
default: // type 'string' or 'value' 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 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 * Test whether a path exists in the json data
* @param {JSONData} data * @param {JSONData} data

View File

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