diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index 8f6f4ead9..230d079db 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -1,143 +1,133 @@ -import React from 'react'; -import createReactClass from 'create-react-class'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; import _ from 'lodash'; import './combobox.less'; -const Combobox = createReactClass({ - displayName : 'Combobox', - getDefaultProps : function() { - return { - id : '', - className : '', - trigger : 'hover', - default : '', - placeholder : '', - tooltip : '', - autoSuggest : { - clearAutoSuggestOnClick : true, - suggestMethod : 'includes', - filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter - }, - valuePatterns : /.+/ - }; +const Combobox = ({ + id = '', + className = '', + trigger = 'hover', + defaultValue = '', + placeholder = '', + tooltip = '', + autoSuggest = { + clearAutoSuggestOnClick : true, + suggestMethod : 'includes', + filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter }, - getInitialState : function() { - this.dropdownRef = React.createRef(); - return { - showDropdown : false, - value : '', - options : [...this.props.options], - inputFocused : false - }; - }, - componentDidMount : function() { - if(this.props.trigger == 'click') - document.addEventListener('click', this.handleClickOutside); - this.setState({ - value : this.props.default - }); - }, - 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); + valuePatterns = '.+', + options = [], + keepValueOnClose = false, + ...props +})=>{ + const dropdownRef = useRef(null); + const [showDropdown, setShowDropdown] = useState(false); + const [inputFocused, setInputFocused] = useState(false); + const [value, setValue] = useState(defaultValue); + + useEffect(()=>{ + if(trigger === 'click') { + document.addEventListener('click', handleClickOutside); + return ()=>document.removeEventListener('click', handleClickOutside); } - }, - handleDropdown : function(show){ - this.setState({ - value : show ? '' : this.props.default, - showDropdown : show, - inputFocused : this.props.autoSuggest.clearAutoSuggestOnClick ? show : false - }); - }, - handleInput : function(e){ - e.persist(); - this.setState({ - value : e.target.value, - inputFocused : false - }, ()=>{ - this.props.onEntry(e); - }); - }, - handleSelect : function(value, data=value){ - this.setState({ - value : value - }, ()=>{this.props.onSelect(data);}); - ; - }, - renderTextInput : function(){ + }, []); + + const handleClickOutside = useCallback((e)=>{ + // Close dropdown when clicked outside + if(!dropdownRef.current?.contains(e.target)) { + handleDropdown(false); + } + }, []); + + const handleDropdown = (show)=>{ + if(show) { + if(autoSuggest.clearAutoSuggestOnClick) { + setValue(''); + } + } else if(!keepValueOnClose) { + setValue(defaultValue); + } + setShowDropdown(show); + setInputFocused(autoSuggest.clearAutoSuggestOnClick && show); + }; + + const handleInput = (e)=>{ + setValue(e.target.value); + setInputFocused(false); + props.onEntry(e); + }; + + const handleSelect = (value, data=value)=>{ + setValue(value); + props.onSelect(data); + }; + + const renderTextInput = ()=>{ return (
{this.handleDropdown(true);} : undefined} - onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined} - {...(this.props.tooltip ? { 'data-tooltip-right': this.props.tooltip } : {})}> + onMouseEnter={trigger == 'hover' ? ()=>{handleDropdown(true);} : undefined} + onClick= {trigger == 'click' ? ()=>{handleDropdown(true);} : undefined} + {...(tooltip ? { 'data-tooltip-right': tooltip } : {})}> this.handleInput(e)} - value={this.state.value || ''} + onChange={(e)=>handleInput(e)} + value={value || ''} title='' - pattern={this.props.valuePatterns} - placeholder={this.props.placeholder} + pattern={valuePatterns} + placeholder={placeholder} onBlur={(e)=>{ if(!e.target.checkValidity()){ - this.setState({ - value : this.props.default - }); + setValue(defaultValue); } }} onKeyDown={(e)=>{ if(e.key === 'Enter') { e.preventDefault(); - this.props.onEntry(e); + props.onEntry(e); } }} />
); - }, - renderDropdown : function(dropdownChildren){ - if(!this.state.showDropdown) return null; - if(this.props.autoSuggest && !this.state.inputFocused){ - const suggestMethod = this.props.autoSuggest.suggestMethod; - const filterOn = _.isString(this.props.autoSuggest.filterOn) ? [this.props.autoSuggest.filterOn] : this.props.autoSuggest.filterOn; + }; + + const renderDropdown = ()=>{ + if(!showDropdown) return null; + let dropdownChildren = options.map((child, i)=>{ + 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 children = dropdownChildren.filter((item)=>{ if(suggestMethod === 'includes') - return item.props[attr]?.toLowerCase().includes(this.state.value.toLowerCase()); + return item.props[attr]?.toLowerCase().includes(value.toLowerCase()); if(suggestMethod === 'startsWith') - return item.props[attr]?.toLowerCase().startsWith(this.state.value.toLowerCase()); + return item.props[attr]?.toLowerCase().startsWith(value.toLowerCase()); }); return children; }); dropdownChildren = _.uniq(filteredArrays.flat(1)); } - return (
{dropdownChildren}
); - }, - render : function () { - const dropdownChildren = this.state.options.map((child, i)=>{ - const clone = React.cloneElement(child, { onClick: ()=>this.handleSelect(child.props.value, child.props.data) }); - return clone; - }); - return ( -
{this.handleDropdown(false);} : undefined}> - {this.renderTextInput()} - {this.renderDropdown(dropdownChildren)} -
- ); - } -}); + }; + + return ( +
{handleDropdown(false);} : undefined}> + {renderTextInput()} + {renderDropdown()} +
+ ); + +}; export default Combobox; diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.jsx b/client/homebrew/editor/metadataEditor/metadataEditor.jsx index d5c4387e9..feb1cf2b1 100644 --- a/client/homebrew/editor/metadataEditor/metadataEditor.jsx +++ b/client/homebrew/editor/metadataEditor/metadataEditor.jsx @@ -219,7 +219,7 @@ const MetadataEditor = ({ handleTheme(value)} onEntry={(e)=>{ @@ -261,8 +261,10 @@ const MetadataEditor = ({ handleLanguage(value)} onEntry={(e)=>{ e.target.setCustomValidity(''); //Clear the validation popup while typing diff --git a/client/homebrew/editor/tagInput/tagInput.jsx b/client/homebrew/editor/tagInput/tagInput.jsx index a153fef8b..ede194834 100644 --- a/client/homebrew/editor/tagInput/tagInput.jsx +++ b/client/homebrew/editor/tagInput/tagInput.jsx @@ -131,7 +131,7 @@ const TagInput = ({ id, tooltip, label, valuePatterns, values = [], unique = tru id={id} trigger='click' className='tagInput-dropdown' - default='' + defaultValue='' placeholder={placeholder} options={label === 'tags' ? suggestionOptions : []} tooltip={tooltip} diff --git a/client/homebrew/pages/basePages/listPage/brewItem/brewItem.less b/client/homebrew/pages/basePages/listPage/brewItem/brewItem.less index 4b3751ca3..d091f636f 100644 --- a/client/homebrew/pages/basePages/listPage/brewItem/brewItem.less +++ b/client/homebrew/pages/basePages/listPage/brewItem/brewItem.less @@ -145,8 +145,8 @@ box-shadow : 0 4px 5px 0 #333333; .text { min-height : 54px; + padding-left : 5px; h2 { margin-bottom : 5px; } - .description { padding-left : 5px; } } .info { padding-left : 5px; } .links { @@ -398,7 +398,7 @@ } .storage { display : none; } } - .brewItem { + .brewItem { h2 { width : 200px; font-size : 1em; @@ -426,7 +426,7 @@ } .info { display : grid; - grid-template-areas : + grid-template-areas : 'tags pages views views' 'authors update update storage'; grid-template-columns : 2fr 1fr 1fr 20px; @@ -440,7 +440,7 @@ a, button { margin : 0; } } - } + } } } @container (max-width : 650px) { diff --git a/client/homebrew/pages/basePages/listPage/listPage.jsx b/client/homebrew/pages/basePages/listPage/listPage.jsx index 802c3a14a..8b5da0997 100644 --- a/client/homebrew/pages/basePages/listPage/listPage.jsx +++ b/client/homebrew/pages/basePages/listPage/listPage.jsx @@ -1,6 +1,6 @@ /*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ import './listPage.less'; -import React, { useEffect, useState, useRef, useMemo } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import moment from 'moment'; import _ from 'lodash'; @@ -26,24 +26,7 @@ const ListPage = ({ brewCollection = [{ title: '', class: '', brews: [] }], navI const [layoutMode, setLayoutMode] = useState(DEFAULT_LAYOUT_MODE); const [themeMode, setThemeMode] = useState(DEFAULT_THEME_MODE); - const groupVisibilityRef = useRef(groupVisibility); - const sortTypeRef = useRef(sortType); - const sortDirRef = useRef(sortDir); - useEffect(()=>{ - groupVisibilityRef.current = groupVisibility; - }, [groupVisibility]); - - useEffect(()=>{ - sortTypeRef.current = sortType; - }, [sortType]); - - useEffect(()=>{ - sortDirRef.current = sortDir; - }, [sortDir]); - - useEffect(()=>{ - window.onbeforeunload = saveToLocalStorage; if(typeof window === 'undefined') return; const newSortType = sortType ?? (localStorage.getItem(USERPAGE_SORT_TYPE) || DEFAULT_SORT_TYPE); @@ -63,20 +46,8 @@ const ListPage = ({ brewCollection = [{ title: '', class: '', brews: [] }], navI setSortDir(newSortDir); setLayoutMode(newLayoutMode); setThemeMode(newThemeMode); - - return ()=>{ - window.onbeforeunload = null; - }; }, []); - const saveToLocalStorage = ()=>{ - brewCollection.forEach((brewGroup)=>{ - localStorage.setItem(`${USERPAGE_GROUP_VISIBILITY_PREFIX}_${brewGroup.class}`, `${groupVisibilityRef.current[brewGroup.class]}`); - }); - localStorage.setItem(USERPAGE_SORT_TYPE, sortTypeRef.current); - localStorage.setItem(USERPAGE_SORT_DIR, sortDirRef.current); - }; - const renderBrews = (brews)=>{ if(!brews || !brews.length) return
No Brews.
; @@ -108,6 +79,8 @@ const ListPage = ({ brewCollection = [{ title: '', class: '', brews: [] }], navI const handleSortOptionChange = (event)=>{ updateUrl(filterString, event.target.value, sortDir); setSortType(event.target.value); + localStorage.setItem(USERPAGE_SORT_TYPE, event.target.value); + return; }; const handleSortDirChange = (event)=>{ @@ -115,6 +88,8 @@ const ListPage = ({ brewCollection = [{ title: '', class: '', brews: [] }], navI updateUrl(filterString, sortType, newDir); setSortDir(newDir); + localStorage.setItem(USERPAGE_SORT_DIR, newDir); + return; }; const renderSortOption = (sortTitle, sortValue)=>{ @@ -187,7 +162,7 @@ const ListPage = ({ brewCollection = [{ title: '', class: '', brews: [] }], navI if(filterTags?.length == 0) return; return (
- Tags: + Tags: {_.map(filterTags, (tag, idx)=>{ const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/); return ( @@ -198,7 +173,7 @@ const ListPage = ({ brewCollection = [{ title: '', class: '', brews: [] }], navI updateUrl(filterString, sortType, sortDir, tag); }}> {matches[2]} - + ); })}
@@ -257,7 +232,13 @@ const ListPage = ({ brewCollection = [{ title: '', class: '', brews: [] }], navI }, [brewCollection, filterString, filterTags, sortType, sortDir]); const toggleBrewCollectionState = (brewGroupClass)=>{ - setGroupVisibility((prevVisibility)=>({ ...prevVisibility, [brewGroupClass]: !prevVisibility[brewGroupClass] })); + setGroupVisibility((prevVisibility)=>{ + const visible = !prevVisibility[brewGroupClass]; + + localStorage.setItem(`${USERPAGE_GROUP_VISIBILITY_PREFIX}_${brewGroupClass}`, visible); + + return {...prevVisibility, [brewGroupClass]: visible}; + }); }; const renderBrewCollection = (brewCollection)=>{ @@ -298,8 +279,8 @@ const ListPage = ({ brewCollection = [{ title: '', class: '', brews: [] }], navI
View: