Implement SearchBox highlight active result (WIP)
This commit is contained in:
parent
9c8febb1ff
commit
d2a84b29fe
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -147,4 +147,8 @@ div.empty {
|
|||
.key.search,
|
||||
.value.search {
|
||||
background-color: $highlight-color;
|
||||
|
||||
&.active {
|
||||
background-color: $highlight-active-color;
|
||||
}
|
||||
}
|
|
@ -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 === ''
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
const pendingChanges = text !== inputText
|
||||
if (pendingChanges) {
|
||||
onChangeDebounced.cancel()
|
||||
onChange(text)
|
||||
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>
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue