Implement SearchBox highlight active result (WIP)

This commit is contained in:
josdejong 2020-05-23 13:43:12 +02:00
parent 9c8febb1ff
commit d2a84b29fe
7 changed files with 129 additions and 64 deletions

View File

@ -15,6 +15,7 @@
color: $white; color: $white;
display: flex; display: flex;
align-items: center; align-items: center;
position: relative;
.button { .button {
width: 32px; width: 32px;
@ -47,34 +48,18 @@
margin: 5px; margin: 5px;
} }
.search-box { .search-box-container {
display: flex; position: absolute;
align-items: center; top: 100%;
right: $search-box-offset + 20px; // keep space for scrollbar
.search-icon { margin-top: $search-box-offset;
margin-right: $menu-padding; z-index: 1;
}
.search-input {
border: none;
font-family: $font-family-menu;
font-size: $font-size;
padding: $input-padding;
margin-right: 2px;
}
} }
} }
.contents { .contents {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
position: relative;
.search {
position: absolute;
top: $search-box-offset;
right: $search-box-offset;
}
.bottom { .bottom {
height: $input-padding; height: $input-padding;

View File

@ -4,9 +4,11 @@
import { faSearch, faUndo, faRedo } from '@fortawesome/free-solid-svg-icons' import { faSearch, faUndo, faRedo } from '@fortawesome/free-solid-svg-icons'
import { createHistory } from './history.js' import { createHistory } from './history.js'
import Node from './JSONNode.svelte' import Node from './JSONNode.svelte'
import { existsIn, setIn } from './utils/immutabilityHelpers.js'
import { keyComboFromEvent } from './utils/keyBindings.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 { immutableJSONPatch } from './utils/immutableJSONPatch'
import { isEqual } from 'lodash'
export let json = {} export let json = {}
export let onChangeJson = () => { export let onChangeJson = () => {
@ -62,7 +64,35 @@
return result 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 $: 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) { function handleChangeKey(key, oldKey) {
// console.log('handleChangeKey', { key, oldKey }) // console.log('handleChangeKey', { key, oldKey })
@ -178,26 +208,33 @@
<Icon data={faRedo} /> <Icon data={faRedo} />
</button> </button>
<div class="space"></div> <div class="space"></div>
{#if showSearch}
<div class="search-box-container">
<SearchBox
text={searchText}
resultCount={flatSearchResult.length}
activeIndex={activeSearchResultIndex}
onChange={(text) => searchText = text}
onNext={nextSearchResult}
onPrevious={previousSearchResult}
onClose={() => {
showSearch = false
searchText = ''
}}
/>
</div>
{/if}
</div> </div>
<div class="contents"> <div class="contents">
<Node <Node
value={json} value={json}
searchResult={searchResult} searchResult={searchResultWithActive}
expanded={true} expanded={true}
onChangeKey={handleChangeKey} onChangeKey={handleChangeKey}
onPatch={handlePatch} onPatch={handlePatch}
getParentPath={getPath} getParentPath={getPath}
/> />
<div class='bottom'></div> <div class='bottom'></div>
{#if showSearch}
<div class="search">
<SearchBox
text={searchText}
onChange={text => searchText = text}
onClose={() => showSearch = false}
/>
</div>
{/if}
</div> </div>
</div> </div>

View File

@ -147,4 +147,8 @@ div.empty {
.key.search, .key.search,
.value.search { .value.search {
background-color: $highlight-color; background-color: $highlight-color;
&.active {
background-color: $highlight-active-color;
}
} }

View File

@ -80,21 +80,15 @@
function getValueClass (value, searchResult) { function getValueClass (value, searchResult) {
const type = valueType (value) const type = valueType (value)
return classnames('value', type, { return classnames('value', type, searchResult && searchResult[SEARCH_VALUE], {
url: isUrl(value), url: isUrl(value),
empty: typeof value === 'string' && value.length === 0, empty: typeof value === 'string' && value.length === 0,
search: searchResult
? !!searchResult[SEARCH_VALUE]
: false
}) })
} }
function getKeyClass(key, searchResult) { function getKeyClass(key, searchResult) {
return classnames('key', { return classnames('key', searchResult && searchResult[SEARCH_PROPERTY], {
empty: key === '', empty: key === ''
search: searchResult
? !!searchResult[SEARCH_PROPERTY]
: false
}) })
} }

View File

@ -6,13 +6,14 @@ $search-size: 24px;
border: 2px solid $theme-color; border: 2px solid $theme-color;
border-radius: $border-radius; border-radius: $border-radius;
background: $white; background: $white;
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.24);
.search-form { .search-form {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 5px; padding: 0 5px;
.search-icon { button {
color: $light-gray; color: $light-gray;
display: block; display: block;
width: $search-size; width: $search-size;
@ -24,13 +25,18 @@ $search-size: 24px;
margin: 0; margin: 0;
} }
button.search-icon { button {
width: 20px; width: 20px;
cursor: pointer; cursor: pointer;
&:hover, &:hover,
&:active { &:active {
color: $gray; color: $gray;
&.search-icon {
color: $light-gray;
cursor: inherit;
}
} }
} }

View File

@ -7,44 +7,47 @@
const DEBOUNCE_DELAY = 300 // milliseconds TODO: make the debounce delay configurable? const DEBOUNCE_DELAY = 300 // milliseconds TODO: make the debounce delay configurable?
export let text = '' export let text = ''
let inputText = ''
export let resultCount = 0
export let activeIndex = 0
export let onChange = () => {} export let onChange = () => {}
export let onPrevious = () => {}
export let onNext = () => {}
export let onClose = () => {} export let onClose = () => {}
let activeResultIndex = 0
let resultCount = 0
$: onChangeDebounced = debounce(onChange, DEBOUNCE_DELAY) $: onChangeDebounced = debounce(onChange, DEBOUNCE_DELAY)
function handleSubmit (event) { function handleSubmit (event) {
event.preventDefault() event.preventDefault()
onChangeDebounced.cancel() const pendingChanges = text !== inputText
onChange(text) if (pendingChanges) {
onChangeDebounced.cancel()
onChange(inputText)
} else {
onNext()
}
} }
function handleInput (event) { function handleInput (event) {
text = event.target.value inputText = event.target.value
onChangeDebounced(text) onChangeDebounced(inputText)
// TODO: fire debounced onChange // TODO: fire debounced onChange
} }
function handleClose () {
onChange('')
onClose()
}
function handleKeyDown (event) { function handleKeyDown (event) {
const combo = keyComboFromEvent(event) const combo = keyComboFromEvent(event)
if (combo === 'Ctrl+Enter' || combo === 'Command+Enter') { if (combo === 'Ctrl+Enter' || combo === 'Command+Enter') {
event.preventDefault() event.preventDefault()
// this.props.onFocusActive() // FIXME // TODO: move focus to the active element
} }
if (combo === 'Escape') { if (combo === 'Escape') {
event.preventDefault() event.preventDefault()
handleClose() onClose()
} }
} }
@ -68,15 +71,15 @@
/> />
</label> </label>
<div class="search-count" class:visible={text !== ''}> <div class="search-count" class:visible={text !== ''}>
{activeResultIndex}/{resultCount} {activeIndex + 1}/{resultCount}
</div> </div>
<button class="search-icon search-next"> <button class="search-next" on:click={onNext} type="button">
<Icon data={faChevronDown} /> <Icon data={faChevronDown} />
</button> </button>
<button class="search-icon search-previous"> <button class="search-previous" on:click={onPrevious} type="button">
<Icon data={faChevronUp} /> <Icon data={faChevronUp} />
</button> </button>
<button class="search-icon search-clear" on:click={handleClose}> <button class="search-clear" on:click={onClose} type="button">
<Icon data={faTimes} /> <Icon data={faTimes} />
</button> </button>
</form> </form>

View File

@ -7,7 +7,7 @@ export function search (key, value, searchText) {
let results = undefined let results = undefined
if (typeof key === 'string' && containsCaseInsensitive(key, searchText)) { if (typeof key === 'string' && containsCaseInsensitive(key, searchText)) {
results = createOrAdd(results, SEARCH_PROPERTY, true) results = createOrAdd(results, SEARCH_PROPERTY, 'search')
} }
const type = valueType(value) const type = valueType(value)
@ -27,13 +27,49 @@ export function search (key, value, searchText) {
}) })
} else { // type is a value } else { // type is a value
if (containsCaseInsensitive(value, searchText)) { if (containsCaseInsensitive(value, searchText)) {
results = createOrAdd(results, SEARCH_VALUE, true) results = createOrAdd(results, SEARCH_VALUE, 'search')
} }
} }
return results 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) { function createOrAdd(object, key, value) {
if (object) { if (object) {
object[key] = value object[key] = value