Introduce concept of "Selected Function" and allow arrow key selection
parent
ac480a9fdc
commit
98dfdb239c
|
@ -1,42 +1,58 @@
|
|||
import React, {SFC, ChangeEvent, KeyboardEvent} from 'react'
|
||||
|
||||
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||
import DropdownInput from 'src/shared/components/DropdownInput'
|
||||
import FuncSelectorInput from 'src/shared/components/FuncSelectorInput'
|
||||
import FuncListItem from 'src/ifql/components/FuncListItem'
|
||||
|
||||
interface Props {
|
||||
inputText: string
|
||||
isOpen: boolean
|
||||
onInputChange: (e: ChangeEvent<HTMLInputElement>) => void
|
||||
onKeyDown: (e: KeyboardEvent<HTMLInputElement>) => void
|
||||
onAddNode: (name: string) => void
|
||||
funcs: string[]
|
||||
selectedFunc: string
|
||||
onSetSelectedFunc: (name: string) => void
|
||||
}
|
||||
|
||||
const FuncList: SFC<Props> = ({
|
||||
inputText,
|
||||
isOpen,
|
||||
onAddNode,
|
||||
onKeyDown,
|
||||
onInputChange,
|
||||
funcs,
|
||||
selectedFunc,
|
||||
onSetSelectedFunc,
|
||||
}) => {
|
||||
return (
|
||||
<ul className="dropdown-menu funcs">
|
||||
<DropdownInput
|
||||
buttonSize="btn-xs"
|
||||
buttonColor="btn-default"
|
||||
<div className="ifql-func--autocomplete">
|
||||
<FuncSelectorInput
|
||||
onFilterChange={onInputChange}
|
||||
onFilterKeyPress={onKeyDown}
|
||||
searchTerm={inputText}
|
||||
/>
|
||||
<FancyScrollbar autoHide={false} autoHeight={true} maxHeight={240}>
|
||||
{isOpen &&
|
||||
funcs.map((func, i) => (
|
||||
<FuncListItem key={i} name={func} onAddNode={onAddNode} />
|
||||
))}
|
||||
</FancyScrollbar>
|
||||
</ul>
|
||||
<ul className="ifql-func--list">
|
||||
<FancyScrollbar
|
||||
autoHide={false}
|
||||
autoHeight={true}
|
||||
maxHeight={240}
|
||||
className="fancy-scroll--func-selector"
|
||||
>
|
||||
{funcs.length > 0 ? (
|
||||
funcs.map((func, i) => (
|
||||
<FuncListItem
|
||||
key={i}
|
||||
name={func}
|
||||
onAddNode={onAddNode}
|
||||
selectedFunc={selectedFunc}
|
||||
onSetSelectedFunc={onSetSelectedFunc}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="ifql-func--item empty">No results</div>
|
||||
)}
|
||||
</FancyScrollbar>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@ import React, {PureComponent} from 'react'
|
|||
interface Props {
|
||||
name: string
|
||||
onAddNode: (name: string) => void
|
||||
selectedFunc: string
|
||||
onSetSelectedFunc: (name: string) => void
|
||||
}
|
||||
|
||||
export default class FuncListItem extends PureComponent<Props> {
|
||||
|
@ -10,12 +12,25 @@ export default class FuncListItem extends PureComponent<Props> {
|
|||
const {name} = this.props
|
||||
|
||||
return (
|
||||
<li onClick={this.handleClick} className="dropdown-item func">
|
||||
<a>{name}</a>
|
||||
<li
|
||||
onClick={this.handleClick}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
className={`ifql-func--item ${this.getActiveClass()}`}
|
||||
>
|
||||
{name}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
private getActiveClass(): string {
|
||||
const {name, selectedFunc} = this.props
|
||||
return name === selectedFunc ? 'active' : ''
|
||||
}
|
||||
|
||||
private handleMouseEnter = () => {
|
||||
this.props.onSetSelectedFunc(this.props.name)
|
||||
}
|
||||
|
||||
private handleClick = () => {
|
||||
this.props.onAddNode(this.props.name)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {ClickOutside} from 'src/shared/components/ClickOutside'
|
||||
import FuncList from 'src/ifql/components/FuncList'
|
||||
|
@ -6,6 +7,8 @@ import FuncList from 'src/ifql/components/FuncList'
|
|||
interface State {
|
||||
isOpen: boolean
|
||||
inputText: string
|
||||
selectedFunc: string
|
||||
availableFuncs: string[]
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
@ -20,71 +23,128 @@ export class FuncSelector extends PureComponent<Props, State> {
|
|||
this.state = {
|
||||
isOpen: false,
|
||||
inputText: '',
|
||||
selectedFunc: '',
|
||||
availableFuncs: [],
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {isOpen, inputText} = this.state
|
||||
const {isOpen, inputText, selectedFunc, availableFuncs} = this.state
|
||||
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.handleClickOutside}>
|
||||
<div className={`dropdown dashboard-switcher ${this.openClass}`}>
|
||||
<button
|
||||
className="btn btn-square btn-default btn-sm dropdown-toggle"
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<span className="icon plus" />
|
||||
</button>
|
||||
<FuncList
|
||||
inputText={inputText}
|
||||
onAddNode={this.handleAddNode}
|
||||
isOpen={isOpen}
|
||||
funcs={this.availableFuncs}
|
||||
onInputChange={this.handleInputChange}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
/>
|
||||
<div className="ifql-func--selector">
|
||||
{isOpen ? (
|
||||
<FuncList
|
||||
inputText={inputText}
|
||||
onAddNode={this.handleAddNode}
|
||||
funcs={availableFuncs}
|
||||
onInputChange={this.handleInputChange}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
selectedFunc={selectedFunc}
|
||||
onSetSelectedFunc={this.handleSetSelectedFunc}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
className="btn btn-square btn-primary btn-sm ifql-func--button"
|
||||
onClick={this.handleOpenList}
|
||||
tabIndex={0}
|
||||
>
|
||||
<span className="icon plus" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</ClickOutside>
|
||||
)
|
||||
}
|
||||
|
||||
private get openClass(): string {
|
||||
if (this.state.isOpen) {
|
||||
return 'open'
|
||||
}
|
||||
|
||||
return ''
|
||||
private handleCloseList = () => {
|
||||
this.setState({isOpen: false, selectedFunc: '', availableFuncs: []})
|
||||
}
|
||||
|
||||
private handleAddNode = (name: string) => {
|
||||
this.setState({isOpen: false})
|
||||
this.handleCloseList()
|
||||
this.props.onAddNode(name)
|
||||
}
|
||||
|
||||
private get availableFuncs(): string[] {
|
||||
return this.props.funcs.filter(f =>
|
||||
f.toLowerCase().includes(this.state.inputText)
|
||||
private setAvailableFuncs = inputText => {
|
||||
const {funcs} = this.props
|
||||
const {selectedFunc} = this.state
|
||||
|
||||
const availableFuncs = funcs.filter(f =>
|
||||
f.toLowerCase().includes(inputText)
|
||||
)
|
||||
const isSelectedVisible = !!availableFuncs.find(a => a === selectedFunc)
|
||||
const newSelectedFunc = availableFuncs.length > 0 ? availableFuncs[0] : ''
|
||||
|
||||
this.setState({
|
||||
inputText,
|
||||
availableFuncs,
|
||||
selectedFunc: isSelectedVisible ? selectedFunc : newSelectedFunc,
|
||||
})
|
||||
}
|
||||
|
||||
private handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({inputText: e.target.value})
|
||||
this.setAvailableFuncs(e.target.value)
|
||||
}
|
||||
|
||||
private handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key !== 'Escape') {
|
||||
return
|
||||
const {selectedFunc, availableFuncs} = this.state
|
||||
const selectedFuncExists = selectedFunc !== ''
|
||||
|
||||
if (e.key === 'Enter' && selectedFuncExists) {
|
||||
return this.handleAddNode(selectedFunc)
|
||||
}
|
||||
|
||||
this.setState({inputText: '', isOpen: false})
|
||||
if (e.key === 'Escape' || e.key === 'Tab') {
|
||||
return this.handleCloseList()
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowUp' && selectedFuncExists) {
|
||||
// get index of selectedFunc in availableFuncs
|
||||
const selectedIndex = _.findIndex(
|
||||
availableFuncs,
|
||||
func => func === selectedFunc
|
||||
)
|
||||
const previousIndex = selectedIndex - 1
|
||||
// if there is selectedIndex - 1 in availableFuncs make that the new SelectedFunc
|
||||
if (previousIndex >= 0) {
|
||||
return this.setState({selectedFunc: availableFuncs[previousIndex]})
|
||||
}
|
||||
// if not then keep selectedFunc as is
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowDown' && selectedFuncExists) {
|
||||
// get index of selectedFunc in availableFuncs
|
||||
const selectedIndex = _.findIndex(
|
||||
availableFuncs,
|
||||
func => func === selectedFunc
|
||||
)
|
||||
const nextIndex = selectedIndex + 1
|
||||
// if there is selectedIndex + 1 in availableFuncs make that the new SelectedFunc
|
||||
if (nextIndex < availableFuncs.length) {
|
||||
return this.setState({selectedFunc: availableFuncs[nextIndex]})
|
||||
}
|
||||
// if not then keep selectedFunc as is
|
||||
}
|
||||
}
|
||||
|
||||
private handleClick = () => {
|
||||
this.setState({isOpen: !this.state.isOpen})
|
||||
private handleSetSelectedFunc = funcName => {
|
||||
this.setState({selectedFunc: funcName})
|
||||
}
|
||||
|
||||
private handleOpenList = () => {
|
||||
const {funcs} = this.props
|
||||
this.setState({
|
||||
isOpen: true,
|
||||
inputText: '',
|
||||
availableFuncs: funcs,
|
||||
selectedFunc: funcs[0],
|
||||
})
|
||||
}
|
||||
|
||||
private handleClickOutside = () => {
|
||||
this.setState({isOpen: false})
|
||||
this.handleCloseList()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,9 @@ $scrollbar-color-b: $c-comet;
|
|||
|
||||
.fancy-scroll--track-h,
|
||||
.fancy-scroll--track-v {
|
||||
&:hover {cursor: pointer;}
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
/* Horizontal Scrollbar Styles */
|
||||
.fancy-scroll--track-h {
|
||||
|
@ -24,7 +26,7 @@ $scrollbar-color-b: $c-comet;
|
|||
.fancy-scroll--thumb-h {
|
||||
height: $scrollbar-thumb-size !important;
|
||||
border-radius: ($scrollbar-thumb-size / 2);
|
||||
@include gradient-h($scrollbar-color-a,$scrollbar-color-b);
|
||||
@include gradient-h($scrollbar-color-a, $scrollbar-color-b);
|
||||
}
|
||||
/* Vertical Scrollbar Styles */
|
||||
.fancy-scroll--track-v {
|
||||
|
@ -37,7 +39,7 @@ $scrollbar-color-b: $c-comet;
|
|||
.fancy-scroll--thumb-v {
|
||||
width: $scrollbar-thumb-size !important;
|
||||
border-radius: ($scrollbar-thumb-size / 2);
|
||||
@include gradient-v($scrollbar-color-a,$scrollbar-color-b);
|
||||
@include gradient-v($scrollbar-color-a, $scrollbar-color-b);
|
||||
}
|
||||
|
||||
/* Kapacitor Theme Scrollbars */
|
||||
|
@ -45,14 +47,35 @@ $scrollbar-color-kap-a: $c-rainforest;
|
|||
$scrollbar-color-kap-b: $c-pool;
|
||||
|
||||
.fancy-scroll--kapacitor {
|
||||
.fancy-scroll--thumb-h { @include gradient-h($scrollbar-color-kap-a,$scrollbar-color-kap-b); }
|
||||
.fancy-scroll--thumb-v { @include gradient-v($scrollbar-color-kap-a,$scrollbar-color-kap-b); }
|
||||
.fancy-scroll--thumb-h {
|
||||
@include gradient-h($scrollbar-color-kap-a, $scrollbar-color-kap-b);
|
||||
}
|
||||
.fancy-scroll--thumb-v {
|
||||
@include gradient-v($scrollbar-color-kap-a, $scrollbar-color-kap-b);
|
||||
}
|
||||
}
|
||||
|
||||
/* Kapacitor Theme Scrollbars */
|
||||
$scrollbar-color-kap-a: $c-rainforest;
|
||||
$scrollbar-color-kap-b: $c-pool;
|
||||
|
||||
.fancy-scroll--func-selector {
|
||||
.fancy-scroll--thumb-h {
|
||||
@include gradient-h($c-neutrino, $c-hydrogen);
|
||||
}
|
||||
.fancy-scroll--thumb-v {
|
||||
@include gradient-v($c-neutrino, $c-hydrogen);
|
||||
}
|
||||
}
|
||||
|
||||
/* Dropdown Theme Scrollbars */
|
||||
ul.dropdown-menu {
|
||||
.fancy-scroll--thumb-h { @include gradient-h($c-neutrino,$c-laser); }
|
||||
.fancy-scroll--thumb-v { @include gradient-v($c-neutrino,$c-laser); }
|
||||
.fancy-scroll--thumb-h {
|
||||
@include gradient-h($c-neutrino, $c-laser);
|
||||
}
|
||||
.fancy-scroll--thumb-v {
|
||||
@include gradient-v($c-neutrino, $c-laser);
|
||||
}
|
||||
}
|
||||
/* Hacky Fix to make fancy scrollbars work in Safari */
|
||||
.query-builder--list {
|
||||
|
|
Loading…
Reference in New Issue