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;
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;

View File

@ -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 @@
<Icon data={faRedo} />
</button>
<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 class="contents">
<Node
value={json}
searchResult={searchResult}
searchResult={searchResultWithActive}
expanded={true}
onChangeKey={handleChangeKey}
onPatch={handlePatch}
getParentPath={getPath}
/>
<div class='bottom'></div>
{#if showSearch}
<div class="search">
<SearchBox
text={searchText}
onChange={text => searchText = text}
onClose={() => showSearch = false}
/>
</div>
{/if}
</div>
</div>

View File

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

View File

@ -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 === ''
})
}

View File

@ -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;
}
}
}

View File

@ -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 @@
/>
</label>
<div class="search-count" class:visible={text !== ''}>
{activeResultIndex}/{resultCount}
{activeIndex + 1}/{resultCount}
</div>
<button class="search-icon search-next">
<button class="search-next" on:click={onNext} type="button">
<Icon data={faChevronDown} />
</button>
<button class="search-icon search-previous">
<button class="search-previous" on:click={onPrevious} type="button">
<Icon data={faChevronUp} />
</button>
<button class="search-icon search-clear" on:click={handleClose}>
<button class="search-clear" on:click={onClose} type="button">
<Icon data={faTimes} />
</button>
</form>

View File

@ -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