First basic implementation of search (WIP)

This commit is contained in:
jos 2016-11-27 15:41:10 +01:00
parent e5e61b71e3
commit 939ad792d6
7 changed files with 140 additions and 23 deletions

View File

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

View File

@ -1,12 +1,12 @@
import { h, Component } from 'preact'
import Ajv from 'ajv'
import { updateIn, getIn } from '../utils/immutabilityHelpers'
import { updateIn, getIn, setIn } from '../utils/immutabilityHelpers'
import { parseJSON } from '../utils/jsonUtils'
import { enrichSchemaError } from '../utils/schemaUtils'
import {
jsonToData, dataToJson, toDataPath, patchData, pathExists,
expand, addErrors
expand, addErrors, search
} from '../jsonData'
import {
duplicate, insert, append, remove,
@ -16,6 +16,7 @@ import JSONNode from './JSONNode'
import JSONNodeView from './JSONNodeView'
import JSONNodeForm from './JSONNodeForm'
import ModeButton from './menu/ModeButton'
import Search from './menu/Search'
const AJV_OPTIONS = {
allErrors: true,
@ -24,6 +25,7 @@ const AJV_OPTIONS = {
}
const MAX_HISTORY_ITEMS = 1000 // maximum number of undo/redo items to be kept in memory
const SEARCH_DEBOUNCE = 300 // milliseconds
export default class TreeMode extends Component {
constructor (props) {
@ -50,7 +52,10 @@ export default class TreeMode extends Component {
onExpand: this.handleExpand
},
search: null
search: {
text: '',
selectedPath: null
}
}
}
@ -61,7 +66,16 @@ export default class TreeMode extends Component {
? JSONNodeForm
: JSONNode
const data = addErrors(state.data, this.getErrors())
// enrich the data with JSON Schema errors and search results
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)
}
return h('div', {
class: `jsoneditor jsoneditor-mode-${props.mode}`,
@ -131,6 +145,19 @@ export default class TreeMode extends Component {
])
}
if (this.props.options.search !== false) {
// option search is true or undefined
items = items.concat([
h('div', {class: 'jsoneditor-menu-panel-right'},
h(Search, {
text: this.state.search.text,
onChange: this.handleSearch,
delay: SEARCH_DEBOUNCE
})
)
])
}
return h('div', {class: 'jsoneditor-menu'}, items)
}
@ -232,6 +259,11 @@ export default class TreeMode extends Component {
})
}
/** @private */
handleSearch = (text) => {
this.setState(setIn(this.state, ['search', 'text'], text))
}
/**
* Apply a JSONPatch to the current JSON document and emit a change event
* @param {JSONPatch} actions

View File

@ -0,0 +1,47 @@
import { h, Component } from 'preact'
import '!style!css!less!./Search.less'
export default class Search extends Component {
constructor (props) {
super (props)
this.state = {
text: props.text || ''
}
}
render (props, state) {
// 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
return h('div', {class: 'jsoneditor-search'},
h('input', {type: 'text', value: state.text, onInput: this.handleChange})
)
}
componentWillReceiveProps (nextProps) {
if (nextProps.text !== this.props.text) {
// clear a pending onChange callback (if any
clearTimeout(this.timeout)
}
}
handleChange = (event) => {
const text = event.target.value
this.setState ({ text })
const delay = this.props.delay || 0
clearTimeout(this.timeout)
this.timeout = setTimeout(this.callbackOnChange, delay)
}
callbackOnChange = () => {
this.props.onChange(this.state.text)
}
timeout = null
}

View File

@ -0,0 +1,15 @@
@theme-color: #3883fa;
div.jsoneditor-search {
background: white;
border: 2px solid @theme-color;
box-sizing: border-box;
input {
border: none;
outline: none;
height: 22px;
line-height: 22px;
padding: 2px;
}
}

View File

@ -64,7 +64,8 @@
modes: ['text', 'code', 'tree', 'form', 'view'],
indentation: 4,
escapeUnicode: true,
history: true
history: true,
search: true
}
const editor = jsoneditor(container, options)
const json = {

View File

@ -509,6 +509,7 @@ export function addErrors (data, errors) {
* @param {string} text
* @return {JSONData} Returns an updated `data` object containing the 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

View File

@ -4,9 +4,10 @@
@fontSize: 10pt;
@black: #1A1A1A;
@contentsMinHeight: 150px;
@theme-color: #3883fa;
.jsoneditor {
border: 1px solid #3883fa;
border: 1px solid @theme-color;
width: 100%;
height: 100%;
@ -21,8 +22,7 @@
box-sizing: border-box;
color: white;
background-color: #3883fa;
border-bottom: 1px solid #3883fa;
background-color: @theme-color;
flex: 0 0 auto;
button {
@ -171,7 +171,7 @@ ul.jsoneditor-list {
.jsoneditor-property:hover,
.jsoneditor-value:hover {
background-color: #f5f5f5;
background-color: rgba(0, 0, 0, 0.05);
}
.jsoneditor-mode-form {
@ -256,6 +256,14 @@ div.jsoneditor-value.jsoneditor-empty::after {
content: 'value';
}
.jsoneditor-highlight {
background-color: yellow;
}
.jsoneditor-highlight-primary {
background-color: gold;
}
.jsoneditor-button-placeholder {
width: 20px;
padding: 0;
@ -313,6 +321,8 @@ button.jsoneditor-button.jsoneditor-actionmenu.jsoneditor-visible {
/******************************* Action Menu **********************************/
// TODO: move into a separate file like menu/Menu.less
div.jsoneditor-actionmenu {
position: absolute;
box-sizing: border-box;
@ -418,6 +428,10 @@ div.jsoneditor-menu-separator {
margin-top: 5px;
}
div.jsoneditor-menu-panel-right {
float: right;
}
button.jsoneditor-remove span.jsoneditor-icon {
background-position: -24px -24px;
}