diff --git a/src/JSONEditor.scss b/src/JSONEditor.scss index 23a53b4..c28de79 100644 --- a/src/JSONEditor.scss +++ b/src/JSONEditor.scss @@ -15,6 +15,7 @@ color: $white; display: flex; align-items: center; + position: relative; .button { width: 32px; @@ -47,34 +48,18 @@ margin: 5px; } - .search-box { - display: flex; - align-items: center; - - .search-icon { - margin-right: $menu-padding; - } - - .search-input { - border: none; - font-family: $font-family-menu; - font-size: $font-size; - padding: $input-padding; - margin-right: 2px; - } + .search-box-container { + position: absolute; + top: 100%; + right: $search-box-offset + 20px; // keep space for scrollbar + margin-top: $search-box-offset; + z-index: 1; } } .contents { flex: 1; overflow: auto; - position: relative; - - .search { - position: absolute; - top: $search-box-offset; - right: $search-box-offset; - } .bottom { height: $input-padding; diff --git a/src/JSONEditor.svelte b/src/JSONEditor.svelte index 2bd5272..2d871ce 100644 --- a/src/JSONEditor.svelte +++ b/src/JSONEditor.svelte @@ -4,9 +4,11 @@ import { faSearch, faUndo, faRedo } from '@fortawesome/free-solid-svg-icons' import { createHistory } from './history.js' import Node from './JSONNode.svelte' + import { existsIn, setIn } from './utils/immutabilityHelpers.js' import { keyComboFromEvent } from './utils/keyBindings.js' - import { search } from './utils/search.js' + import { flattenSearch, search } from './utils/search.js' import { immutableJSONPatch } from './utils/immutableJSONPatch' + import { isEqual } from 'lodash' export let json = {} export let onChangeJson = () => { @@ -62,7 +64,35 @@ return result } + // TODO: refactor the search solution, it's too complex. Also, move it in a separate component + let searchResult + let activeSearchResult = undefined + let activeSearchResultIndex + let flatSearchResult + let searchResultWithActive $: searchResult = searchText ? doSearch(json, searchText) : undefined + $: flatSearchResult = flattenSearch(searchResult) + + $: { + if (!activeSearchResult || !existsIn(searchResult, activeSearchResult.path.concat(activeSearchResult.what))) { + activeSearchResult = flatSearchResult[0] + } + } + + $: activeSearchResultIndex = flatSearchResult.findIndex(item => isEqual(item, activeSearchResult)) + $: searchResultWithActive = searchResult + ? activeSearchResult + ? setIn(searchResult, activeSearchResult.path.concat(activeSearchResult.what), 'search active') + : searchResult + : undefined + + function nextSearchResult () { + activeSearchResult = flatSearchResult[activeSearchResultIndex + 1] || activeSearchResult + } + + function previousSearchResult () { + activeSearchResult = flatSearchResult[activeSearchResultIndex - 1] || activeSearchResult + } function handleChangeKey(key, oldKey) { // console.log('handleChangeKey', { key, oldKey }) @@ -178,26 +208,33 @@
+ {#if showSearch} +
+ searchText = text} + onNext={nextSearchResult} + onPrevious={previousSearchResult} + onClose={() => { + showSearch = false + searchText = '' + }} + /> +
+ {/if}
- {#if showSearch} - - {/if}
diff --git a/src/JSONNode.scss b/src/JSONNode.scss index 72be6c0..68df794 100644 --- a/src/JSONNode.scss +++ b/src/JSONNode.scss @@ -147,4 +147,8 @@ div.empty { .key.search, .value.search { background-color: $highlight-color; + + &.active { + background-color: $highlight-active-color; + } } \ No newline at end of file diff --git a/src/JSONNode.svelte b/src/JSONNode.svelte index bb51491..3dda4a5 100644 --- a/src/JSONNode.svelte +++ b/src/JSONNode.svelte @@ -80,21 +80,15 @@ function getValueClass (value, searchResult) { const type = valueType (value) - return classnames('value', type, { + return classnames('value', type, searchResult && searchResult[SEARCH_VALUE], { url: isUrl(value), empty: typeof value === 'string' && value.length === 0, - search: searchResult - ? !!searchResult[SEARCH_VALUE] - : false }) } function getKeyClass(key, searchResult) { - return classnames('key', { - empty: key === '', - search: searchResult - ? !!searchResult[SEARCH_PROPERTY] - : false + return classnames('key', searchResult && searchResult[SEARCH_PROPERTY], { + empty: key === '' }) } diff --git a/src/SearchBox.scss b/src/SearchBox.scss index da512e8..44b6b85 100644 --- a/src/SearchBox.scss +++ b/src/SearchBox.scss @@ -6,13 +6,14 @@ $search-size: 24px; border: 2px solid $theme-color; border-radius: $border-radius; background: $white; + box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.24); .search-form { display: flex; align-items: center; padding: 0 5px; - .search-icon { + button { color: $light-gray; display: block; width: $search-size; @@ -24,13 +25,18 @@ $search-size: 24px; margin: 0; } - button.search-icon { + button { width: 20px; cursor: pointer; &:hover, &:active { color: $gray; + + &.search-icon { + color: $light-gray; + cursor: inherit; + } } } diff --git a/src/SearchBox.svelte b/src/SearchBox.svelte index 55b272f..77e9688 100644 --- a/src/SearchBox.svelte +++ b/src/SearchBox.svelte @@ -7,44 +7,47 @@ const DEBOUNCE_DELAY = 300 // milliseconds TODO: make the debounce delay configurable? export let text = '' + let inputText = '' + export let resultCount = 0 + export let activeIndex = 0 export let onChange = () => {} + export let onPrevious = () => {} + export let onNext = () => {} export let onClose = () => {} - let activeResultIndex = 0 - let resultCount = 0 - $: onChangeDebounced = debounce(onChange, DEBOUNCE_DELAY) function handleSubmit (event) { event.preventDefault() - onChangeDebounced.cancel() - onChange(text) + const pendingChanges = text !== inputText + if (pendingChanges) { + onChangeDebounced.cancel() + onChange(inputText) + } else { + onNext() + } } function handleInput (event) { - text = event.target.value + inputText = event.target.value - onChangeDebounced(text) + onChangeDebounced(inputText) // TODO: fire debounced onChange } - function handleClose () { - onChange('') - onClose() - } - function handleKeyDown (event) { const combo = keyComboFromEvent(event) + if (combo === 'Ctrl+Enter' || combo === 'Command+Enter') { event.preventDefault() - // this.props.onFocusActive() // FIXME + // TODO: move focus to the active element } if (combo === 'Escape') { event.preventDefault() - handleClose() + onClose() } } @@ -68,15 +71,15 @@ />
- {activeResultIndex}/{resultCount} + {activeIndex + 1}/{resultCount}
- - - diff --git a/src/utils/search.js b/src/utils/search.js index e64c876..f480828 100644 --- a/src/utils/search.js +++ b/src/utils/search.js @@ -7,7 +7,7 @@ export function search (key, value, searchText) { let results = undefined if (typeof key === 'string' && containsCaseInsensitive(key, searchText)) { - results = createOrAdd(results, SEARCH_PROPERTY, true) + results = createOrAdd(results, SEARCH_PROPERTY, 'search') } const type = valueType(value) @@ -27,13 +27,49 @@ export function search (key, value, searchText) { }) } else { // type is a value if (containsCaseInsensitive(value, searchText)) { - results = createOrAdd(results, SEARCH_VALUE, true) + results = createOrAdd(results, SEARCH_VALUE, 'search') } } return results } +export function flattenSearch (searchResult) { + const resultArray = [] + + function _flattenSearch (value, path) { + if (value) { + if (value[SEARCH_PROPERTY]) { + resultArray.push({ + what: SEARCH_PROPERTY, + path + }) + } + if (value[SEARCH_VALUE]) { + resultArray.push({ + what: SEARCH_VALUE, + path + }) + } + } + + const type = valueType(value) + if (type === 'array') { + searchResult.forEach((item, index) => { + _flattenSearch(item, path.concat(index)) + }) + } else if (type === 'object') { + Object.keys(value).forEach(prop => { + _flattenSearch(value[prop], path.concat(prop)) + }) + } + } + + _flattenSearch(searchResult, []) + + return resultArray +} + function createOrAdd(object, key, value) { if (object) { object[key] = value