First basic implementation of search (WIP)
This commit is contained in:
parent
e5e61b71e3
commit
939ad792d6
|
@ -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' : '')
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue