mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-11-04 20:29:01 +00:00
Merge pull request #5161 from naturalcrit/refactor-combobox
Refactor of combobox as functional
This commit is contained in:
+94
-104
@@ -1,143 +1,133 @@
|
|||||||
import React from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import createReactClass from 'create-react-class';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import './combobox.less';
|
import './combobox.less';
|
||||||
|
|
||||||
const Combobox = createReactClass({
|
const Combobox = ({
|
||||||
displayName : 'Combobox',
|
id = '',
|
||||||
getDefaultProps : function() {
|
className = '',
|
||||||
return {
|
trigger = 'hover',
|
||||||
id : '',
|
defaultValue = '',
|
||||||
className : '',
|
placeholder = '',
|
||||||
trigger : 'hover',
|
tooltip = '',
|
||||||
default : '',
|
autoSuggest = {
|
||||||
placeholder : '',
|
clearAutoSuggestOnClick : true,
|
||||||
tooltip : '',
|
suggestMethod : 'includes',
|
||||||
autoSuggest : {
|
filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter
|
||||||
clearAutoSuggestOnClick : true,
|
|
||||||
suggestMethod : 'includes',
|
|
||||||
filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter
|
|
||||||
},
|
|
||||||
valuePatterns : /.+/
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
valuePatterns = '.+',
|
||||||
this.dropdownRef = React.createRef();
|
options = [],
|
||||||
return {
|
keepValueOnClose = false,
|
||||||
showDropdown : false,
|
...props
|
||||||
value : '',
|
})=>{
|
||||||
options : [...this.props.options],
|
const dropdownRef = useRef(null);
|
||||||
inputFocused : false
|
const [showDropdown, setShowDropdown] = useState(false);
|
||||||
};
|
const [inputFocused, setInputFocused] = useState(false);
|
||||||
},
|
const [value, setValue] = useState(defaultValue);
|
||||||
componentDidMount : function() {
|
|
||||||
if(this.props.trigger == 'click')
|
useEffect(()=>{
|
||||||
document.addEventListener('click', this.handleClickOutside);
|
if(trigger === 'click') {
|
||||||
this.setState({
|
document.addEventListener('click', handleClickOutside);
|
||||||
value : this.props.default
|
return ()=>document.removeEventListener('click', handleClickOutside);
|
||||||
});
|
|
||||||
},
|
|
||||||
componentWillUnmount : function() {
|
|
||||||
if(this.props.trigger == 'click')
|
|
||||||
document.removeEventListener('click', this.handleClickOutside);
|
|
||||||
},
|
|
||||||
handleClickOutside : function(e){
|
|
||||||
// Close dropdown when clicked outside
|
|
||||||
if(this.dropdownRef.current && !this.dropdownRef.current.contains(e.target)) {
|
|
||||||
this.handleDropdown(false);
|
|
||||||
}
|
}
|
||||||
},
|
}, []);
|
||||||
handleDropdown : function(show){
|
|
||||||
this.setState({
|
const handleClickOutside = useCallback((e)=>{
|
||||||
value : show ? '' : this.props.default,
|
// Close dropdown when clicked outside
|
||||||
showDropdown : show,
|
if(!dropdownRef.current?.contains(e.target)) {
|
||||||
inputFocused : this.props.autoSuggest.clearAutoSuggestOnClick ? show : false
|
handleDropdown(false);
|
||||||
});
|
}
|
||||||
},
|
}, []);
|
||||||
handleInput : function(e){
|
|
||||||
e.persist();
|
const handleDropdown = (show)=>{
|
||||||
this.setState({
|
if(show) {
|
||||||
value : e.target.value,
|
if(autoSuggest.clearAutoSuggestOnClick) {
|
||||||
inputFocused : false
|
setValue('');
|
||||||
}, ()=>{
|
}
|
||||||
this.props.onEntry(e);
|
} else if(!keepValueOnClose) {
|
||||||
});
|
setValue(defaultValue);
|
||||||
},
|
}
|
||||||
handleSelect : function(value, data=value){
|
setShowDropdown(show);
|
||||||
this.setState({
|
setInputFocused(autoSuggest.clearAutoSuggestOnClick && show);
|
||||||
value : value
|
};
|
||||||
}, ()=>{this.props.onSelect(data);});
|
|
||||||
;
|
const handleInput = (e)=>{
|
||||||
},
|
setValue(e.target.value);
|
||||||
renderTextInput : function(){
|
setInputFocused(false);
|
||||||
|
props.onEntry(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelect = (value, data=value)=>{
|
||||||
|
setValue(value);
|
||||||
|
props.onSelect(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTextInput = ()=>{
|
||||||
return (
|
return (
|
||||||
<div className='dropdown-input item'
|
<div className='dropdown-input item'
|
||||||
onMouseEnter={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(true);} : undefined}
|
onMouseEnter={trigger == 'hover' ? ()=>{handleDropdown(true);} : undefined}
|
||||||
onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined}
|
onClick= {trigger == 'click' ? ()=>{handleDropdown(true);} : undefined}
|
||||||
{...(this.props.tooltip ? { 'data-tooltip-right': this.props.tooltip } : {})}>
|
{...(tooltip ? { 'data-tooltip-right': tooltip } : {})}>
|
||||||
<input
|
<input
|
||||||
id={this.props.id}
|
id={id}
|
||||||
type='text'
|
type='text'
|
||||||
onChange={(e)=>this.handleInput(e)}
|
onChange={(e)=>handleInput(e)}
|
||||||
value={this.state.value || ''}
|
value={value || ''}
|
||||||
title=''
|
title=''
|
||||||
pattern={this.props.valuePatterns}
|
pattern={valuePatterns}
|
||||||
placeholder={this.props.placeholder}
|
placeholder={placeholder}
|
||||||
onBlur={(e)=>{
|
onBlur={(e)=>{
|
||||||
if(!e.target.checkValidity()){
|
if(!e.target.checkValidity()){
|
||||||
this.setState({
|
setValue(defaultValue);
|
||||||
value : this.props.default
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e)=>{
|
onKeyDown={(e)=>{
|
||||||
if(e.key === 'Enter') {
|
if(e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.onEntry(e);
|
props.onEntry(e);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<i className='fas fa-caret-down'/>
|
<i className='fas fa-caret-down'/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
renderDropdown : function(dropdownChildren){
|
|
||||||
if(!this.state.showDropdown) return null;
|
const renderDropdown = ()=>{
|
||||||
if(this.props.autoSuggest && !this.state.inputFocused){
|
if(!showDropdown) return null;
|
||||||
const suggestMethod = this.props.autoSuggest.suggestMethod;
|
let dropdownChildren = options.map((child, i)=>{
|
||||||
const filterOn = _.isString(this.props.autoSuggest.filterOn) ? [this.props.autoSuggest.filterOn] : this.props.autoSuggest.filterOn;
|
const clone = React.cloneElement(child, { onClick: ()=>handleSelect(child.props.value, child.props.data) });
|
||||||
|
return clone;
|
||||||
|
});
|
||||||
|
if(autoSuggest && !inputFocused){
|
||||||
|
const suggestMethod = autoSuggest.suggestMethod;
|
||||||
|
const filterOn = _.isString(autoSuggest.filterOn) ? [autoSuggest.filterOn] : autoSuggest.filterOn;
|
||||||
const filteredArrays = filterOn.map((attr)=>{
|
const filteredArrays = filterOn.map((attr)=>{
|
||||||
const children = dropdownChildren.filter((item)=>{
|
const children = dropdownChildren.filter((item)=>{
|
||||||
if(suggestMethod === 'includes')
|
if(suggestMethod === 'includes')
|
||||||
return item.props[attr]?.toLowerCase().includes(this.state.value.toLowerCase());
|
return item.props[attr]?.toLowerCase().includes(value.toLowerCase());
|
||||||
if(suggestMethod === 'startsWith')
|
if(suggestMethod === 'startsWith')
|
||||||
return item.props[attr]?.toLowerCase().startsWith(this.state.value.toLowerCase());
|
return item.props[attr]?.toLowerCase().startsWith(value.toLowerCase());
|
||||||
});
|
});
|
||||||
return children;
|
return children;
|
||||||
});
|
});
|
||||||
dropdownChildren = _.uniq(filteredArrays.flat(1));
|
dropdownChildren = _.uniq(filteredArrays.flat(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='dropdown-options'>
|
<div className='dropdown-options'>
|
||||||
{dropdownChildren}
|
{dropdownChildren}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
render : function () {
|
|
||||||
const dropdownChildren = this.state.options.map((child, i)=>{
|
return (
|
||||||
const clone = React.cloneElement(child, { onClick: ()=>this.handleSelect(child.props.value, child.props.data) });
|
<div className={`dropdown-container ${className}`}
|
||||||
return clone;
|
ref={dropdownRef}
|
||||||
});
|
onMouseLeave={trigger == 'hover' ? ()=>{handleDropdown(false);} : undefined}>
|
||||||
return (
|
{renderTextInput()}
|
||||||
<div className={`dropdown-container ${this.props.className}`}
|
{renderDropdown()}
|
||||||
ref={this.dropdownRef}
|
</div>
|
||||||
onMouseLeave={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(false);} : undefined}>
|
);
|
||||||
{this.renderTextInput()}
|
|
||||||
{this.renderDropdown(dropdownChildren)}
|
};
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Combobox;
|
export default Combobox;
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ const MetadataEditor = ({
|
|||||||
<Combobox trigger='click'
|
<Combobox trigger='click'
|
||||||
id='combobox-themes'
|
id='combobox-themes'
|
||||||
className='themes-dropdown'
|
className='themes-dropdown'
|
||||||
default={currentThemeDisplay}
|
defaultValue={currentThemeDisplay}
|
||||||
placeholder='Select from below, or enter the Share URL or ID of a brew with the meta:theme tag'
|
placeholder='Select from below, or enter the Share URL or ID of a brew with the meta:theme tag'
|
||||||
onSelect={(value)=>handleTheme(value)}
|
onSelect={(value)=>handleTheme(value)}
|
||||||
onEntry={(e)=>{
|
onEntry={(e)=>{
|
||||||
@@ -261,8 +261,10 @@ const MetadataEditor = ({
|
|||||||
<Combobox trigger='click'
|
<Combobox trigger='click'
|
||||||
id='combobox-language'
|
id='combobox-language'
|
||||||
className='language-dropdown'
|
className='language-dropdown'
|
||||||
default={metadata.lang || ''}
|
defaultValue={metadata.lang || ''}
|
||||||
|
keepValueOnClose={true}
|
||||||
placeholder='en'
|
placeholder='en'
|
||||||
|
valuePatterns="^[a-z]{2,3}(-[A-Z][a-z]{3})?(-[A-Z]{2})?$"
|
||||||
onSelect={(value)=>handleLanguage(value)}
|
onSelect={(value)=>handleLanguage(value)}
|
||||||
onEntry={(e)=>{
|
onEntry={(e)=>{
|
||||||
e.target.setCustomValidity(''); //Clear the validation popup while typing
|
e.target.setCustomValidity(''); //Clear the validation popup while typing
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ const TagInput = ({ id, tooltip, label, valuePatterns, values = [], unique = tru
|
|||||||
id={id}
|
id={id}
|
||||||
trigger='click'
|
trigger='click'
|
||||||
className='tagInput-dropdown'
|
className='tagInput-dropdown'
|
||||||
default=''
|
defaultValue=''
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
options={label === 'tags' ? suggestionOptions : []}
|
options={label === 'tags' ? suggestionOptions : []}
|
||||||
tooltip={tooltip}
|
tooltip={tooltip}
|
||||||
|
|||||||
Reference in New Issue
Block a user