jsoneditor
Version:
A web-based tool to view, edit, format, and validate JSON
326 lines (290 loc) • 8.78 kB
JavaScript
'use strict'
import { translate } from './i18n'
/**
* @constructor SearchBox
* Create a search box in given HTML container
* @param {JSONEditor} editor The JSON Editor to attach to
* @param {Element} container HTML container element of where to
* create the search box
*/
export class SearchBox {
constructor (editor, container) {
const searchBox = this
this.editor = editor
this.timeout = undefined
this.delay = 200 // ms
this.lastText = undefined
this.results = null
this.dom = {}
this.dom.container = container
const wrapper = document.createElement('div')
this.dom.wrapper = wrapper
wrapper.className = 'jsoneditor-search'
container.appendChild(wrapper)
const results = document.createElement('div')
this.dom.results = results
results.className = 'jsoneditor-results'
wrapper.appendChild(results)
const divInput = document.createElement('div')
this.dom.input = divInput
divInput.className = 'jsoneditor-frame'
divInput.title = translate('searchTitle')
wrapper.appendChild(divInput)
const refreshSearch = document.createElement('button')
refreshSearch.type = 'button'
refreshSearch.className = 'jsoneditor-refresh'
divInput.appendChild(refreshSearch)
const search = document.createElement('input')
search.type = 'text'
this.dom.search = search
search.oninput = event => {
searchBox._onDelayedSearch(event)
}
search.onchange = event => {
// For IE 9
searchBox._onSearch()
}
search.onkeydown = event => {
searchBox._onKeyDown(event)
}
search.onkeyup = event => {
searchBox._onKeyUp(event)
}
refreshSearch.onclick = event => {
search.select()
}
// TODO: ESC in FF restores the last input, is a FF bug, https://bugzilla.mozilla.org/show_bug.cgi?id=598819
divInput.appendChild(search)
const searchNext = document.createElement('button')
searchNext.type = 'button'
searchNext.title = translate('searchNextResultTitle')
searchNext.className = 'jsoneditor-next'
searchNext.onclick = () => {
searchBox.next()
}
divInput.appendChild(searchNext)
const searchPrevious = document.createElement('button')
searchPrevious.type = 'button'
searchPrevious.title = translate('searchPreviousResultTitle')
searchPrevious.className = 'jsoneditor-previous'
searchPrevious.onclick = () => {
searchBox.previous()
}
divInput.appendChild(searchPrevious)
}
/**
* Go to the next search result
* @param {boolean} [focus] If true, focus will be set to the next result
* focus is false by default.
*/
next (focus) {
if (this.results) {
let index = this.resultIndex !== null ? this.resultIndex + 1 : 0
if (index > this.results.length - 1) {
index = 0
}
this._setActiveResult(index, focus)
}
}
/**
* Go to the prevous search result
* @param {boolean} [focus] If true, focus will be set to the next result
* focus is false by default.
*/
previous (focus) {
if (this.results) {
const max = this.results.length - 1
let index = this.resultIndex !== null ? this.resultIndex - 1 : max
if (index < 0) {
index = max
}
this._setActiveResult(index, focus)
}
}
/**
* Set new value for the current active result
* @param {Number} index
* @param {boolean} [focus] If true, focus will be set to the next result.
* focus is false by default.
* @private
*/
_setActiveResult (index, focus) {
// de-activate current active result
if (this.activeResult) {
const prevNode = this.activeResult.node
const prevElem = this.activeResult.elem
if (prevElem === 'field') {
delete prevNode.searchFieldActive
} else {
delete prevNode.searchValueActive
}
prevNode.updateDom()
}
if (!this.results || !this.results[index]) {
// out of range, set to undefined
this.resultIndex = undefined
this.activeResult = undefined
return
}
this.resultIndex = index
// set new node active
const node = this.results[this.resultIndex].node
const elem = this.results[this.resultIndex].elem
if (elem === 'field') {
node.searchFieldActive = true
} else {
node.searchValueActive = true
}
this.activeResult = this.results[this.resultIndex]
node.updateDom()
// TODO: not so nice that the focus is only set after the animation is finished
node.scrollTo(() => {
if (focus) {
node.focus(elem)
}
})
}
/**
* Cancel any running onDelayedSearch.
* @private
*/
_clearDelay () {
if (this.timeout !== undefined) {
clearTimeout(this.timeout)
delete this.timeout
}
}
/**
* Start a timer to execute a search after a short delay.
* Used for reducing the number of searches while typing.
* @param {Event} event
* @private
*/
_onDelayedSearch (event) {
// execute the search after a short delay (reduces the number of
// search actions while typing in the search text box)
this._clearDelay()
const searchBox = this
this.timeout = setTimeout(event => {
searchBox._onSearch()
}, this.delay)
}
/**
* Handle onSearch event
* @param {boolean} [forceSearch] If true, search will be executed again even
* when the search text is not changed.
* Default is false.
* @private
*/
_onSearch (forceSearch) {
this._clearDelay()
const value = this.dom.search.value
const text = value.length > 0 ? value : undefined
if (text !== this.lastText || forceSearch) {
// only search again when changed
this.lastText = text
this.results = this.editor.search(text)
const MAX_SEARCH_RESULTS = this.results[0]
? this.results[0].node.MAX_SEARCH_RESULTS
: Infinity
// try to maintain the current active result if this is still part of the new search results
let activeResultIndex = 0
if (this.activeResult) {
for (let i = 0; i < this.results.length; i++) {
if (this.results[i].node === this.activeResult.node) {
activeResultIndex = i
break
}
}
}
this._setActiveResult(activeResultIndex, false)
// display search results
if (text !== undefined) {
const resultCount = this.results.length
if (resultCount === 0) {
this.dom.results.textContent = 'no\u00A0results'
} else if (resultCount === 1) {
this.dom.results.textContent = '1\u00A0result'
} else if (resultCount > MAX_SEARCH_RESULTS) {
this.dom.results.textContent = MAX_SEARCH_RESULTS + '+\u00A0results'
} else {
this.dom.results.textContent = resultCount + '\u00A0results'
}
} else {
this.dom.results.textContent = ''
}
}
}
/**
* Handle onKeyDown event in the input box
* @param {Event} event
* @private
*/
_onKeyDown (event) {
const keynum = event.which
if (keynum === 27) {
// ESC
this.dom.search.value = '' // clear search
this._onSearch()
event.preventDefault()
event.stopPropagation()
} else if (keynum === 13) {
// Enter
if (event.ctrlKey) {
// force to search again
this._onSearch(true)
} else if (event.shiftKey) {
// move to the previous search result
this.previous()
} else {
// move to the next search result
this.next()
}
event.preventDefault()
event.stopPropagation()
}
}
/**
* Handle onKeyUp event in the input box
* @param {Event} event
* @private
*/
_onKeyUp (event) {
const keynum = event.keyCode
if (keynum !== 27 && keynum !== 13) {
// !show and !Enter
this._onDelayedSearch(event) // For IE 9
}
}
/**
* Clear the search results
*/
clear () {
this.dom.search.value = ''
this._onSearch()
}
/**
* Refresh searchResults if there is a search value
*/
forceSearch () {
this._onSearch(true)
}
/**
* Test whether the search box value is empty
* @returns {boolean} Returns true when empty.
*/
isEmpty () {
return this.dom.search.value === ''
}
/**
* Destroy the search box
*/
destroy () {
this.editor = null
this.dom.container.removeChild(this.dom.wrapper)
this.dom = null
this.results = null
this.activeResult = null
this._clearDelay()
}
}