UNPKG

@react-awesome-query-builder/mui

Version:
280 lines (261 loc) 8.56 kB
import React, {useCallback} from "react"; import omit from "lodash/omit"; import TextField from "@mui/material/TextField"; import FormControl from "@mui/material/FormControl"; import Autocomplete, { createFilterOptions } from "@mui/material/Autocomplete"; import CircularProgress from "@mui/material/CircularProgress"; import Chip from "@mui/material/Chip"; import MenuItem from "@mui/material/MenuItem"; import Check from "@mui/icons-material/Check"; import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemText from "@mui/material/ListItemText"; import Tooltip from "@mui/material/Tooltip"; import { Hooks } from "@react-awesome-query-builder/ui"; import { useTheme } from "@mui/material/styles"; const { useListValuesAutocomplete } = Hooks; const emptyArray = []; // tip: option can contain `group: {label, title}` intead of `groupTitle` // but it's internal format, made for field autocomplete // see `JSON.stringify(option.group)` and `JSON.parse(groupMaybeJson)` export default (props) => { const { allowCustomValues, multiple, disableClearable, value: selectedValue, customProps, readonly, config, filterOptionsConfig, errorText, tooltipText, isFieldAutocomplete, dontFixOptionsOrder, } = props; const stringifyOption = useCallback((option) => { const keysForFilter = config.settings.listKeysForSearch; const valueForFilter = keysForFilter .map(k => (typeof option[k] == "string" ? option[k] : "")) .join("\0"); return valueForFilter; }, [config]); const defaultFilterOptionsConfig = { stringify: stringifyOption }; const filterOptionsFn = createFilterOptions(filterOptionsConfig || defaultFilterOptionsConfig); // hook const { open, onOpen, onClose, onChange, onInputChange, inputValue, options, isInitialLoading, isLoading, aPlaceholder, extendOptions, getOptionSelected, getOptionDisabled, getOptionIsCustom, getOptionLabel, selectedListValue, } = useListValuesAutocomplete(props, { debounceTimeout: 100, multiple, uif: "mui", isFieldAutocomplete, dontFixOptionsOrder, }); // settings const {defaultSelectWidth, defaultSearchWidth, renderSize} = config.settings; const {width, ...rest} = customProps || {}; let customInputProps = rest.input || {}; const inputWidth = customInputProps.width || defaultSearchWidth; // todo: use as min-width for Autocomplete comp customInputProps = omit(customInputProps, ["width"]); const customAutocompleteProps = omit(rest, ["showSearch", "showCheckboxes"]); const fullWidth = false; const minWidth = width || defaultSelectWidth; const style = { width: (multiple ? undefined : minWidth), minWidth: minWidth }; const placeholder = !readonly ? aPlaceholder : ""; // For accessibility, always give the input field an aria-label const ariaLabel = placeholder || config.settings.fieldPlaceholder; const hasValue = selectedValue != null; // should be simple value to prevent re-render!s const value = hasValue ? selectedValue : (multiple ? emptyArray : null); const filterOptions = (options, params) => { const filtered = filterOptionsFn(options, params); const extended = extendOptions(filtered); return extended; }; const groupBy = (option) => option?.group ? JSON.stringify(option.group) : option?.groupTitle; const theme = useTheme(); // render const renderInput = (params) => { // parity with Antd const shouldRenderSelected = !multiple && !open; const selectedTitle = selectedListValue?.title ?? value?.toString() ?? ""; const shouldHide = multiple && !open; const renderValue = shouldRenderSelected ? selectedTitle : (shouldHide ? "" : inputValue ?? value?.toString() ?? ""); return ( <TextField variant="standard" {...params} inputProps={{ "aria-label": ariaLabel, ...params.inputProps, value: renderValue, }} InputProps={{ ...params.InputProps, readOnly: readonly, endAdornment: ( <React.Fragment> {isLoading ? <CircularProgress color="inherit" size={20} /> : null} {params.InputProps.endAdornment} </React.Fragment> ), }} size={renderSize} disabled={readonly} placeholder={placeholder} error={!!errorText} //onChange={onInputChange} {...customInputProps} /> ); }; const GroupHeader = ({group}) => { let groupLabel = group.label; if (groupLabel && group.tooltip) { groupLabel = ( <Tooltip title={group.tooltip} placement="left-start"><span>{groupLabel}</span></Tooltip> ); } let res = ( <div style={{ position: "sticky", top: "-8px", padding: "4px 10px", color: theme.palette.primary.main, backgroundColor: theme.palette.background.default, }}> {groupLabel} </div> ); return res; }; const GroupItems = ({children}) => { return <>{children}</>; }; const renderGroup = (params) => { const groupMaybeJson = params.group; let group; if (typeof groupMaybeJson === "string" && groupMaybeJson[0] === "{") { try { group = JSON.parse(groupMaybeJson); } catch (_) { // ignore } } else if (groupMaybeJson) { group = { label: groupMaybeJson, }; } const groups = group ? (group.parentGroups ?? [group]) : []; let res = ( <div key={params.key}> {groups.map((gr) => (<GroupHeader key={gr?.path} group={gr} />))} <GroupItems>{params.children}</GroupItems> </div> ); return res; }; const renderTags = (value, getTagProps) => value.map((option, index) => { return <Chip key={option.value} label={getOptionLabel(option)} size={"small"} variant={getOptionIsCustom(option) ? "outlined" : "filled"} {...getTagProps({ index })} />; }); const isOptionEqualToValue = (option, value) => { return option?.value == value; }; const renderOption = (props, option) => { const { title, renderTitle, value, isHidden, tooltip, group, groupTitle } = option; const isGrouped = groupTitle || group; const isSelected = multiple ? (selectedValue || []).includes(value) : selectedValue == value; const className = getOptionIsCustom(option) ? "customSelectOption" : undefined; const prefix = !isFieldAutocomplete && isGrouped ? "\u00A0\u00A0" : ""; const finalTitle = (renderTitle || prefix + title); let titleSpan = ( <span className={className}> {finalTitle} </span> ); if (tooltip) { titleSpan = ( <Tooltip title={tooltip} placement="left-start">{titleSpan}</Tooltip> ); } if (isHidden) return null; if (option.specialValue) { return <div {...props}>{finalTitle}</div>; } else if (multiple) { const itemContent = isSelected ? ( <><ListItemIcon><Check /></ListItemIcon>{titleSpan}</> ) : ( <ListItemText inset>{titleSpan}</ListItemText> ); return ( <MenuItem {...props} size={"small"} selected={isSelected} >{itemContent}</MenuItem> ); } else { return <div {...props}>{titleSpan}</div>; } }; let res = ( <Autocomplete disableClearable={disableClearable} disableCloseOnSelect={multiple} fullWidth={fullWidth} multiple={multiple} style={style} freeSolo={allowCustomValues} loading={isInitialLoading} open={open} onOpen={onOpen} onClose={onClose} inputValue={inputValue} onInputChange={onInputChange} label={placeholder} onChange={onChange} value={value} disabled={readonly} readOnly={readonly} options={options} groupBy={groupBy} getOptionLabel={getOptionLabel} getOptionDisabled={getOptionDisabled} renderInput={renderInput} renderGroup={renderGroup} renderTags={renderTags} renderOption={renderOption} filterOptions={filterOptions} isOptionEqualToValue={isOptionEqualToValue} size={renderSize} {...customAutocompleteProps} /> ); if (tooltipText) { res = ( <Tooltip title={!open ? tooltipText : null} placement="top-start">{res}</Tooltip> ); } res = ( <FormControl fullWidth={fullWidth}>{res}</FormControl> ); return res; };