diff --git a/src/components/JSONNode.js b/src/components/JSONNode.js index 903845e..239ae43 100644 --- a/src/components/JSONNode.js +++ b/src/components/JSONNode.js @@ -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' : '') } /** diff --git a/src/components/TreeMode.js b/src/components/TreeMode.js index 69e1530..c048574 100644 --- a/src/components/TreeMode.js +++ b/src/components/TreeMode.js @@ -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 diff --git a/src/components/menu/Search.js b/src/components/menu/Search.js new file mode 100644 index 0000000..d4fda95 --- /dev/null +++ b/src/components/menu/Search.js @@ -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 +} \ No newline at end of file diff --git a/src/components/menu/Search.less b/src/components/menu/Search.less new file mode 100644 index 0000000..66bd4a2 --- /dev/null +++ b/src/components/menu/Search.less @@ -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; + } +} \ No newline at end of file diff --git a/src/develop.html b/src/develop.html index 74104cc..cd343f6 100644 --- a/src/develop.html +++ b/src/develop.html @@ -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 = { diff --git a/src/jsonData.js b/src/jsonData.js index 3e9d517..46d2920 100644 --- a/src/jsonData.js +++ b/src/jsonData.js @@ -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 diff --git a/src/jsoneditor.less b/src/jsoneditor.less index ef1b64a..1e50330 100644 --- a/src/jsoneditor.less +++ b/src/jsoneditor.less @@ -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; }