2020-02-25 15:07:13 +01:00
|
|
|
import React, { useCallback } from 'react';
|
2020-01-07 09:20:06 +01:00
|
|
|
// @ts-ignore
|
2020-01-28 07:43:18 +01:00
|
|
|
import { default as ReactSelect } from '@torkelo/react-select';
|
2020-01-07 09:20:06 +01:00
|
|
|
// @ts-ignore
|
2020-01-28 07:43:18 +01:00
|
|
|
import Creatable from '@torkelo/react-select/creatable';
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
import { default as ReactAsyncSelect } from '@torkelo/react-select/async';
|
2020-01-28 08:38:21 +01:00
|
|
|
// @ts-ignore
|
|
|
|
|
import { default as AsyncCreatable } from '@torkelo/react-select/async-creatable';
|
2020-01-07 09:20:06 +01:00
|
|
|
|
2020-04-02 10:57:35 +02:00
|
|
|
import { Icon } from '../Icon/Icon';
|
2020-01-28 07:43:18 +01:00
|
|
|
import { css, cx } from 'emotion';
|
2020-01-07 09:20:06 +01:00
|
|
|
import resetSelectStyles from './resetSelectStyles';
|
|
|
|
|
import { SelectMenu, SelectMenuOptions } from './SelectMenu';
|
|
|
|
|
import { IndicatorsContainer } from './IndicatorsContainer';
|
|
|
|
|
import { ValueContainer } from './ValueContainer';
|
|
|
|
|
import { InputControl } from './InputControl';
|
|
|
|
|
import { DropdownIndicator } from './DropdownIndicator';
|
|
|
|
|
import { SelectOptionGroup } from './SelectOptionGroup';
|
|
|
|
|
import { SingleValue } from './SingleValue';
|
|
|
|
|
import { MultiValueContainer, MultiValueRemove } from './MultiValue';
|
2020-04-02 10:57:35 +02:00
|
|
|
import { useTheme } from '../../themes';
|
2020-01-07 09:20:06 +01:00
|
|
|
import { getSelectStyles } from './getSelectStyles';
|
2020-03-03 16:09:52 +02:00
|
|
|
import { cleanValue } from './utils';
|
|
|
|
|
import { SelectBaseProps, SelectValue } from './types';
|
2020-01-07 09:20:06 +01:00
|
|
|
|
2020-04-21 16:06:34 +01:00
|
|
|
interface ExtraValuesIndicatorProps {
|
|
|
|
|
maxVisibleValues?: number | undefined;
|
|
|
|
|
selectedValuesCount: number;
|
|
|
|
|
menuIsOpen: boolean;
|
|
|
|
|
showAllSelectedWhenOpen: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const renderExtraValuesIndicator = (props: ExtraValuesIndicatorProps) => {
|
|
|
|
|
const { maxVisibleValues, selectedValuesCount, menuIsOpen, showAllSelectedWhenOpen } = props;
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
maxVisibleValues !== undefined &&
|
|
|
|
|
selectedValuesCount > maxVisibleValues &&
|
|
|
|
|
!(showAllSelectedWhenOpen && menuIsOpen)
|
|
|
|
|
) {
|
|
|
|
|
return (
|
|
|
|
|
<span key="excess-values" id="excess-values">
|
|
|
|
|
(+{selectedValuesCount - maxVisibleValues})
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
|
2020-01-07 09:20:06 +01:00
|
|
|
const CustomControl = (props: any) => {
|
|
|
|
|
const {
|
|
|
|
|
children,
|
|
|
|
|
innerProps,
|
|
|
|
|
selectProps: { menuIsOpen, onMenuClose, onMenuOpen },
|
|
|
|
|
isFocused,
|
|
|
|
|
isMulti,
|
|
|
|
|
getValue,
|
|
|
|
|
innerRef,
|
|
|
|
|
} = props;
|
|
|
|
|
const selectProps = props.selectProps as SelectBaseProps<any>;
|
|
|
|
|
|
|
|
|
|
if (selectProps.renderControl) {
|
|
|
|
|
return React.createElement(selectProps.renderControl, {
|
|
|
|
|
isOpen: menuIsOpen,
|
|
|
|
|
value: isMulti ? getValue() : getValue()[0],
|
|
|
|
|
ref: innerRef,
|
|
|
|
|
onClick: menuIsOpen ? onMenuClose : onMenuOpen,
|
|
|
|
|
onBlur: onMenuClose,
|
|
|
|
|
disabled: !!selectProps.disabled,
|
|
|
|
|
invalid: !!selectProps.invalid,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<InputControl
|
|
|
|
|
ref={innerRef}
|
|
|
|
|
innerProps={innerProps}
|
|
|
|
|
prefix={selectProps.prefix}
|
|
|
|
|
focused={isFocused}
|
|
|
|
|
invalid={!!selectProps.invalid}
|
|
|
|
|
disabled={!!selectProps.disabled}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</InputControl>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function SelectBase<T>({
|
2020-03-10 08:41:38 +02:00
|
|
|
allowCustomValue = false,
|
|
|
|
|
autoFocus = false,
|
|
|
|
|
backspaceRemovesValue = true,
|
2020-04-25 21:48:20 +01:00
|
|
|
cacheOptions,
|
2020-04-23 08:18:53 +03:00
|
|
|
className,
|
2020-04-21 16:06:34 +01:00
|
|
|
closeMenuOnSelect = true,
|
2020-03-10 08:41:38 +02:00
|
|
|
components,
|
|
|
|
|
defaultOptions,
|
2020-01-07 09:20:06 +01:00
|
|
|
defaultValue,
|
|
|
|
|
disabled = false,
|
2020-03-10 08:41:38 +02:00
|
|
|
formatCreateLabel,
|
|
|
|
|
getOptionLabel,
|
|
|
|
|
getOptionValue,
|
|
|
|
|
inputValue,
|
|
|
|
|
invalid,
|
2020-01-07 09:20:06 +01:00
|
|
|
isClearable = false,
|
|
|
|
|
isLoading = false,
|
2020-03-10 08:41:38 +02:00
|
|
|
isMulti = false,
|
2020-01-07 09:20:06 +01:00
|
|
|
isOpen,
|
2020-04-25 21:48:20 +01:00
|
|
|
isOptionDisabled,
|
2020-03-10 08:41:38 +02:00
|
|
|
isSearchable = true,
|
|
|
|
|
loadOptions,
|
|
|
|
|
loadingMessage = 'Loading options...',
|
2020-01-07 09:20:06 +01:00
|
|
|
maxMenuHeight = 300,
|
2020-04-21 16:06:34 +01:00
|
|
|
maxVisibleValues,
|
2020-04-10 18:03:27 +03:00
|
|
|
menuPlacement = 'auto',
|
2020-04-23 08:18:53 +03:00
|
|
|
menuPosition,
|
2020-01-07 09:20:06 +01:00
|
|
|
noOptionsMessage = 'No options found',
|
2020-03-10 08:41:38 +02:00
|
|
|
onBlur,
|
|
|
|
|
onChange,
|
|
|
|
|
onCloseMenu,
|
|
|
|
|
onCreateOption,
|
|
|
|
|
onInputChange,
|
|
|
|
|
onKeyDown,
|
|
|
|
|
onOpenMenu,
|
|
|
|
|
openMenuOnFocus = false,
|
|
|
|
|
options = [],
|
|
|
|
|
placeholder = 'Choose',
|
2020-01-07 09:20:06 +01:00
|
|
|
prefix,
|
|
|
|
|
renderControl,
|
2020-04-21 16:06:34 +01:00
|
|
|
showAllSelectedWhenOpen = true,
|
2020-03-10 08:41:38 +02:00
|
|
|
tabSelectsValue = true,
|
|
|
|
|
value,
|
2020-01-07 09:20:06 +01:00
|
|
|
width,
|
|
|
|
|
}: SelectBaseProps<T>) {
|
|
|
|
|
const theme = useTheme();
|
|
|
|
|
const styles = getSelectStyles(theme);
|
2020-02-25 15:07:13 +01:00
|
|
|
const onChangeWithEmpty = useCallback(
|
|
|
|
|
(value: SelectValue<T>) => {
|
|
|
|
|
if (isMulti && (value === undefined || value === null)) {
|
|
|
|
|
return onChange([]);
|
|
|
|
|
}
|
|
|
|
|
onChange(value);
|
|
|
|
|
},
|
2020-03-10 08:41:38 +02:00
|
|
|
[isMulti, value, onChange]
|
2020-02-25 15:07:13 +01:00
|
|
|
);
|
2020-01-28 07:43:18 +01:00
|
|
|
let ReactSelectComponent: ReactSelect | Creatable = ReactSelect;
|
2020-01-07 09:20:06 +01:00
|
|
|
const creatableProps: any = {};
|
|
|
|
|
let asyncSelectProps: any = {};
|
|
|
|
|
let selectedValue = [];
|
|
|
|
|
if (isMulti && loadOptions) {
|
|
|
|
|
selectedValue = value as any;
|
|
|
|
|
} else {
|
|
|
|
|
// If option is passed as a plain value (value property from SelectableValue property)
|
|
|
|
|
// we are selecting the corresponding value from the options
|
|
|
|
|
if (isMulti && value && Array.isArray(value) && !loadOptions) {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
selectedValue = value.map(v => {
|
|
|
|
|
return options.filter(o => {
|
|
|
|
|
return v === o.value || o.value === v.value;
|
|
|
|
|
})[0];
|
|
|
|
|
});
|
2020-02-13 10:13:03 +00:00
|
|
|
} else if (loadOptions) {
|
|
|
|
|
const hasValue = defaultValue || value;
|
|
|
|
|
selectedValue = hasValue ? [hasValue] : [];
|
2020-01-07 09:20:06 +01:00
|
|
|
} else {
|
2020-03-03 16:09:52 +02:00
|
|
|
selectedValue = cleanValue(value, options);
|
2020-01-07 09:20:06 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const commonSelectProps = {
|
|
|
|
|
autoFocus,
|
2020-03-10 08:41:38 +02:00
|
|
|
backspaceRemovesValue,
|
|
|
|
|
captureMenuScroll: false,
|
2020-04-21 16:06:34 +01:00
|
|
|
closeMenuOnSelect,
|
2020-03-10 08:41:38 +02:00
|
|
|
defaultValue,
|
2020-01-07 09:20:06 +01:00
|
|
|
// Also passing disabled, as this is the new Select API, and I want to use this prop instead of react-select's one
|
|
|
|
|
disabled,
|
2020-03-10 08:41:38 +02:00
|
|
|
getOptionLabel,
|
|
|
|
|
getOptionValue,
|
|
|
|
|
inputValue,
|
2020-01-07 09:20:06 +01:00
|
|
|
invalid,
|
|
|
|
|
isClearable,
|
2020-03-10 08:41:38 +02:00
|
|
|
// Passing isDisabled as react-select accepts this prop
|
|
|
|
|
isDisabled: disabled,
|
2020-01-07 09:20:06 +01:00
|
|
|
isLoading,
|
2020-03-10 08:41:38 +02:00
|
|
|
isMulti,
|
2020-04-25 21:48:20 +01:00
|
|
|
isOptionDisabled,
|
2020-03-10 08:41:38 +02:00
|
|
|
isSearchable,
|
|
|
|
|
maxMenuHeight,
|
2020-04-21 16:06:34 +01:00
|
|
|
maxVisibleValues,
|
2020-01-07 09:20:06 +01:00
|
|
|
menuIsOpen: isOpen,
|
2020-04-10 18:03:27 +03:00
|
|
|
menuPlacement,
|
2020-03-10 08:41:38 +02:00
|
|
|
menuPosition,
|
|
|
|
|
menuShouldScrollIntoView: false,
|
|
|
|
|
onBlur,
|
|
|
|
|
onChange: onChangeWithEmpty,
|
2020-01-17 11:30:33 +01:00
|
|
|
onInputChange,
|
2020-03-10 08:41:38 +02:00
|
|
|
onKeyDown,
|
2020-01-07 09:20:06 +01:00
|
|
|
onMenuClose: onCloseMenu,
|
2020-03-10 08:41:38 +02:00
|
|
|
onMenuOpen: onOpenMenu,
|
|
|
|
|
openMenuOnFocus,
|
2020-01-07 09:20:06 +01:00
|
|
|
options,
|
2020-03-10 08:41:38 +02:00
|
|
|
placeholder,
|
|
|
|
|
prefix,
|
2020-01-07 09:20:06 +01:00
|
|
|
renderControl,
|
2020-04-21 16:06:34 +01:00
|
|
|
showAllSelectedWhenOpen,
|
2020-03-10 08:41:38 +02:00
|
|
|
tabSelectsValue,
|
|
|
|
|
value: isMulti ? selectedValue : selectedValue[0],
|
2020-01-07 09:20:06 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (allowCustomValue) {
|
2020-01-28 07:43:18 +01:00
|
|
|
ReactSelectComponent = Creatable;
|
2020-01-07 09:20:06 +01:00
|
|
|
creatableProps.formatCreateLabel = formatCreateLabel ?? ((input: string) => `Create: ${input}`);
|
2020-01-31 13:47:34 +01:00
|
|
|
creatableProps.onCreateOption = onCreateOption;
|
2020-01-07 09:20:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Instead of having AsyncSelect, as a separate component we render ReactAsyncSelect
|
|
|
|
|
if (loadOptions) {
|
2020-01-28 08:38:21 +01:00
|
|
|
ReactSelectComponent = allowCustomValue ? AsyncCreatable : ReactAsyncSelect;
|
2020-01-07 09:20:06 +01:00
|
|
|
asyncSelectProps = {
|
|
|
|
|
loadOptions,
|
2020-04-25 21:48:20 +01:00
|
|
|
cacheOptions,
|
2020-01-07 09:20:06 +01:00
|
|
|
defaultOptions,
|
|
|
|
|
};
|
|
|
|
|
}
|
2020-03-03 11:51:17 +01:00
|
|
|
|
2020-01-07 09:20:06 +01:00
|
|
|
return (
|
2020-02-09 13:37:00 +01:00
|
|
|
<>
|
2020-03-03 11:51:17 +01:00
|
|
|
<ReactSelectComponent
|
2020-02-09 13:37:00 +01:00
|
|
|
components={{
|
|
|
|
|
MenuList: SelectMenu,
|
|
|
|
|
Group: SelectOptionGroup,
|
2020-04-21 16:06:34 +01:00
|
|
|
ValueContainer: (props: any) => {
|
|
|
|
|
const { menuIsOpen } = props.selectProps;
|
|
|
|
|
if (
|
|
|
|
|
Array.isArray(props.children) &&
|
|
|
|
|
Array.isArray(props.children[0]) &&
|
|
|
|
|
maxVisibleValues !== undefined &&
|
|
|
|
|
!(showAllSelectedWhenOpen && menuIsOpen)
|
|
|
|
|
) {
|
|
|
|
|
const [valueChildren, ...otherChildren] = props.children;
|
|
|
|
|
const truncatedValues = valueChildren.slice(0, maxVisibleValues);
|
|
|
|
|
|
|
|
|
|
return <ValueContainer {...props} children={[truncatedValues, ...otherChildren]} />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return <ValueContainer {...props} />;
|
|
|
|
|
},
|
2020-02-09 13:37:00 +01:00
|
|
|
Placeholder: (props: any) => (
|
|
|
|
|
<div
|
|
|
|
|
{...props.innerProps}
|
|
|
|
|
className={cx(
|
|
|
|
|
css(props.getStyles('placeholder', props)),
|
|
|
|
|
css`
|
|
|
|
|
display: inline-block;
|
2020-03-11 09:20:28 +01:00
|
|
|
color: ${theme.colors.formInputPlaceholderText};
|
2020-02-09 13:37:00 +01:00
|
|
|
position: absolute;
|
|
|
|
|
top: 50%;
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
line-height: 1;
|
|
|
|
|
`
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{props.children}
|
2020-01-07 09:20:06 +01:00
|
|
|
</div>
|
2020-02-09 13:37:00 +01:00
|
|
|
),
|
2020-04-21 16:06:34 +01:00
|
|
|
IndicatorsContainer: (props: any) => {
|
|
|
|
|
const { selectProps } = props;
|
|
|
|
|
const { value, showAllSelectedWhenOpen, maxVisibleValues, menuIsOpen } = selectProps;
|
|
|
|
|
|
|
|
|
|
if (maxVisibleValues !== undefined) {
|
|
|
|
|
const selectedValuesCount = value.length;
|
|
|
|
|
const indicatorChildren = [...props.children];
|
|
|
|
|
indicatorChildren.splice(
|
|
|
|
|
-1,
|
|
|
|
|
0,
|
|
|
|
|
renderExtraValuesIndicator({
|
|
|
|
|
maxVisibleValues,
|
|
|
|
|
selectedValuesCount,
|
|
|
|
|
showAllSelectedWhenOpen,
|
|
|
|
|
menuIsOpen,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
return <IndicatorsContainer {...props} children={indicatorChildren} />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return <IndicatorsContainer {...props} />;
|
|
|
|
|
},
|
2020-02-09 13:37:00 +01:00
|
|
|
IndicatorSeparator: () => <></>,
|
|
|
|
|
Control: CustomControl,
|
|
|
|
|
Option: SelectMenuOptions,
|
|
|
|
|
ClearIndicator: (props: any) => {
|
|
|
|
|
const { clearValue } = props;
|
|
|
|
|
return (
|
|
|
|
|
<Icon
|
|
|
|
|
name="times"
|
|
|
|
|
onMouseDown={e => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
clearValue();
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
LoadingIndicator: (props: any) => {
|
2020-04-12 22:20:02 +02:00
|
|
|
return <Icon className="fa-spin" name="fa fa-spinner" />;
|
2020-02-09 13:37:00 +01:00
|
|
|
},
|
|
|
|
|
LoadingMessage: (props: any) => {
|
|
|
|
|
return <div className={styles.loadingMessage}>{loadingMessage}</div>;
|
|
|
|
|
},
|
|
|
|
|
NoOptionsMessage: (props: any) => {
|
|
|
|
|
return (
|
|
|
|
|
<div className={styles.loadingMessage} aria-label="No options provided">
|
|
|
|
|
{noOptionsMessage}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
DropdownIndicator: (props: any) => <DropdownIndicator isOpen={props.selectProps.menuIsOpen} />,
|
|
|
|
|
SingleValue: SingleValue,
|
|
|
|
|
MultiValueContainer: MultiValueContainer,
|
|
|
|
|
MultiValueRemove: MultiValueRemove,
|
|
|
|
|
...components,
|
|
|
|
|
}}
|
|
|
|
|
styles={{
|
|
|
|
|
...resetSelectStyles(),
|
2020-04-22 16:18:22 +02:00
|
|
|
menuPortal: () => ({
|
|
|
|
|
zIndex: theme.zIndex.dropdown,
|
|
|
|
|
}),
|
2020-02-09 13:37:00 +01:00
|
|
|
//These are required for the menu positioning to function
|
2020-04-26 21:59:14 +02:00
|
|
|
menu: ({ top, bottom, position }: any) => ({
|
2020-02-09 13:37:00 +01:00
|
|
|
top,
|
|
|
|
|
bottom,
|
|
|
|
|
position,
|
|
|
|
|
marginBottom: !!bottom ? '10px' : '0',
|
2020-04-27 15:08:48 +03:00
|
|
|
minWidth: '100%',
|
2020-04-22 16:18:22 +02:00
|
|
|
zIndex: theme.zIndex.dropdown,
|
2020-02-09 13:37:00 +01:00
|
|
|
}),
|
2020-02-20 07:25:23 +01:00
|
|
|
container: () => ({
|
|
|
|
|
position: 'relative',
|
2020-04-21 10:42:57 +02:00
|
|
|
width: width ? `${8 * width}px` : '100%',
|
2020-02-20 07:25:23 +01:00
|
|
|
}),
|
2020-04-25 21:48:20 +01:00
|
|
|
option: (provided: any, state: any) => ({
|
|
|
|
|
...provided,
|
|
|
|
|
opacity: state.isDisabled ? 0.5 : 1,
|
|
|
|
|
}),
|
2020-02-09 13:37:00 +01:00
|
|
|
}}
|
2020-04-22 13:38:50 +02:00
|
|
|
className={className}
|
2020-02-09 13:37:00 +01:00
|
|
|
{...commonSelectProps}
|
|
|
|
|
{...creatableProps}
|
|
|
|
|
{...asyncSelectProps}
|
|
|
|
|
/>
|
|
|
|
|
</>
|
2020-01-07 09:20:06 +01:00
|
|
|
);
|
|
|
|
|
}
|