UNPKG

jsoneditor

Version:

A web-based tool to view, edit, format, and validate JSON

326 lines (290 loc) 8.78 kB
'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() } }