ui: autocomplete: rewrite in typescript

This commit is contained in:
undefined committed 2022-04-05 03:26:30 +08:00
1 parent 370fc07914
commit 8f4a63006a
11 files changed
+332 -293

No files matched your search

@@ -5,6 +5,22 @@ import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import AutoComplete from '.';
import DomainSelectAutoCompleteFC from './components/DomainSelectAutoComplete';
const Component = React.forwardRef((props: { value: string, multi: boolean, onChange: (v: string) => void }, ref) => {
const [value, setValue] = React.useState(props.value);
return (
<DomainSelectAutoCompleteFC
ref={ref as any}
height="34px"
selectedKeys={value.split(',').map((i) => i.trim())}
onChange={(v) => {
setValue(v);
props.onChange(v);
}}
multi={props.multi}
/>
);
});
export default class DomainSelectAutoComplete extends AutoComplete {
static DOMAttachKey = 'ucwDomainSelectAutoCompleteInstance';
@@ -18,13 +34,11 @@ export default class DomainSelectAutoComplete extends AutoComplete {
attach() {
const value = this.$dom.val();
ReactDOM.render(
<DomainSelectAutoCompleteFC
<Component
ref={(ref) => { this.ref = ref; }}
height="34px"
defaultItems={value}
value={value}
onChange={this.onChange}
multi={this.options.multi}
freeSolo={this.options.multi}
/>,
this.container,
);
@@ -5,6 +5,22 @@ import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import AutoComplete from '.';
import ProblemSelectAutoCompleteFC from './components/ProblemSelectAutoComplete';
const Component = React.forwardRef<any, any>((props, ref) => {
const [value, setValue] = React.useState(props.value);
return (
<ProblemSelectAutoCompleteFC
ref={ref as any}
height="34px"
selectedKeys={value.split(',').map((i) => i.trim())}
onChange={(v) => {
setValue(v);
props.onChange(v);
}}
multi={props.multi}
/>
);
});
export default class ProblemSelectAutoComplete extends AutoComplete {
static DOMAttachKey = 'ucwProblemSelectAutoCompleteInstance';
@@ -18,13 +34,11 @@ export default class ProblemSelectAutoComplete extends AutoComplete {
attach() {
const value = this.$dom.val();
ReactDOM.render(
<ProblemSelectAutoCompleteFC
<Component
ref={(ref) => { this.ref = ref; }}
height="34px"
defaultItems={value}
onChange={this.onChange}
value={value}
multi={this.options.multi}
freeSolo={this.options.multi}
onChange={this.onChange}
/>,
this.container,
);
@@ -1,24 +1,28 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import { assign } from 'lodash';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
import AutoComplete, { AutoCompleteOptions } from '.';
import UserSelectAutoCompleteFC from './components/UserSelectAutoComplete';
const Component = React.forwardRef<any, any>((props, ref) => {
const [value, setValue] = React.useState(props.value);
return (
<UserSelectAutoCompleteFC
ref={ref as any}
height="34px"
selectedKeys={value.split(',').map((i) => i.trim())}
onChange={(v) => {
setValue(v);
props.onChange(v);
}}
multi={props.multi}
/>
);
});
export default class UserSelectAutoComplete<Multi extends boolean> extends AutoComplete {
static DOMAttachKey = 'ucwUserSelectAutoCompleteInstance';
client = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
retry: false,
staleTime: Infinity,
},
},
});
constructor($dom, options: AutoCompleteOptions<Multi>) {
super($dom, {
@@ -35,16 +39,12 @@ export default class UserSelectAutoComplete<Multi extends boolean> extends AutoC
attach() {
const value = this.$dom.val();
ReactDOM.render(
<QueryClientProvider client={this.client}>
<UserSelectAutoCompleteFC
ref={(ref) => { this.ref = ref; }}
height={this.options.height || '34px'}
defaultItems={value}
onChange={this.onChange}
multi={this.options.multi}
freeSolo={this.options.multi}
/>
</QueryClientProvider>,
<Component
ref={(ref) => { this.ref = ref; }}
value={value}
multi={this.options.multi}
onChange={this.onChange}
/>,
this.container,
);
}
@@ -7,38 +7,89 @@ import PropTypes from 'prop-types';
import { debounce } from 'lodash';
import Icon from 'vj/components/react/IconComponent';
export interface AutoCompleteProps<Item> {
width?: string;
/**
* if you need fix height, set to at least "30px"
* for Hydro, no less then "34px" can be better
*/
height?: string;
disabled?: boolean;
disabledHint?: string;
listStyle?: React.CSSProperties;
cacheKey?: string;
renderItem?: (item: Item) => any;
queryItems?: (query: string) => Promise<Item[]> | Item[];
fetchItems?: (ids: string[]) => Promise<Item[]> | Item[];
itemText?: (item: Item) => string;
itemKey?: (item: Item) => string;
onChange?: (value: string) => any;
multi?: boolean;
selectedKeys?: string[];
allowEmptyQuery?: boolean;
freeSolo?: boolean;
freeSoloConverter?: (value: string) => string;
}
export interface AutoCompleteHandle<Item> {
getSelectedItems: () => Item[];
getSelectedItemKeys: () => string[];
getSelectedItemsAsString: () => string;
setSelectedItems: (items: Item[]) => void;
getQuery: () => string;
setQuery: (query: string) => void;
triggerQuery: () => any;
closeList: () => void;
/** returns comma seperated value text */
getValue: () => string;
/** return value detail */
getValueArray: () => Item[];
clear: () => void;
focus: () => void;
}
const superCache = {};
// eslint-disable-next-line prefer-arrow-callback
const AutoComplete = forwardRef(function AutoComplete(props, ref) {
const AutoComplete = forwardRef(function Impl<T>(props: AutoCompleteProps<T>, ref: React.Ref<AutoCompleteHandle<T>>) {
const width = props.width ?? '100%';
// if you need fix height, set to at least "30px"
// for Hydro, no less then "34px" can be better
const height = props.height ?? 'auto';
const disabled = props.disabled ?? false;
const disabledHint = props.disabledHint ?? '';
const listStyle = props.listStyle ?? {};
const itemsFn = props.itemsFn ?? (async () => []);
const queryItems = props.queryItems ?? (() => []);
const renderItem = props.renderItem ?? ((item) => item);
const itemText = props.itemText ?? ((item) => item);
const itemText = props.itemText ?? ((item) => item.toString());
const itemKey = props.itemKey ?? itemText;
const onChange = props.onChange ?? (() => { });
const multi = props.multi ?? false;
const rawDefaultItems = props.defaultItems ?? [];
const defaultItems = typeof rawDefaultItems === 'string'
? rawDefaultItems.split(',').map((i) => i.trim()).filter((i) => i.length > 0) : rawDefaultItems;
const allowEmptyQuery = props.allowEmptyQuery ?? false;
const freeSolo = props.freeSolo ?? false;
const freeSoloConverter = freeSolo ? props.freeSoloConverter ?? ((i) => i) : ((i) => i);
const [focused, setFocused] = useState(false); // is focused
const [selected, setSelected] = useState(multi ? defaultItems : []); // selected items
const [selectedKeys, setSelectedKeys] = useState(multi ? defaultItems.map((i) => itemKey(i)) : []); // keys of selected items
const [selected, setSelected] = useState([]); // selected items
const [selectedKeys, setSelectedKeys] = useState(props.selectedKeys); // keys of selected items
const [itemList, setItemList] = useState([]); // items list
const [currentItem, setCurrentItem] = useState(null); // index of current item (in item list)
const [rerender, setRerender] = useState(false);
const inputRef = useRef();
const listRef = useRef();
const inputRef = useRef<HTMLInputElement>();
const listRef = useRef<HTMLUListElement>();
const cache = useRef({});
if (props.cacheKey) superCache[props.cacheKey] ||= { query: {}, value: {} };
const queryCache = props.cacheKey ? superCache[props.cacheKey].query : useRef({}).current;
const valueCache = props.cacheKey ? superCache[props.cacheKey].value : useRef({}).current;
useEffect(() => {
const ids = [];
for (const key of selectedKeys) if (!valueCache[key]) ids.push(key);
if (!ids.length) return;
Promise.resolve(props.fetchItems(ids)).then((items) => {
for (const item of items) valueCache[itemKey(item)] = item;
setRerender(!rerender);
});
}, [selectedKeys]);
const queryList = async (query) => {
if (!query && !allowEmptyQuery) {
@@ -46,27 +97,19 @@ const AutoComplete = forwardRef(function AutoComplete(props, ref) {
setCurrentItem(null);
return;
}
let items;
if (cache.current[query]) {
items = cache.current[query];
} else {
items = await itemsFn(query);
cache.current[query] = items;
}
setItemList(items);
if (!queryCache[query]) queryCache[query] = await queryItems(query);
for (const item of queryCache[query]) valueCache[itemKey(item)] = item;
setItemList(queryCache[query]);
setCurrentItem(null);
};
const calculateValue = () => {
const query = inputRef.current?.value;
if (!query) return multi ? selectedKeys.join(',') : '';
return (multi && selectedKeys.length > 0) ? `${selectedKeys.join(',')},${query}` : query;
return (multi && selectedKeys.length) ? `${selectedKeys.join(',')},${query}` : query;
};
const dispatchChange = () => {
const value = calculateValue();
onChange(value);
};
const dispatchChange = () => onChange(calculateValue());
useEffect(() => {
dispatchChange();
@@ -78,8 +121,7 @@ const AutoComplete = forwardRef(function AutoComplete(props, ref) {
queryList(value);
}, 300);
const toggleItem = (item) => {
const key = itemKey(item);
const toggleItem = (item: T, key = itemKey(item)) => {
if (multi) {
const idx = selectedKeys.indexOf(key);
if (idx !== -1) {
@@ -159,6 +201,7 @@ const AutoComplete = forwardRef(function AutoComplete(props, ref) {
useImperativeHandle(ref, () => ({
getSelectedItems: () => selected,
getSelectedItemKeys: () => selectedKeys,
getSelectedItemsAsString: () => selectedKeys.join(','),
setSelectedItems: (items) => {
setSelected(items);
@@ -175,12 +218,6 @@ const AutoComplete = forwardRef(function AutoComplete(props, ref) {
},
getValue: () => (multi ? selectedKeys.join(',') : (inputRef.current.value ?? '')),
getValueArray: () => (multi ? selected : [inputRef.current?.value].filter((i) => !!i)),
getValueWithQuery: () => calculateValue(),
getValueArrayWithQuery: () => {
const query = inputRef.current?.value;
if (!query) return multi ? selected : [''];
return multi ? [...selected, query] : [query];
},
clear: () => {
setSelected([]);
setSelectedKeys([]);
@@ -198,12 +235,15 @@ const AutoComplete = forwardRef(function AutoComplete(props, ref) {
className={focused ? 'autocomplete-wrapper focused' : 'autocomplete-wrapper'}
style={{ width, height }}
>
{multi && selected.map((item, idx) => (
<div className="autocomplete-tag" key={selectedKeys[idx]}>
<div>{itemText(item)}</div>
<Icon name="close" onClick={() => toggleItem(item)} />
</div>
))}
{multi && selectedKeys.map((key) => {
const item = valueCache[key];
return (
<div className="autocomplete-tag" key={item ? key : `draft-${key}`}>
<div>{item ? itemText(item) : key}</div>
<Icon name="close" onClick={() => toggleItem(item, key)} />
</div>
);
})}
{disabled && (
<input
disabled
@@ -219,15 +259,10 @@ const AutoComplete = forwardRef(function AutoComplete(props, ref) {
dispatchChange();
handleInputChange(e);
}}
onFocus={() => {
setFocused(true);
if (allowEmptyQuery) {
handleInputChange();
}
}}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
onKeyDown={handleInputKeyDown}
defaultValue={multi ? '' : defaultItems.join(',')}
defaultValue={multi ? '' : selectedKeys.join(',')}
/>
</div>
{focused && itemList.length > 0 && (
@@ -241,14 +276,14 @@ const AutoComplete = forwardRef(function AutoComplete(props, ref) {
data-focus={idx === currentItem}
>
<div>{renderItem(item)}</div>
<Icon name="check" />
{selectedKeys.includes(itemKey(item)) && <Icon name="check" />}
</li>
))}
</ul>
)}
</div>
);
});
}) as (<T>(props: AutoCompleteProps<T> & { ref: React.Ref<AutoCompleteHandle<T>> }) => React.ReactElement) & React.FC;
AutoComplete.propTypes = {
width: PropTypes.string,
@@ -256,13 +291,13 @@ AutoComplete.propTypes = {
disabled: PropTypes.bool,
disabledHint: PropTypes.string,
listStyle: PropTypes.object,
itemsFn: PropTypes.func.isRequired,
queryItems: PropTypes.func.isRequired,
itemKey: PropTypes.func,
renderItem: PropTypes.func,
itemText: PropTypes.func,
onChange: PropTypes.func.isRequired,
multi: PropTypes.bool,
defaultItems: PropTypes.oneOfType([(PropTypes.arrayOf(PropTypes.any)), PropTypes.string]),
selectedKeys: PropTypes.arrayOf(PropTypes.string),
allowEmptyQuery: PropTypes.bool,
freeSolo: PropTypes.bool,
freeSoloConverter: PropTypes.func,
@@ -275,9 +310,9 @@ AutoComplete.defaultProps = {
disabledHint: '',
listStyle: {},
renderItem: (item) => item,
itemText: (item) => item,
itemText: (item) => item.toString(),
multi: false,
defaultItems: [],
selectedKeys: [],
allowEmptyQuery: false,
freeSolo: false,
freeSoloConverter: (input) => input,
@@ -0,0 +1,42 @@
import React from 'react';
import PropTypes from 'prop-types';
import AutoComplete from './AutoComplete';
const CustomSelectAutoComplete = React.forwardRef<any, any>((props, ref) => (
<AutoComplete<any>
ref={ref as any}
queryItems={(query) => props.data.filter((i) => (i.name ? i.name.match(query) : i.match(query)))}
itemText={(item) => `${item.name || item}`}
itemKey={(item) => `${item._id?.toString() || item.name || item}`}
renderItem={(item) => `${item.name || item}`}
{...props}
/>
));
CustomSelectAutoComplete.propTypes = {
width: PropTypes.string,
height: PropTypes.string,
listStyle: PropTypes.object,
onChange: PropTypes.func.isRequired,
multi: PropTypes.bool,
selectedKeys: PropTypes.arrayOf(PropTypes.string),
allowEmptyQuery: PropTypes.bool,
freeSolo: PropTypes.bool,
freeSoloConverter: PropTypes.func,
data: PropTypes.any,
};
CustomSelectAutoComplete.displayName = 'CustomSelectAutoComplete';
CustomSelectAutoComplete.defaultProps = {
width: '100%',
height: 'auto',
listStyle: {},
multi: false,
selectedKeys: [],
allowEmptyQuery: false,
freeSolo: false,
freeSoloConverter: (input) => input,
};
export default CustomSelectAutoComplete;
@@ -1,65 +0,0 @@
import React, { forwardRef } from 'react';
import PropTypes from 'prop-types';
import request from 'vj/utils/request';
import AutoComplete from './AutoComplete';
// eslint-disable-next-line prefer-arrow-callback
const DomainSelectAutoComplete = forwardRef(function DomainSelectAutoComplete(props, ref) {
const itemsFn = (query) => request.get('/domain/search', { q: query });
/* const itemText = (domain) => {
return domain.name;
}; */
const itemKey = (domain) => domain._id || domain;
const renderItem = (domain) => (
<div className="media">
<div className="media__left medium">
<img className="small domain-profile-avatar" src={domain.avatarUrl} alt="" width="30" height="30" />
</div>
<div className="media__body medium">
<div className="domain-select__name">{domain.name}</div>
<div className="domain-select__id">ID = {domain._id}</div>
</div>
</div>
);
return (
<AutoComplete
ref={ref}
itemsFn={itemsFn}
itemText={itemKey}
itemKey={itemKey}
renderItem={renderItem}
{...props}
/>
);
});
DomainSelectAutoComplete.propTypes = {
width: PropTypes.string,
height: PropTypes.string,
listStyle: PropTypes.object,
onChange: PropTypes.func.isRequired,
multi: PropTypes.bool,
defaultItems: PropTypes.oneOfType([(PropTypes.arrayOf(PropTypes.any)), PropTypes.string]),
allowEmptyQuery: PropTypes.bool,
freeSolo: PropTypes.bool,
freeSoloConverter: PropTypes.func,
};
DomainSelectAutoComplete.defaultProps = {
width: '100%',
height: 'auto',
listStyle: {},
multi: false,
defaultItems: [],
allowEmptyQuery: false,
freeSolo: false,
freeSoloConverter: (input) => input,
};
DomainSelectAutoComplete.displayName = 'DomainSelectAutoComplete';
export default DomainSelectAutoComplete;
@@ -0,0 +1,54 @@
import React, { forwardRef } from 'react';
import PropTypes from 'prop-types';
import request from 'vj/utils/request';
import type { DomainDoc } from 'hydrooj/src/interface';
import AutoComplete, { AutoCompleteHandle, AutoCompleteProps } from './AutoComplete';
const DomainSelectAutoComplete = forwardRef<AutoCompleteHandle<DomainDoc>, AutoCompleteProps<DomainDoc>>((props, ref) => (
<AutoComplete<DomainDoc>
ref={ref as any}
cacheKey="domain"
renderItem={(domain) => (
<div className="media">
<div className="media__left medium">
<img className="small domain-profile-avatar" src={domain.avatarUrl} alt="" width="30" height="30" />
</div>
<div className="media__body medium">
<div className="domain-select__name">{domain.name}</div>
<div className="domain-select__id">ID = {domain._id}</div>
</div>
</div>
)}
queryItems={(query) => request.get('/domain/search', { q: query })}
itemText={(domain) => domain.name}
itemKey={(domain) => domain._id}
{...props}
/>
));
DomainSelectAutoComplete.propTypes = {
width: PropTypes.string,
height: PropTypes.string,
listStyle: PropTypes.object,
onChange: PropTypes.func.isRequired,
multi: PropTypes.bool,
selectedKeys: PropTypes.arrayOf(PropTypes.string),
allowEmptyQuery: PropTypes.bool,
freeSolo: PropTypes.bool,
freeSoloConverter: PropTypes.func,
};
DomainSelectAutoComplete.defaultProps = {
width: '100%',
height: 'auto',
listStyle: {},
multi: false,
selectedKeys: [],
allowEmptyQuery: false,
freeSolo: false,
freeSoloConverter: (input) => input,
};
DomainSelectAutoComplete.displayName = 'DomainSelectAutoComplete';
export default DomainSelectAutoComplete;
@@ -1,62 +0,0 @@
import React, { forwardRef } from 'react';
import PropTypes from 'prop-types';
import request from 'vj/utils/request';
import AutoComplete from './AutoComplete';
// eslint-disable-next-line prefer-arrow-callback
const ProblemSelectAutoComplete = forwardRef(function ProblemSelectAutoComplete(props, ref) {
const itemsFn = (query) => request.get(`/d/${UiContext.domainId}/problem/list`, { prefix: query });
/* const itemText = (pdoc) => {
return (pdoc.pid ? `${pdoc.pid} ` : '') + pdoc.title;
}; */
const itemKey = (pdoc) => `${pdoc.docId || pdoc}`; // force string
const renderItem = (pdoc) => (
<div className="media">
<div className="media__body medium">
<div className="problem-select__name">{pdoc.pid ? `${pdoc.pid} ` : ''}{pdoc.title}</div>
<div className="problem-select__id">ID = {pdoc.docId}</div>
</div>
</div>
);
return (
<AutoComplete
ref={ref}
itemsFn={itemsFn}
itemText={itemKey}
itemKey={itemKey}
renderItem={renderItem}
{...props}
/>
);
});
ProblemSelectAutoComplete.propTypes = {
width: PropTypes.string,
height: PropTypes.string,
listStyle: PropTypes.object,
onChange: PropTypes.func.isRequired,
multi: PropTypes.bool,
defaultItems: PropTypes.oneOfType([(PropTypes.arrayOf(PropTypes.any)), PropTypes.string]),
allowEmptyQuery: PropTypes.bool,
freeSolo: PropTypes.bool,
freeSoloConverter: PropTypes.func,
};
ProblemSelectAutoComplete.defaultProps = {
width: '100%',
height: 'auto',
listStyle: {},
multi: false,
defaultItems: [],
allowEmptyQuery: false,
freeSolo: false,
freeSoloConverter: (input) => input,
};
ProblemSelectAutoComplete.displayName = 'ProblemSelectAutoComplete';
export default ProblemSelectAutoComplete;
@@ -0,0 +1,53 @@
import React, { forwardRef } from 'react';
import PropTypes from 'prop-types';
import request from 'vj/utils/request';
import type { ProblemDoc } from 'hydrooj/src/interface';
import AutoComplete, { AutoCompleteHandle, AutoCompleteProps } from './AutoComplete';
const ProblemSelectAutoComplete = forwardRef<AutoCompleteHandle<ProblemDoc>, AutoCompleteProps<ProblemDoc>>((props, ref) => (
<AutoComplete<ProblemDoc>
ref={ref as any}
cacheKey={`problem-${UiContext.domainId}`}
queryItems={(query) => request.get(`/d/${UiContext.domainId}/problem/list`, { prefix: query })}
// FIXME fetch items
fetchItems={() => []}
itemText={(pdoc) => `${pdoc.docId || pdoc}`}
itemKey={(pdoc) => `${pdoc.docId || pdoc}`}
renderItem={(pdoc) => (
<div className="media">
<div className="media__body medium">
<div className="problem-select__name">{pdoc.pid ? `${pdoc.pid} ` : ''}{pdoc.title}</div>
<div className="problem-select__id">ID = {pdoc.docId}</div>
</div>
</div>
)}
{...props}
/>
));
ProblemSelectAutoComplete.propTypes = {
width: PropTypes.string,
height: PropTypes.string,
listStyle: PropTypes.object,
onChange: PropTypes.func.isRequired,
multi: PropTypes.bool,
selectedKeys: PropTypes.arrayOf(PropTypes.string),
allowEmptyQuery: PropTypes.bool,
freeSolo: PropTypes.bool,
freeSoloConverter: PropTypes.func,
};
ProblemSelectAutoComplete.defaultProps = {
width: '100%',
height: 'auto',
listStyle: {},
multi: false,
selectedKeys: [],
allowEmptyQuery: false,
freeSolo: false,
freeSoloConverter: (input) => input,
};
ProblemSelectAutoComplete.displayName = 'ProblemSelectAutoComplete';
export default ProblemSelectAutoComplete;
@@ -1,89 +1,43 @@
import React, {
forwardRef, useImperativeHandle, useRef, useEffect,
} from 'react';
import { useQuery } from 'react-query';
import React, { forwardRef } from 'react';
import PropTypes from 'prop-types';
import api, { gql } from 'vj/utils/api';
import AutoComplete from './AutoComplete';
import type { AutoCompleteOptions } from '..';
import type { Udoc } from 'hydrooj/src/interface';
import AutoComplete, { AutoCompleteHandle, AutoCompleteProps } from './AutoComplete';
// eslint-disable-next-line prefer-arrow-callback
const UserSelectAutoComplete = forwardRef(function UserSelectAutoComplete(
props: AutoCompleteOptions, ref: React.ForwardedRef<AutoComplete>,
) {
const multi = props.multi ?? false;
const rawDefaultItems = props.defaultItems ?? '';
const defaultItems = multi ? rawDefaultItems
.split(',')
.map((i) => +i.trim())
.filter((i) => i) : rawDefaultItems;
const comRef = useRef<AutoComplete>();
const itemsBox = useRef([]);
const { isLoading } = useQuery(['default_user', defaultItems], async () => {
if (!multi || defaultItems.length === 0) return;
const items = await api(gql`
users(ids: ${defaultItems}) {
const UserSelectAutoComplete = forwardRef<AutoCompleteHandle<Udoc>, AutoCompleteProps<Udoc>>((props, ref) => (
<AutoComplete<Udoc>
ref={ref as any}
cacheKey="user"
{...props}
queryItems={(query) => api(gql`
users(search: ${query}) {
_id
uname
avatarUrl
}
`, ['data', 'users'])}
fetchItems={(ids) => api(gql`
users(ids: ${ids.map((i) => +i)}) {
_id
uname
}
`, ['data', 'users']);
itemsBox.current = items;
});
useEffect(() => {
if (multi && defaultItems.length > 0) {
comRef.current.setSelectedItems(itemsBox.current);
}
}, [itemsBox.current]);
useImperativeHandle(ref, () => new Proxy(comRef, {
get(target, propKey) {
if (typeof target.current?.[propKey] === 'function') {
return (...args: any[]) => target.current[propKey](...args);
}
return target.current?.[propKey];
},
}));
const itemsFn = (query) => api(gql`
users(search: ${query}) {
_id
uname
avatarUrl
}
`, ['data', 'users']);
const itemText = (user) => user.uname || user;
const itemKey = (user) => user._id;
const renderItem = (user) => (
<div className="media">
<div className="media__left medium">
<img className="small user-profile-avatar" alt="" src={user.avatarUrl} width="30" height="30" />
`, ['data', 'users'])}
itemText={(user) => user.uname}
itemKey={(user) => (props.multi ? user._id.toString() : user.uname)}
renderItem={(user) => (
<div className="media">
<div className="media__left medium">
<img className="small user-profile-avatar" alt="" src={user.avatarUrl} width="30" height="30" />
</div>
<div className="media__body medium">
<div className="user-select__uname">{user.uname}</div>
<div className="user-select__uid">UID = {user._id}</div>
</div>
</div>
<div className="media__body medium">
<div className="user-select__uname">{user.uname}</div>
<div className="user-select__uid">UID = {user._id}</div>
</div>
</div>
);
return (
<AutoComplete
ref={comRef}
{...props}
disabled={isLoading}
disabledHint="Loading..."
itemsFn={itemsFn}
itemText={itemText}
itemKey={multi ? itemKey : itemText}
renderItem={renderItem}
defaultItems={multi ? [] : rawDefaultItems}
/>
);
});
)}
/>
));
UserSelectAutoComplete.propTypes = {
width: PropTypes.string,
@@ -91,7 +45,7 @@ UserSelectAutoComplete.propTypes = {
listStyle: PropTypes.object,
onChange: PropTypes.func.isRequired,
multi: PropTypes.bool,
defaultItems: PropTypes.string,
selectedKeys: PropTypes.arrayOf(PropTypes.string),
allowEmptyQuery: PropTypes.bool,
freeSolo: PropTypes.bool,
freeSoloConverter: PropTypes.func,
@@ -102,7 +56,7 @@ UserSelectAutoComplete.defaultProps = {
height: 'auto',
listStyle: {},
multi: false,
defaultItems: '',
selectedKeys: [],
allowEmptyQuery: false,
freeSolo: false,
freeSoloConverter: (input) => input,
@@ -68,10 +68,10 @@ export default class AutoComplete extends DOMAttachedObject {
<AutoCompleteFC
ref={(ref) => { this.ref = ref; }}
height="34px"
itemsFn={this.options.items}
queryItems={this.options.items}
renderItem={this.options.render}
itemText={this.options.text}
defaultItems={value}
selectedKeys={value.split(',').map((i) => i.trim())}
onChange={this.onChange}
multi={this.options.multi}
freeSolo={this.options.multi}