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 React, {SFC, ChangeEvent, KeyboardEvent} from 'react'
|
||||||
|
|
||||||
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
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'
|
import FuncListItem from 'src/ifql/components/FuncListItem'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
inputText: string
|
inputText: string
|
||||||
isOpen: boolean
|
|
||||||
onInputChange: (e: ChangeEvent<HTMLInputElement>) => void
|
onInputChange: (e: ChangeEvent<HTMLInputElement>) => void
|
||||||
onKeyDown: (e: KeyboardEvent<HTMLInputElement>) => void
|
onKeyDown: (e: KeyboardEvent<HTMLInputElement>) => void
|
||||||
onAddNode: (name: string) => void
|
onAddNode: (name: string) => void
|
||||||
funcs: string[]
|
funcs: string[]
|
||||||
|
selectedFunc: string
|
||||||
|
onSetSelectedFunc: (name: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const FuncList: SFC<Props> = ({
|
const FuncList: SFC<Props> = ({
|
||||||
inputText,
|
inputText,
|
||||||
isOpen,
|
|
||||||
onAddNode,
|
onAddNode,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
onInputChange,
|
onInputChange,
|
||||||
funcs,
|
funcs,
|
||||||
|
selectedFunc,
|
||||||
|
onSetSelectedFunc,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<ul className="dropdown-menu funcs">
|
<div className="ifql-func--autocomplete">
|
||||||
<DropdownInput
|
<FuncSelectorInput
|
||||||
buttonSize="btn-xs"
|
|
||||||
buttonColor="btn-default"
|
|
||||||
onFilterChange={onInputChange}
|
onFilterChange={onInputChange}
|
||||||
onFilterKeyPress={onKeyDown}
|
onFilterKeyPress={onKeyDown}
|
||||||
searchTerm={inputText}
|
searchTerm={inputText}
|
||||||
/>
|
/>
|
||||||
<FancyScrollbar autoHide={false} autoHeight={true} maxHeight={240}>
|
<ul className="ifql-func--list">
|
||||||
{isOpen &&
|
<FancyScrollbar
|
||||||
|
autoHide={false}
|
||||||
|
autoHeight={true}
|
||||||
|
maxHeight={240}
|
||||||
|
className="fancy-scroll--func-selector"
|
||||||
|
>
|
||||||
|
{funcs.length > 0 ? (
|
||||||
funcs.map((func, i) => (
|
funcs.map((func, i) => (
|
||||||
<FuncListItem key={i} name={func} onAddNode={onAddNode} />
|
<FuncListItem
|
||||||
))}
|
key={i}
|
||||||
|
name={func}
|
||||||
|
onAddNode={onAddNode}
|
||||||
|
selectedFunc={selectedFunc}
|
||||||
|
onSetSelectedFunc={onSetSelectedFunc}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="ifql-func--item empty">No results</div>
|
||||||
|
)}
|
||||||
</FancyScrollbar>
|
</FancyScrollbar>
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@ import React, {PureComponent} from 'react'
|
||||||
interface Props {
|
interface Props {
|
||||||
name: string
|
name: string
|
||||||
onAddNode: (name: string) => void
|
onAddNode: (name: string) => void
|
||||||
|
selectedFunc: string
|
||||||
|
onSetSelectedFunc: (name: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class FuncListItem extends PureComponent<Props> {
|
export default class FuncListItem extends PureComponent<Props> {
|
||||||
|
@ -10,12 +12,25 @@ export default class FuncListItem extends PureComponent<Props> {
|
||||||
const {name} = this.props
|
const {name} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li onClick={this.handleClick} className="dropdown-item func">
|
<li
|
||||||
<a>{name}</a>
|
onClick={this.handleClick}
|
||||||
|
onMouseEnter={this.handleMouseEnter}
|
||||||
|
className={`ifql-func--item ${this.getActiveClass()}`}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getActiveClass(): string {
|
||||||
|
const {name, selectedFunc} = this.props
|
||||||
|
return name === selectedFunc ? 'active' : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMouseEnter = () => {
|
||||||
|
this.props.onSetSelectedFunc(this.props.name)
|
||||||
|
}
|
||||||
|
|
||||||
private handleClick = () => {
|
private handleClick = () => {
|
||||||
this.props.onAddNode(this.props.name)
|
this.props.onAddNode(this.props.name)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
|
import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
import {ClickOutside} from 'src/shared/components/ClickOutside'
|
import {ClickOutside} from 'src/shared/components/ClickOutside'
|
||||||
import FuncList from 'src/ifql/components/FuncList'
|
import FuncList from 'src/ifql/components/FuncList'
|
||||||
|
@ -6,6 +7,8 @@ import FuncList from 'src/ifql/components/FuncList'
|
||||||
interface State {
|
interface State {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
inputText: string
|
inputText: string
|
||||||
|
selectedFunc: string
|
||||||
|
availableFuncs: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -20,71 +23,128 @@ export class FuncSelector extends PureComponent<Props, State> {
|
||||||
this.state = {
|
this.state = {
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
inputText: '',
|
inputText: '',
|
||||||
|
selectedFunc: '',
|
||||||
|
availableFuncs: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const {isOpen, inputText} = this.state
|
const {isOpen, inputText, selectedFunc, availableFuncs} = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ClickOutside onClickOutside={this.handleClickOutside}>
|
<ClickOutside onClickOutside={this.handleClickOutside}>
|
||||||
<div className={`dropdown dashboard-switcher ${this.openClass}`}>
|
<div className="ifql-func--selector">
|
||||||
<button
|
{isOpen ? (
|
||||||
className="btn btn-square btn-default btn-sm dropdown-toggle"
|
|
||||||
onClick={this.handleClick}
|
|
||||||
>
|
|
||||||
<span className="icon plus" />
|
|
||||||
</button>
|
|
||||||
<FuncList
|
<FuncList
|
||||||
inputText={inputText}
|
inputText={inputText}
|
||||||
onAddNode={this.handleAddNode}
|
onAddNode={this.handleAddNode}
|
||||||
isOpen={isOpen}
|
funcs={availableFuncs}
|
||||||
funcs={this.availableFuncs}
|
|
||||||
onInputChange={this.handleInputChange}
|
onInputChange={this.handleInputChange}
|
||||||
onKeyDown={this.handleKeyDown}
|
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>
|
</div>
|
||||||
</ClickOutside>
|
</ClickOutside>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private get openClass(): string {
|
private handleCloseList = () => {
|
||||||
if (this.state.isOpen) {
|
this.setState({isOpen: false, selectedFunc: '', availableFuncs: []})
|
||||||
return 'open'
|
|
||||||
}
|
|
||||||
|
|
||||||
return ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleAddNode = (name: string) => {
|
private handleAddNode = (name: string) => {
|
||||||
this.setState({isOpen: false})
|
this.handleCloseList()
|
||||||
this.props.onAddNode(name)
|
this.props.onAddNode(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
private get availableFuncs(): string[] {
|
private setAvailableFuncs = inputText => {
|
||||||
return this.props.funcs.filter(f =>
|
const {funcs} = this.props
|
||||||
f.toLowerCase().includes(this.state.inputText)
|
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>) => {
|
private handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
this.setState({inputText: e.target.value})
|
this.setAvailableFuncs(e.target.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
private handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (e.key !== 'Escape') {
|
const {selectedFunc, availableFuncs} = this.state
|
||||||
return
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleClick = () => {
|
if (e.key === 'ArrowUp' && selectedFuncExists) {
|
||||||
this.setState({isOpen: !this.state.isOpen})
|
// 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 handleSetSelectedFunc = funcName => {
|
||||||
|
this.setState({selectedFunc: funcName})
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleOpenList = () => {
|
||||||
|
const {funcs} = this.props
|
||||||
|
this.setState({
|
||||||
|
isOpen: true,
|
||||||
|
inputText: '',
|
||||||
|
availableFuncs: funcs,
|
||||||
|
selectedFunc: funcs[0],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleClickOutside = () => {
|
private handleClickOutside = () => {
|
||||||
this.setState({isOpen: false})
|
this.handleCloseList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,9 @@ $scrollbar-color-b: $c-comet;
|
||||||
|
|
||||||
.fancy-scroll--track-h,
|
.fancy-scroll--track-h,
|
||||||
.fancy-scroll--track-v {
|
.fancy-scroll--track-v {
|
||||||
&:hover {cursor: pointer;}
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/* Horizontal Scrollbar Styles */
|
/* Horizontal Scrollbar Styles */
|
||||||
.fancy-scroll--track-h {
|
.fancy-scroll--track-h {
|
||||||
|
@ -45,14 +47,35 @@ $scrollbar-color-kap-a: $c-rainforest;
|
||||||
$scrollbar-color-kap-b: $c-pool;
|
$scrollbar-color-kap-b: $c-pool;
|
||||||
|
|
||||||
.fancy-scroll--kapacitor {
|
.fancy-scroll--kapacitor {
|
||||||
.fancy-scroll--thumb-h { @include gradient-h($scrollbar-color-kap-a,$scrollbar-color-kap-b); }
|
.fancy-scroll--thumb-h {
|
||||||
.fancy-scroll--thumb-v { @include gradient-v($scrollbar-color-kap-a,$scrollbar-color-kap-b); }
|
@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 */
|
/* Dropdown Theme Scrollbars */
|
||||||
ul.dropdown-menu {
|
ul.dropdown-menu {
|
||||||
.fancy-scroll--thumb-h { @include gradient-h($c-neutrino,$c-laser); }
|
.fancy-scroll--thumb-h {
|
||||||
.fancy-scroll--thumb-v { @include gradient-v($c-neutrino,$c-laser); }
|
@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 */
|
/* Hacky Fix to make fancy scrollbars work in Safari */
|
||||||
.query-builder--list {
|
.query-builder--list {
|
||||||
|
|
Loading…
Reference in New Issue