refactor: select
parent
54cdc3ff40
commit
1c508d61fb
|
@ -0,0 +1,500 @@
|
||||||
|
export interface ShowSearchType<OptionType extends BaseOptionType = DefaultOptionType> {
|
||||||
|
filter?: (inputValue: string, options: OptionType[], fieldNames: FieldNames) => boolean;
|
||||||
|
render?: (arg?: {
|
||||||
|
inputValue: string;
|
||||||
|
path: OptionType[];
|
||||||
|
prefixCls: string;
|
||||||
|
fieldNames: FieldNames;
|
||||||
|
}) => any;
|
||||||
|
sort?: (a: OptionType[], b: OptionType[], inputValue: string, fieldNames: FieldNames) => number;
|
||||||
|
matchInputWidth?: boolean;
|
||||||
|
limit?: number | false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FieldNames {
|
||||||
|
label?: string;
|
||||||
|
value?: string;
|
||||||
|
children?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InternalFieldNames extends Required<FieldNames> {
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SingleValueType = (string | number)[];
|
||||||
|
|
||||||
|
export type ValueType = SingleValueType | SingleValueType[];
|
||||||
|
|
||||||
|
export interface BaseOptionType {
|
||||||
|
disabled?: boolean;
|
||||||
|
[name: string]: any;
|
||||||
|
}
|
||||||
|
export interface DefaultOptionType extends BaseOptionType {
|
||||||
|
label: React.ReactNode;
|
||||||
|
value?: string | number | null;
|
||||||
|
children?: DefaultOptionType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaseCascaderProps<OptionType extends BaseOptionType = DefaultOptionType>
|
||||||
|
extends Omit<
|
||||||
|
BaseSelectPropsWithoutPrivate,
|
||||||
|
'tokenSeparators' | 'labelInValue' | 'mode' | 'showSearch'
|
||||||
|
> {
|
||||||
|
// MISC
|
||||||
|
id?: string;
|
||||||
|
prefixCls?: string;
|
||||||
|
fieldNames?: FieldNames;
|
||||||
|
children?: React.ReactElement;
|
||||||
|
|
||||||
|
// Value
|
||||||
|
value?: ValueType;
|
||||||
|
defaultValue?: ValueType;
|
||||||
|
changeOnSelect?: boolean;
|
||||||
|
onChange?: (value: ValueType, selectedOptions?: OptionType[] | OptionType[][]) => void;
|
||||||
|
displayRender?: (label: string[], selectedOptions?: OptionType[]) => React.ReactNode;
|
||||||
|
checkable?: boolean | React.ReactNode;
|
||||||
|
|
||||||
|
// Search
|
||||||
|
showSearch?: boolean | ShowSearchType<OptionType>;
|
||||||
|
searchValue?: string;
|
||||||
|
onSearch?: (value: string) => void;
|
||||||
|
|
||||||
|
// Trigger
|
||||||
|
expandTrigger?: 'hover' | 'click';
|
||||||
|
|
||||||
|
// Options
|
||||||
|
options?: OptionType[];
|
||||||
|
/** @private Internal usage. Do not use in your production. */
|
||||||
|
dropdownPrefixCls?: string;
|
||||||
|
loadData?: (selectOptions: OptionType[]) => void;
|
||||||
|
|
||||||
|
// Open
|
||||||
|
/** @deprecated Use `open` instead */
|
||||||
|
popupVisible?: boolean;
|
||||||
|
|
||||||
|
/** @deprecated Use `dropdownClassName` instead */
|
||||||
|
popupClassName?: string;
|
||||||
|
dropdownClassName?: string;
|
||||||
|
dropdownMenuColumnStyle?: React.CSSProperties;
|
||||||
|
|
||||||
|
/** @deprecated Use `placement` instead */
|
||||||
|
popupPlacement?: Placement;
|
||||||
|
placement?: Placement;
|
||||||
|
|
||||||
|
/** @deprecated Use `onDropdownVisibleChange` instead */
|
||||||
|
onPopupVisibleChange?: (open: boolean) => void;
|
||||||
|
onDropdownVisibleChange?: (open: boolean) => void;
|
||||||
|
|
||||||
|
// Icon
|
||||||
|
expandIcon?: React.ReactNode;
|
||||||
|
loadingIcon?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
type OnSingleChange<OptionType> = (value: SingleValueType, selectOptions: OptionType[]) => void;
|
||||||
|
type OnMultipleChange<OptionType> = (
|
||||||
|
value: SingleValueType[],
|
||||||
|
selectOptions: OptionType[][],
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export interface SingleCascaderProps<OptionType extends BaseOptionType = DefaultOptionType>
|
||||||
|
extends BaseCascaderProps<OptionType> {
|
||||||
|
checkable?: false;
|
||||||
|
|
||||||
|
onChange?: OnSingleChange<OptionType>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MultipleCascaderProps<OptionType extends BaseOptionType = DefaultOptionType>
|
||||||
|
extends BaseCascaderProps<OptionType> {
|
||||||
|
checkable: true | React.ReactNode;
|
||||||
|
|
||||||
|
onChange?: OnMultipleChange<OptionType>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CascaderProps<OptionType extends BaseOptionType = DefaultOptionType> =
|
||||||
|
| SingleCascaderProps<OptionType>
|
||||||
|
| MultipleCascaderProps<OptionType>;
|
||||||
|
|
||||||
|
type InternalCascaderProps<OptionType extends BaseOptionType = DefaultOptionType> = Omit<
|
||||||
|
SingleCascaderProps<OptionType> | MultipleCascaderProps<OptionType>,
|
||||||
|
'onChange'
|
||||||
|
> & {
|
||||||
|
onChange?: (
|
||||||
|
value: SingleValueType | SingleValueType[],
|
||||||
|
selectOptions: OptionType[] | OptionType[][],
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CascaderRef = Omit<BaseSelectRef, 'scrollTo'>;
|
||||||
|
|
||||||
|
function isMultipleValue(value: ValueType): value is SingleValueType[] {
|
||||||
|
return Array.isArray(value) && Array.isArray(value[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toRawValues(value: ValueType): SingleValueType[] {
|
||||||
|
if (!value) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMultipleValue(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.length === 0 ? [] : [value];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Cascader = React.forwardRef<CascaderRef, InternalCascaderProps>((props, ref) => {
|
||||||
|
const {
|
||||||
|
// MISC
|
||||||
|
id,
|
||||||
|
prefixCls = 'rc-cascader',
|
||||||
|
fieldNames,
|
||||||
|
|
||||||
|
// Value
|
||||||
|
defaultValue,
|
||||||
|
value,
|
||||||
|
changeOnSelect,
|
||||||
|
onChange,
|
||||||
|
displayRender,
|
||||||
|
checkable,
|
||||||
|
|
||||||
|
// Search
|
||||||
|
searchValue,
|
||||||
|
onSearch,
|
||||||
|
showSearch,
|
||||||
|
|
||||||
|
// Trigger
|
||||||
|
expandTrigger,
|
||||||
|
|
||||||
|
// Options
|
||||||
|
options,
|
||||||
|
dropdownPrefixCls,
|
||||||
|
loadData,
|
||||||
|
|
||||||
|
// Open
|
||||||
|
popupVisible,
|
||||||
|
open,
|
||||||
|
|
||||||
|
popupClassName,
|
||||||
|
dropdownClassName,
|
||||||
|
dropdownMenuColumnStyle,
|
||||||
|
|
||||||
|
popupPlacement,
|
||||||
|
placement,
|
||||||
|
|
||||||
|
onDropdownVisibleChange,
|
||||||
|
onPopupVisibleChange,
|
||||||
|
|
||||||
|
// Icon
|
||||||
|
expandIcon = '>',
|
||||||
|
loadingIcon,
|
||||||
|
|
||||||
|
// Children
|
||||||
|
children,
|
||||||
|
|
||||||
|
...restProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const mergedId = useId(id);
|
||||||
|
const multiple = !!checkable;
|
||||||
|
|
||||||
|
// =========================== Values ===========================
|
||||||
|
const [rawValues, setRawValues] = useMergedState<ValueType, SingleValueType[]>(defaultValue, {
|
||||||
|
value,
|
||||||
|
postState: toRawValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ========================= FieldNames =========================
|
||||||
|
const mergedFieldNames = React.useMemo(
|
||||||
|
() => fillFieldNames(fieldNames),
|
||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
[JSON.stringify(fieldNames)],
|
||||||
|
/* eslint-enable react-hooks/exhaustive-deps */
|
||||||
|
);
|
||||||
|
|
||||||
|
// =========================== Option ===========================
|
||||||
|
const mergedOptions = React.useMemo(() => options || [], [options]);
|
||||||
|
|
||||||
|
// Only used in multiple mode, this fn will not call in single mode
|
||||||
|
const getPathKeyEntities = useEntities(mergedOptions, mergedFieldNames);
|
||||||
|
|
||||||
|
/** Convert path key back to value format */
|
||||||
|
const getValueByKeyPath = React.useCallback(
|
||||||
|
(pathKeys: React.Key[]): SingleValueType[] => {
|
||||||
|
const ketPathEntities = getPathKeyEntities();
|
||||||
|
|
||||||
|
return pathKeys.map(pathKey => {
|
||||||
|
const { nodes } = ketPathEntities[pathKey];
|
||||||
|
|
||||||
|
return nodes.map(node => node[mergedFieldNames.value]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[getPathKeyEntities, mergedFieldNames],
|
||||||
|
);
|
||||||
|
|
||||||
|
// =========================== Search ===========================
|
||||||
|
const [mergedSearchValue, setSearchValue] = useMergedState('', {
|
||||||
|
value: searchValue,
|
||||||
|
postState: search => search || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const onInternalSearch: BaseSelectProps['onSearch'] = (searchText, info) => {
|
||||||
|
setSearchValue(searchText);
|
||||||
|
|
||||||
|
if (info.source !== 'blur' && onSearch) {
|
||||||
|
onSearch(searchText);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const [mergedShowSearch, searchConfig] = useSearchConfig(showSearch);
|
||||||
|
|
||||||
|
const searchOptions = useSearchOptions(
|
||||||
|
mergedSearchValue,
|
||||||
|
mergedOptions,
|
||||||
|
mergedFieldNames,
|
||||||
|
dropdownPrefixCls || prefixCls,
|
||||||
|
searchConfig,
|
||||||
|
changeOnSelect,
|
||||||
|
);
|
||||||
|
|
||||||
|
// =========================== Values ===========================
|
||||||
|
const getMissingValues = useMissingValues(mergedOptions, mergedFieldNames);
|
||||||
|
|
||||||
|
// Fill `rawValues` with checked conduction values
|
||||||
|
const [checkedValues, halfCheckedValues, missingCheckedValues] = React.useMemo(() => {
|
||||||
|
const [existValues, missingValues] = getMissingValues(rawValues);
|
||||||
|
|
||||||
|
if (!multiple || !rawValues.length) {
|
||||||
|
return [existValues, [], missingValues];
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyPathValues = toPathKeys(existValues);
|
||||||
|
const ketPathEntities = getPathKeyEntities();
|
||||||
|
|
||||||
|
const { checkedKeys, halfCheckedKeys } = conductCheck(keyPathValues, true, ketPathEntities);
|
||||||
|
|
||||||
|
// Convert key back to value cells
|
||||||
|
return [getValueByKeyPath(checkedKeys), getValueByKeyPath(halfCheckedKeys), missingValues];
|
||||||
|
}, [multiple, rawValues, getPathKeyEntities, getValueByKeyPath, getMissingValues]);
|
||||||
|
|
||||||
|
const deDuplicatedValues = React.useMemo(() => {
|
||||||
|
const checkedKeys = toPathKeys(checkedValues);
|
||||||
|
const deduplicateKeys = formatStrategyValues(checkedKeys, getPathKeyEntities);
|
||||||
|
|
||||||
|
return [...missingCheckedValues, ...getValueByKeyPath(deduplicateKeys)];
|
||||||
|
}, [checkedValues, getPathKeyEntities, getValueByKeyPath, missingCheckedValues]);
|
||||||
|
|
||||||
|
const displayValues = useDisplayValues(
|
||||||
|
deDuplicatedValues,
|
||||||
|
mergedOptions,
|
||||||
|
mergedFieldNames,
|
||||||
|
multiple,
|
||||||
|
displayRender,
|
||||||
|
);
|
||||||
|
|
||||||
|
// =========================== Change ===========================
|
||||||
|
const triggerChange = useRefFunc((nextValues: ValueType) => {
|
||||||
|
setRawValues(nextValues);
|
||||||
|
|
||||||
|
// Save perf if no need trigger event
|
||||||
|
if (onChange) {
|
||||||
|
const nextRawValues = toRawValues(nextValues);
|
||||||
|
|
||||||
|
const valueOptions = nextRawValues.map(valueCells =>
|
||||||
|
toPathOptions(valueCells, mergedOptions, mergedFieldNames).map(valueOpt => valueOpt.option),
|
||||||
|
);
|
||||||
|
|
||||||
|
const triggerValues = multiple ? nextRawValues : nextRawValues[0];
|
||||||
|
const triggerOptions = multiple ? valueOptions : valueOptions[0];
|
||||||
|
|
||||||
|
onChange(triggerValues, triggerOptions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// =========================== Select ===========================
|
||||||
|
const onInternalSelect = useRefFunc((valuePath: SingleValueType) => {
|
||||||
|
if (!multiple) {
|
||||||
|
triggerChange(valuePath);
|
||||||
|
} else {
|
||||||
|
// Prepare conduct required info
|
||||||
|
const pathKey = toPathKey(valuePath);
|
||||||
|
const checkedPathKeys = toPathKeys(checkedValues);
|
||||||
|
const halfCheckedPathKeys = toPathKeys(halfCheckedValues);
|
||||||
|
|
||||||
|
const existInChecked = checkedPathKeys.includes(pathKey);
|
||||||
|
const existInMissing = missingCheckedValues.some(
|
||||||
|
valueCells => toPathKey(valueCells) === pathKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Do update
|
||||||
|
let nextCheckedValues = checkedValues;
|
||||||
|
let nextMissingValues = missingCheckedValues;
|
||||||
|
|
||||||
|
if (existInMissing && !existInChecked) {
|
||||||
|
// Missing value only do filter
|
||||||
|
nextMissingValues = missingCheckedValues.filter(
|
||||||
|
valueCells => toPathKey(valueCells) !== pathKey,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Update checked key first
|
||||||
|
const nextRawCheckedKeys = existInChecked
|
||||||
|
? checkedPathKeys.filter(key => key !== pathKey)
|
||||||
|
: [...checkedPathKeys, pathKey];
|
||||||
|
|
||||||
|
const pathKeyEntities = getPathKeyEntities();
|
||||||
|
|
||||||
|
// Conduction by selected or not
|
||||||
|
let checkedKeys: React.Key[];
|
||||||
|
if (existInChecked) {
|
||||||
|
({ checkedKeys } = conductCheck(
|
||||||
|
nextRawCheckedKeys,
|
||||||
|
{ checked: false, halfCheckedKeys: halfCheckedPathKeys },
|
||||||
|
pathKeyEntities,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
({ checkedKeys } = conductCheck(nextRawCheckedKeys, true, pathKeyEntities));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Roll up to parent level keys
|
||||||
|
const deDuplicatedKeys = formatStrategyValues(checkedKeys, getPathKeyEntities);
|
||||||
|
nextCheckedValues = getValueByKeyPath(deDuplicatedKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerChange([...nextMissingValues, ...nextCheckedValues]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Display Value change logic
|
||||||
|
const onDisplayValuesChange: BaseSelectProps['onDisplayValuesChange'] = (_, info) => {
|
||||||
|
if (info.type === 'clear') {
|
||||||
|
triggerChange([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cascader do not support `add` type. Only support `remove`
|
||||||
|
const { valueCells } = info.values[0] as DisplayValueType & { valueCells: SingleValueType };
|
||||||
|
onInternalSelect(valueCells);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================ Open ============================
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
warning(
|
||||||
|
!onPopupVisibleChange,
|
||||||
|
'`onPopupVisibleChange` is deprecated. Please use `onDropdownVisibleChange` instead.',
|
||||||
|
);
|
||||||
|
warning(popupVisible === undefined, '`popupVisible` is deprecated. Please use `open` instead.');
|
||||||
|
warning(
|
||||||
|
popupClassName === undefined,
|
||||||
|
'`popupClassName` is deprecated. Please use `dropdownClassName` instead.',
|
||||||
|
);
|
||||||
|
warning(
|
||||||
|
popupPlacement === undefined,
|
||||||
|
'`popupPlacement` is deprecated. Please use `placement` instead.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedOpen = open !== undefined ? open : popupVisible;
|
||||||
|
|
||||||
|
const mergedDropdownClassName = dropdownClassName || popupClassName;
|
||||||
|
|
||||||
|
const mergedPlacement = placement || popupPlacement;
|
||||||
|
|
||||||
|
const onInternalDropdownVisibleChange = (nextVisible: boolean) => {
|
||||||
|
onDropdownVisibleChange?.(nextVisible);
|
||||||
|
onPopupVisibleChange?.(nextVisible);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========================== Context ===========================
|
||||||
|
const cascaderContext = React.useMemo(
|
||||||
|
() => ({
|
||||||
|
options: mergedOptions,
|
||||||
|
fieldNames: mergedFieldNames,
|
||||||
|
values: checkedValues,
|
||||||
|
halfValues: halfCheckedValues,
|
||||||
|
changeOnSelect,
|
||||||
|
onSelect: onInternalSelect,
|
||||||
|
checkable,
|
||||||
|
searchOptions,
|
||||||
|
dropdownPrefixCls,
|
||||||
|
loadData,
|
||||||
|
expandTrigger,
|
||||||
|
expandIcon,
|
||||||
|
loadingIcon,
|
||||||
|
dropdownMenuColumnStyle,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
mergedOptions,
|
||||||
|
mergedFieldNames,
|
||||||
|
checkedValues,
|
||||||
|
halfCheckedValues,
|
||||||
|
changeOnSelect,
|
||||||
|
onInternalSelect,
|
||||||
|
checkable,
|
||||||
|
searchOptions,
|
||||||
|
dropdownPrefixCls,
|
||||||
|
loadData,
|
||||||
|
expandTrigger,
|
||||||
|
expandIcon,
|
||||||
|
loadingIcon,
|
||||||
|
dropdownMenuColumnStyle,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// ==============================================================
|
||||||
|
// == Render ==
|
||||||
|
// ==============================================================
|
||||||
|
const emptyOptions = !(mergedSearchValue ? searchOptions : mergedOptions).length;
|
||||||
|
|
||||||
|
const dropdownStyle: React.CSSProperties =
|
||||||
|
// Search to match width
|
||||||
|
(mergedSearchValue && searchConfig.matchInputWidth) ||
|
||||||
|
// Empty keep the width
|
||||||
|
emptyOptions
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
minWidth: 'auto',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CascaderContext.Provider value={cascaderContext}>
|
||||||
|
<BaseSelect
|
||||||
|
{...restProps}
|
||||||
|
// MISC
|
||||||
|
ref={ref as any}
|
||||||
|
id={mergedId}
|
||||||
|
prefixCls={prefixCls}
|
||||||
|
dropdownMatchSelectWidth={false}
|
||||||
|
dropdownStyle={dropdownStyle}
|
||||||
|
// Value
|
||||||
|
displayValues={displayValues}
|
||||||
|
onDisplayValuesChange={onDisplayValuesChange}
|
||||||
|
mode={multiple ? 'multiple' : undefined}
|
||||||
|
// Search
|
||||||
|
searchValue={mergedSearchValue}
|
||||||
|
onSearch={onInternalSearch}
|
||||||
|
showSearch={mergedShowSearch}
|
||||||
|
// Options
|
||||||
|
OptionList={OptionList}
|
||||||
|
emptyOptions={emptyOptions}
|
||||||
|
// Open
|
||||||
|
open={mergedOpen}
|
||||||
|
dropdownClassName={mergedDropdownClassName}
|
||||||
|
placement={mergedPlacement}
|
||||||
|
onDropdownVisibleChange={onInternalDropdownVisibleChange}
|
||||||
|
// Children
|
||||||
|
getRawInputElement={() => children}
|
||||||
|
/>
|
||||||
|
</CascaderContext.Provider>
|
||||||
|
);
|
||||||
|
}) as (<OptionType extends BaseOptionType | DefaultOptionType = DefaultOptionType>(
|
||||||
|
props: React.PropsWithChildren<CascaderProps<OptionType>> & {
|
||||||
|
ref?: React.Ref<BaseSelectRef>;
|
||||||
|
},
|
||||||
|
) => React.ReactElement) & {
|
||||||
|
displayName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
Cascader.displayName = 'Cascader';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Cascader;
|
|
@ -0,0 +1,44 @@
|
||||||
|
import type { MouseEventHandler } from '../../_util/EventInterface';
|
||||||
|
import { useInjectCascader } from '../context';
|
||||||
|
|
||||||
|
export interface CheckboxProps {
|
||||||
|
prefixCls: string;
|
||||||
|
checked?: boolean;
|
||||||
|
halfChecked?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
onClick?: MouseEventHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Checkbox({
|
||||||
|
prefixCls,
|
||||||
|
checked,
|
||||||
|
halfChecked,
|
||||||
|
disabled,
|
||||||
|
onClick,
|
||||||
|
}: CheckboxProps) {
|
||||||
|
const { slotsContext, checkable } = useInjectCascader();
|
||||||
|
|
||||||
|
const mergedCheckable = checkable.value === undefined ? slotsContext.value.checkable : checkable;
|
||||||
|
const customCheckbox =
|
||||||
|
typeof mergedCheckable === 'function'
|
||||||
|
? mergedCheckable()
|
||||||
|
: typeof mergedCheckable === 'boolean'
|
||||||
|
? null
|
||||||
|
: mergedCheckable;
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
class={{
|
||||||
|
[prefixCls]: true,
|
||||||
|
[`${prefixCls}-checked`]: checked,
|
||||||
|
[`${prefixCls}-indeterminate`]: !checked && halfChecked,
|
||||||
|
[`${prefixCls}-disabled`]: disabled,
|
||||||
|
}}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{customCheckbox}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Checkbox.props = ['prefixCls', 'checked', 'halfChecked', 'disabled', 'onClick'];
|
||||||
|
Checkbox.displayName = 'Checkbox';
|
||||||
|
Checkbox.inheritAttrs = false;
|
|
@ -0,0 +1,157 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { isLeaf, toPathKey } from '../utils/commonUtil';
|
||||||
|
import CascaderContext from '../context';
|
||||||
|
import Checkbox from './Checkbox';
|
||||||
|
import type { DefaultOptionType, SingleValueType } from '../Cascader';
|
||||||
|
import { SEARCH_MARK } from '../hooks/useSearchOptions';
|
||||||
|
|
||||||
|
export interface ColumnProps {
|
||||||
|
prefixCls: string;
|
||||||
|
multiple?: boolean;
|
||||||
|
options: DefaultOptionType[];
|
||||||
|
/** Current Column opened item key */
|
||||||
|
activeValue?: React.Key;
|
||||||
|
/** The value path before current column */
|
||||||
|
prevValuePath: React.Key[];
|
||||||
|
onToggleOpen: (open: boolean) => void;
|
||||||
|
onSelect: (valuePath: SingleValueType, leaf: boolean) => void;
|
||||||
|
onActive: (valuePath: SingleValueType) => void;
|
||||||
|
checkedSet: Set<React.Key>;
|
||||||
|
halfCheckedSet: Set<React.Key>;
|
||||||
|
loadingKeys: React.Key[];
|
||||||
|
isSelectable: (option: DefaultOptionType) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Column({
|
||||||
|
prefixCls,
|
||||||
|
multiple,
|
||||||
|
options,
|
||||||
|
activeValue,
|
||||||
|
prevValuePath,
|
||||||
|
onToggleOpen,
|
||||||
|
onSelect,
|
||||||
|
onActive,
|
||||||
|
checkedSet,
|
||||||
|
halfCheckedSet,
|
||||||
|
loadingKeys,
|
||||||
|
isSelectable,
|
||||||
|
}: ColumnProps) {
|
||||||
|
const menuPrefixCls = `${prefixCls}-menu`;
|
||||||
|
const menuItemPrefixCls = `${prefixCls}-menu-item`;
|
||||||
|
|
||||||
|
const {
|
||||||
|
fieldNames,
|
||||||
|
changeOnSelect,
|
||||||
|
expandTrigger,
|
||||||
|
expandIcon,
|
||||||
|
loadingIcon,
|
||||||
|
dropdownMenuColumnStyle,
|
||||||
|
} = React.useContext(CascaderContext);
|
||||||
|
|
||||||
|
const hoverOpen = expandTrigger === 'hover';
|
||||||
|
|
||||||
|
// ============================ Render ============================
|
||||||
|
return (
|
||||||
|
<ul className={menuPrefixCls} role="menu">
|
||||||
|
{options.map(option => {
|
||||||
|
const { disabled } = option;
|
||||||
|
const searchOptions = option[SEARCH_MARK];
|
||||||
|
const label = option[fieldNames.label];
|
||||||
|
const value = option[fieldNames.value];
|
||||||
|
|
||||||
|
const isMergedLeaf = isLeaf(option, fieldNames);
|
||||||
|
|
||||||
|
// Get real value of option. Search option is different way.
|
||||||
|
const fullPath = searchOptions
|
||||||
|
? searchOptions.map(opt => opt[fieldNames.value])
|
||||||
|
: [...prevValuePath, value];
|
||||||
|
const fullPathKey = toPathKey(fullPath);
|
||||||
|
|
||||||
|
const isLoading = loadingKeys.includes(fullPathKey);
|
||||||
|
|
||||||
|
// >>>>> checked
|
||||||
|
const checked = checkedSet.has(fullPathKey);
|
||||||
|
|
||||||
|
// >>>>> halfChecked
|
||||||
|
const halfChecked = halfCheckedSet.has(fullPathKey);
|
||||||
|
|
||||||
|
// >>>>> Open
|
||||||
|
const triggerOpenPath = () => {
|
||||||
|
if (!disabled && (!hoverOpen || !isMergedLeaf)) {
|
||||||
|
onActive(fullPath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// >>>>> Selection
|
||||||
|
const triggerSelect = () => {
|
||||||
|
if (isSelectable(option)) {
|
||||||
|
onSelect(fullPath, isMergedLeaf);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// >>>>> Title
|
||||||
|
let title: string;
|
||||||
|
if (typeof option.title === 'string') {
|
||||||
|
title = option.title;
|
||||||
|
} else if (typeof label === 'string') {
|
||||||
|
title = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
// >>>>> Render
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={fullPathKey}
|
||||||
|
className={classNames(menuItemPrefixCls, {
|
||||||
|
[`${menuItemPrefixCls}-expand`]: !isMergedLeaf,
|
||||||
|
[`${menuItemPrefixCls}-active`]: activeValue === value,
|
||||||
|
[`${menuItemPrefixCls}-disabled`]: disabled,
|
||||||
|
[`${menuItemPrefixCls}-loading`]: isLoading,
|
||||||
|
})}
|
||||||
|
style={dropdownMenuColumnStyle}
|
||||||
|
role="menuitemcheckbox"
|
||||||
|
title={title}
|
||||||
|
aria-checked={checked}
|
||||||
|
data-path-key={fullPathKey}
|
||||||
|
onClick={() => {
|
||||||
|
triggerOpenPath();
|
||||||
|
if (!multiple || isMergedLeaf) {
|
||||||
|
triggerSelect();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onDoubleClick={() => {
|
||||||
|
if (changeOnSelect) {
|
||||||
|
onToggleOpen(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
if (hoverOpen) {
|
||||||
|
triggerOpenPath();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{multiple && (
|
||||||
|
<Checkbox
|
||||||
|
prefixCls={`${prefixCls}-checkbox`}
|
||||||
|
checked={checked}
|
||||||
|
halfChecked={halfChecked}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={(e: React.MouseEvent<HTMLSpanElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
triggerSelect();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className={`${menuItemPrefixCls}-content`}>{option[fieldNames.label]}</div>
|
||||||
|
{!isLoading && expandIcon && !isMergedLeaf && (
|
||||||
|
<div className={`${menuItemPrefixCls}-expand-icon`}>{expandIcon}</div>
|
||||||
|
)}
|
||||||
|
{isLoading && loadingIcon && (
|
||||||
|
<div className={`${menuItemPrefixCls}-loading-icon`}>{loadingIcon}</div>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,213 @@
|
||||||
|
/* eslint-disable default-case */
|
||||||
|
import * as React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useBaseProps } from 'rc-select';
|
||||||
|
import type { RefOptionListProps } from 'rc-select/lib/OptionList';
|
||||||
|
import Column from './Column';
|
||||||
|
import CascaderContext from '../context';
|
||||||
|
import type { DefaultOptionType, SingleValueType } from '../Cascader';
|
||||||
|
import { isLeaf, toPathKey, toPathKeys, toPathValueStr } from '../utils/commonUtil';
|
||||||
|
import useActive from './useActive';
|
||||||
|
import useKeyboard from './useKeyboard';
|
||||||
|
import { toPathOptions } from '../utils/treeUtil';
|
||||||
|
|
||||||
|
const RefOptionList = React.forwardRef<RefOptionListProps>((props, ref) => {
|
||||||
|
const { prefixCls, multiple, searchValue, toggleOpen, notFoundContent, direction } =
|
||||||
|
useBaseProps();
|
||||||
|
|
||||||
|
const containerRef = React.useRef<HTMLDivElement>();
|
||||||
|
const rtl = direction === 'rtl';
|
||||||
|
|
||||||
|
const {
|
||||||
|
options,
|
||||||
|
values,
|
||||||
|
halfValues,
|
||||||
|
fieldNames,
|
||||||
|
changeOnSelect,
|
||||||
|
onSelect,
|
||||||
|
searchOptions,
|
||||||
|
dropdownPrefixCls,
|
||||||
|
loadData,
|
||||||
|
expandTrigger,
|
||||||
|
} = React.useContext(CascaderContext);
|
||||||
|
|
||||||
|
const mergedPrefixCls = dropdownPrefixCls || prefixCls;
|
||||||
|
|
||||||
|
// ========================= loadData =========================
|
||||||
|
const [loadingKeys, setLoadingKeys] = React.useState([]);
|
||||||
|
|
||||||
|
const internalLoadData = (valueCells: React.Key[]) => {
|
||||||
|
// Do not load when search
|
||||||
|
if (!loadData || searchValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const optionList = toPathOptions(valueCells, options, fieldNames);
|
||||||
|
const rawOptions = optionList.map(({ option }) => option);
|
||||||
|
const lastOption = rawOptions[rawOptions.length - 1];
|
||||||
|
|
||||||
|
if (lastOption && !isLeaf(lastOption, fieldNames)) {
|
||||||
|
const pathKey = toPathKey(valueCells);
|
||||||
|
|
||||||
|
setLoadingKeys(keys => [...keys, pathKey]);
|
||||||
|
|
||||||
|
loadData(rawOptions);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// zombieJ: This is bad. We should make this same as `rc-tree` to use Promise instead.
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (loadingKeys.length) {
|
||||||
|
loadingKeys.forEach(loadingKey => {
|
||||||
|
const valueStrCells = toPathValueStr(loadingKey);
|
||||||
|
const optionList = toPathOptions(valueStrCells, options, fieldNames, true).map(
|
||||||
|
({ option }) => option,
|
||||||
|
);
|
||||||
|
const lastOption = optionList[optionList.length - 1];
|
||||||
|
|
||||||
|
if (!lastOption || lastOption[fieldNames.children] || isLeaf(lastOption, fieldNames)) {
|
||||||
|
setLoadingKeys(keys => keys.filter(key => key !== loadingKey));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [options, loadingKeys, fieldNames]);
|
||||||
|
|
||||||
|
// ========================== Values ==========================
|
||||||
|
const checkedSet = React.useMemo(() => new Set(toPathKeys(values)), [values]);
|
||||||
|
const halfCheckedSet = React.useMemo(() => new Set(toPathKeys(halfValues)), [halfValues]);
|
||||||
|
|
||||||
|
// ====================== Accessibility =======================
|
||||||
|
const [activeValueCells, setActiveValueCells] = useActive();
|
||||||
|
|
||||||
|
// =========================== Path ===========================
|
||||||
|
const onPathOpen = (nextValueCells: React.Key[]) => {
|
||||||
|
setActiveValueCells(nextValueCells);
|
||||||
|
|
||||||
|
// Trigger loadData
|
||||||
|
internalLoadData(nextValueCells);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSelectable = (option: DefaultOptionType) => {
|
||||||
|
const { disabled } = option;
|
||||||
|
|
||||||
|
const isMergedLeaf = isLeaf(option, fieldNames);
|
||||||
|
return !disabled && (isMergedLeaf || changeOnSelect || multiple);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPathSelect = (valuePath: SingleValueType, leaf: boolean, fromKeyboard = false) => {
|
||||||
|
onSelect(valuePath);
|
||||||
|
|
||||||
|
if (!multiple && (leaf || (changeOnSelect && (expandTrigger === 'hover' || fromKeyboard)))) {
|
||||||
|
toggleOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========================== Option ==========================
|
||||||
|
const mergedOptions = React.useMemo(() => {
|
||||||
|
if (searchValue) {
|
||||||
|
return searchOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}, [searchValue, searchOptions, options]);
|
||||||
|
|
||||||
|
// ========================== Column ==========================
|
||||||
|
const optionColumns = React.useMemo(() => {
|
||||||
|
const optionList = [{ options: mergedOptions }];
|
||||||
|
let currentList = mergedOptions;
|
||||||
|
|
||||||
|
for (let i = 0; i < activeValueCells.length; i += 1) {
|
||||||
|
const activeValueCell = activeValueCells[i];
|
||||||
|
const currentOption = currentList.find(
|
||||||
|
option => option[fieldNames.value] === activeValueCell,
|
||||||
|
);
|
||||||
|
|
||||||
|
const subOptions = currentOption?.[fieldNames.children];
|
||||||
|
if (!subOptions?.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentList = subOptions;
|
||||||
|
optionList.push({ options: subOptions });
|
||||||
|
}
|
||||||
|
|
||||||
|
return optionList;
|
||||||
|
}, [mergedOptions, activeValueCells, fieldNames]);
|
||||||
|
|
||||||
|
// ========================= Keyboard =========================
|
||||||
|
const onKeyboardSelect = (selectValueCells: SingleValueType, option: DefaultOptionType) => {
|
||||||
|
if (isSelectable(option)) {
|
||||||
|
onPathSelect(selectValueCells, isLeaf(option, fieldNames), true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useKeyboard(
|
||||||
|
ref,
|
||||||
|
mergedOptions,
|
||||||
|
fieldNames,
|
||||||
|
activeValueCells,
|
||||||
|
onPathOpen,
|
||||||
|
containerRef,
|
||||||
|
onKeyboardSelect,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ========================== Render ==========================
|
||||||
|
// >>>>> Empty
|
||||||
|
const isEmpty = !optionColumns[0]?.options?.length;
|
||||||
|
|
||||||
|
const emptyList: DefaultOptionType[] = [
|
||||||
|
{
|
||||||
|
[fieldNames.label as 'label']: notFoundContent,
|
||||||
|
[fieldNames.value as 'value']: '__EMPTY__',
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const columnProps = {
|
||||||
|
...props,
|
||||||
|
multiple: !isEmpty && multiple,
|
||||||
|
onSelect: onPathSelect,
|
||||||
|
onActive: onPathOpen,
|
||||||
|
onToggleOpen: toggleOpen,
|
||||||
|
checkedSet,
|
||||||
|
halfCheckedSet,
|
||||||
|
loadingKeys,
|
||||||
|
isSelectable,
|
||||||
|
};
|
||||||
|
|
||||||
|
// >>>>> Columns
|
||||||
|
const mergedOptionColumns = isEmpty ? [{ options: emptyList }] : optionColumns;
|
||||||
|
|
||||||
|
const columnNodes: React.ReactElement[] = mergedOptionColumns.map((col, index) => {
|
||||||
|
const prevValuePath = activeValueCells.slice(0, index);
|
||||||
|
const activeValue = activeValueCells[index];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column
|
||||||
|
key={index}
|
||||||
|
{...columnProps}
|
||||||
|
prefixCls={mergedPrefixCls}
|
||||||
|
options={col.options}
|
||||||
|
prevValuePath={prevValuePath}
|
||||||
|
activeValue={activeValue}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// >>>>> Render
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={classNames(`${mergedPrefixCls}-menus`, {
|
||||||
|
[`${mergedPrefixCls}-menu-empty`]: isEmpty,
|
||||||
|
[`${mergedPrefixCls}-rtl`]: rtl,
|
||||||
|
})}
|
||||||
|
ref={containerRef}
|
||||||
|
>
|
||||||
|
{columnNodes}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default RefOptionList;
|
|
@ -0,0 +1,29 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import CascaderContext from '../context';
|
||||||
|
import { useBaseProps } from 'rc-select';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Control the active open options path.
|
||||||
|
*/
|
||||||
|
export default (): [React.Key[], (activeValueCells: React.Key[]) => void] => {
|
||||||
|
const { multiple, open } = useBaseProps();
|
||||||
|
const { values } = React.useContext(CascaderContext);
|
||||||
|
|
||||||
|
// Record current dropdown active options
|
||||||
|
// This also control the open status
|
||||||
|
const [activeValueCells, setActiveValueCells] = React.useState<React.Key[]>([]);
|
||||||
|
|
||||||
|
React.useEffect(
|
||||||
|
() => {
|
||||||
|
if (open && !multiple) {
|
||||||
|
const firstValueCells = values[0];
|
||||||
|
setActiveValueCells(firstValueCells || []);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
[open],
|
||||||
|
/* eslint-enable react-hooks/exhaustive-deps */
|
||||||
|
);
|
||||||
|
|
||||||
|
return [activeValueCells, setActiveValueCells];
|
||||||
|
};
|
|
@ -0,0 +1,176 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import type { RefOptionListProps } from 'rc-select/lib/OptionList';
|
||||||
|
import KeyCode from 'rc-util/lib/KeyCode';
|
||||||
|
import type { DefaultOptionType, InternalFieldNames, SingleValueType } from '../Cascader';
|
||||||
|
import { toPathKey } from '../utils/commonUtil';
|
||||||
|
import { useBaseProps } from 'rc-select';
|
||||||
|
|
||||||
|
export default (
|
||||||
|
ref: React.Ref<RefOptionListProps>,
|
||||||
|
options: DefaultOptionType[],
|
||||||
|
fieldNames: InternalFieldNames,
|
||||||
|
activeValueCells: React.Key[],
|
||||||
|
setActiveValueCells: (activeValueCells: React.Key[]) => void,
|
||||||
|
containerRef: React.RefObject<HTMLElement>,
|
||||||
|
onKeyBoardSelect: (valueCells: SingleValueType, option: DefaultOptionType) => void,
|
||||||
|
) => {
|
||||||
|
const { direction, searchValue, toggleOpen, open } = useBaseProps();
|
||||||
|
const rtl = direction === 'rtl';
|
||||||
|
|
||||||
|
const [validActiveValueCells, lastActiveIndex, lastActiveOptions] = React.useMemo(() => {
|
||||||
|
let activeIndex = -1;
|
||||||
|
let currentOptions = options;
|
||||||
|
|
||||||
|
const mergedActiveIndexes: number[] = [];
|
||||||
|
const mergedActiveValueCells: React.Key[] = [];
|
||||||
|
|
||||||
|
const len = activeValueCells.length;
|
||||||
|
|
||||||
|
// Fill validate active value cells and index
|
||||||
|
for (let i = 0; i < len; i += 1) {
|
||||||
|
// Mark the active index for current options
|
||||||
|
const nextActiveIndex = currentOptions.findIndex(
|
||||||
|
option => option[fieldNames.value] === activeValueCells[i],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nextActiveIndex === -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeIndex = nextActiveIndex;
|
||||||
|
mergedActiveIndexes.push(activeIndex);
|
||||||
|
mergedActiveValueCells.push(activeValueCells[i]);
|
||||||
|
|
||||||
|
currentOptions = currentOptions[activeIndex][fieldNames.children];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill last active options
|
||||||
|
let activeOptions = options;
|
||||||
|
for (let i = 0; i < mergedActiveIndexes.length - 1; i += 1) {
|
||||||
|
activeOptions = activeOptions[mergedActiveIndexes[i]][fieldNames.children];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [mergedActiveValueCells, activeIndex, activeOptions];
|
||||||
|
}, [activeValueCells, fieldNames, options]);
|
||||||
|
|
||||||
|
// Update active value cells and scroll to target element
|
||||||
|
const internalSetActiveValueCells = (next: React.Key[]) => {
|
||||||
|
setActiveValueCells(next);
|
||||||
|
|
||||||
|
const ele = containerRef.current?.querySelector(`li[data-path-key="${toPathKey(next)}"]`);
|
||||||
|
ele?.scrollIntoView?.({ block: 'nearest' });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Same options offset
|
||||||
|
const offsetActiveOption = (offset: number) => {
|
||||||
|
const len = lastActiveOptions.length;
|
||||||
|
|
||||||
|
let currentIndex = lastActiveIndex;
|
||||||
|
if (currentIndex === -1 && offset < 0) {
|
||||||
|
currentIndex = len;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < len; i += 1) {
|
||||||
|
currentIndex = (currentIndex + offset + len) % len;
|
||||||
|
const option = lastActiveOptions[currentIndex];
|
||||||
|
|
||||||
|
if (option && !option.disabled) {
|
||||||
|
const value = option[fieldNames.value];
|
||||||
|
const nextActiveCells = validActiveValueCells.slice(0, -1).concat(value);
|
||||||
|
internalSetActiveValueCells(nextActiveCells);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Different options offset
|
||||||
|
const prevColumn = () => {
|
||||||
|
if (validActiveValueCells.length > 1) {
|
||||||
|
const nextActiveCells = validActiveValueCells.slice(0, -1);
|
||||||
|
internalSetActiveValueCells(nextActiveCells);
|
||||||
|
} else {
|
||||||
|
toggleOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextColumn = () => {
|
||||||
|
const nextOptions: DefaultOptionType[] =
|
||||||
|
lastActiveOptions[lastActiveIndex]?.[fieldNames.children] || [];
|
||||||
|
|
||||||
|
const nextOption = nextOptions.find(option => !option.disabled);
|
||||||
|
|
||||||
|
if (nextOption) {
|
||||||
|
const nextActiveCells = [...validActiveValueCells, nextOption[fieldNames.value]];
|
||||||
|
internalSetActiveValueCells(nextActiveCells);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useImperativeHandle(ref, () => ({
|
||||||
|
// scrollTo: treeRef.current?.scrollTo,
|
||||||
|
onKeyDown: event => {
|
||||||
|
const { which } = event;
|
||||||
|
|
||||||
|
switch (which) {
|
||||||
|
// >>> Arrow keys
|
||||||
|
case KeyCode.UP:
|
||||||
|
case KeyCode.DOWN: {
|
||||||
|
let offset = 0;
|
||||||
|
if (which === KeyCode.UP) {
|
||||||
|
offset = -1;
|
||||||
|
} else if (which === KeyCode.DOWN) {
|
||||||
|
offset = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset !== 0) {
|
||||||
|
offsetActiveOption(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case KeyCode.LEFT: {
|
||||||
|
if (rtl) {
|
||||||
|
nextColumn();
|
||||||
|
} else {
|
||||||
|
prevColumn();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case KeyCode.RIGHT: {
|
||||||
|
if (rtl) {
|
||||||
|
prevColumn();
|
||||||
|
} else {
|
||||||
|
nextColumn();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case KeyCode.BACKSPACE: {
|
||||||
|
if (!searchValue) {
|
||||||
|
prevColumn();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// >>> Select
|
||||||
|
case KeyCode.ENTER: {
|
||||||
|
if (validActiveValueCells.length) {
|
||||||
|
onKeyBoardSelect(validActiveValueCells, lastActiveOptions[lastActiveIndex]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// >>> Close
|
||||||
|
case KeyCode.ESC: {
|
||||||
|
toggleOpen(false);
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onKeyUp: () => {},
|
||||||
|
}));
|
||||||
|
};
|
|
@ -0,0 +1,36 @@
|
||||||
|
import type { CSSProperties, InjectionKey, Ref } from 'vue';
|
||||||
|
import { inject, provide } from 'vue';
|
||||||
|
import type { VueNode } from '../_util/type';
|
||||||
|
import type {
|
||||||
|
CascaderProps,
|
||||||
|
InternalFieldNames,
|
||||||
|
DefaultOptionType,
|
||||||
|
SingleValueType,
|
||||||
|
} from './Cascader';
|
||||||
|
|
||||||
|
export interface CascaderContextProps {
|
||||||
|
options: Ref<CascaderProps['options']>;
|
||||||
|
fieldNames: Ref<InternalFieldNames>;
|
||||||
|
values: Ref<SingleValueType[]>;
|
||||||
|
halfValues: Ref<SingleValueType[]>;
|
||||||
|
changeOnSelect: Ref<boolean>;
|
||||||
|
onSelect: (valuePath: SingleValueType) => void;
|
||||||
|
checkable: Ref<boolean | VueNode>;
|
||||||
|
searchOptions: Ref<DefaultOptionType[]>;
|
||||||
|
dropdownPrefixCls?: Ref<string>;
|
||||||
|
loadData: Ref<(selectOptions: DefaultOptionType[]) => void>;
|
||||||
|
expandTrigger: Ref<'hover' | 'click'>;
|
||||||
|
expandIcon: Ref<VueNode>;
|
||||||
|
loadingIcon: Ref<VueNode>;
|
||||||
|
dropdownMenuColumnStyle: Ref<CSSProperties>;
|
||||||
|
slotsContext: Ref<Record<string, Function>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CascaderContextKey: InjectionKey<CascaderContextProps> = Symbol('CascaderContextKey');
|
||||||
|
export const useProvideCascader = (props: CascaderContextProps) => {
|
||||||
|
provide(CascaderContextKey, props);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useInjectCascader = () => {
|
||||||
|
return inject(CascaderContextKey);
|
||||||
|
};
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { toPathOptions } from '../utils/treeUtil';
|
||||||
|
import * as React from 'react';
|
||||||
|
import type {
|
||||||
|
DefaultOptionType,
|
||||||
|
SingleValueType,
|
||||||
|
CascaderProps,
|
||||||
|
InternalFieldNames,
|
||||||
|
} from '../Cascader';
|
||||||
|
import { toPathKey } from '../utils/commonUtil';
|
||||||
|
|
||||||
|
export default (
|
||||||
|
rawValues: SingleValueType[],
|
||||||
|
options: DefaultOptionType[],
|
||||||
|
fieldNames: InternalFieldNames,
|
||||||
|
multiple: boolean,
|
||||||
|
displayRender: CascaderProps['displayRender'],
|
||||||
|
) => {
|
||||||
|
return React.useMemo(() => {
|
||||||
|
const mergedDisplayRender =
|
||||||
|
displayRender ||
|
||||||
|
// Default displayRender
|
||||||
|
(labels => {
|
||||||
|
const mergedLabels = multiple ? labels.slice(-1) : labels;
|
||||||
|
const SPLIT = ' / ';
|
||||||
|
|
||||||
|
if (mergedLabels.every(label => ['string', 'number'].includes(typeof label))) {
|
||||||
|
return mergedLabels.join(SPLIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If exist non-string value, use ReactNode instead
|
||||||
|
return mergedLabels.reduce((list, label, index) => {
|
||||||
|
const keyedLabel = React.isValidElement(label)
|
||||||
|
? React.cloneElement(label, { key: index })
|
||||||
|
: label;
|
||||||
|
|
||||||
|
if (index === 0) {
|
||||||
|
return [keyedLabel];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...list, SPLIT, keyedLabel];
|
||||||
|
}, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
return rawValues.map(valueCells => {
|
||||||
|
const valueOptions = toPathOptions(valueCells, options, fieldNames);
|
||||||
|
|
||||||
|
const label = mergedDisplayRender(
|
||||||
|
valueOptions.map(({ option, value }) => option?.[fieldNames.label] ?? value),
|
||||||
|
valueOptions.map(({ option }) => option),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
value: toPathKey(valueCells),
|
||||||
|
valueCells,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [rawValues, options, fieldNames, displayRender, multiple]);
|
||||||
|
};
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { convertDataToEntities } from '../../vc-tree/utils/treeUtil';
|
||||||
|
import type { DataEntity } from '../../vc-tree/interface';
|
||||||
|
import type { DefaultOptionType, InternalFieldNames } from '../Cascader';
|
||||||
|
import { VALUE_SPLIT } from '../utils/commonUtil';
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
export interface OptionsInfo {
|
||||||
|
keyEntities: Record<string, DataEntity>;
|
||||||
|
pathKeyEntities: Record<string, DataEntity>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetEntities = () => OptionsInfo['pathKeyEntities'];
|
||||||
|
|
||||||
|
/** Lazy parse options data into conduct-able info to avoid perf issue in single mode */
|
||||||
|
export default (options: Ref<DefaultOptionType[]>, fieldNames: Ref<InternalFieldNames>) => {
|
||||||
|
const entities = computed(() => {
|
||||||
|
return (
|
||||||
|
convertDataToEntities(options as any, {
|
||||||
|
fieldNames: fieldNames.value,
|
||||||
|
initWrapper: wrapper => ({
|
||||||
|
...wrapper,
|
||||||
|
pathKeyEntities: {},
|
||||||
|
}),
|
||||||
|
processEntity: (entity, wrapper: any) => {
|
||||||
|
const pathKey = entity.nodes.map(node => node[fieldNames.value.value]).join(VALUE_SPLIT);
|
||||||
|
|
||||||
|
wrapper.pathKeyEntities[pathKey] = entity;
|
||||||
|
|
||||||
|
// Overwrite origin key.
|
||||||
|
// this is very hack but we need let conduct logic work with connect path
|
||||||
|
entity.key = pathKey;
|
||||||
|
},
|
||||||
|
}) as any
|
||||||
|
).pathKeyEntities;
|
||||||
|
});
|
||||||
|
return entities;
|
||||||
|
};
|
|
@ -0,0 +1,26 @@
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import type { SingleValueType, DefaultOptionType, InternalFieldNames } from '../Cascader';
|
||||||
|
import { toPathOptions } from '../utils/treeUtil';
|
||||||
|
|
||||||
|
export default (
|
||||||
|
options: Ref<DefaultOptionType[]>,
|
||||||
|
fieldNames: Ref<InternalFieldNames>,
|
||||||
|
rawValues: Ref<SingleValueType[]>,
|
||||||
|
) => {
|
||||||
|
return computed(() => {
|
||||||
|
const missingValues: SingleValueType[] = [];
|
||||||
|
const existsValues: SingleValueType[] = [];
|
||||||
|
|
||||||
|
rawValues.value.forEach(valueCell => {
|
||||||
|
const pathOptions = toPathOptions(valueCell, options.value, fieldNames.value);
|
||||||
|
if (pathOptions.every(opt => opt.option)) {
|
||||||
|
existsValues.push(valueCell);
|
||||||
|
} else {
|
||||||
|
missingValues.push(valueCell);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return [existsValues, missingValues];
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,35 @@
|
||||||
|
import type { CascaderProps, ShowSearchType } from '../Cascader';
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { warning } from '../../vc-util/warning';
|
||||||
|
|
||||||
|
// Convert `showSearch` to unique config
|
||||||
|
export default function useSearchConfig(showSearch?: Ref<CascaderProps['showSearch']>) {
|
||||||
|
return computed(() => {
|
||||||
|
if (!showSearch.value) {
|
||||||
|
return [false, {}];
|
||||||
|
}
|
||||||
|
|
||||||
|
let searchConfig: ShowSearchType = {
|
||||||
|
matchInputWidth: true,
|
||||||
|
limit: 50,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (showSearch.value && typeof showSearch.value === 'object') {
|
||||||
|
searchConfig = {
|
||||||
|
...searchConfig,
|
||||||
|
...showSearch.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchConfig.limit <= 0) {
|
||||||
|
delete searchConfig.limit;
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
warning(false, "'limit' of showSearch should be positive number or false.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [true, searchConfig];
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import type { DefaultOptionType, ShowSearchType, InternalFieldNames } from '../Cascader';
|
||||||
|
|
||||||
|
export const SEARCH_MARK = '__rc_cascader_search_mark__';
|
||||||
|
|
||||||
|
const defaultFilter: ShowSearchType['filter'] = (search, options, { label }) =>
|
||||||
|
options.some(opt => String(opt[label]).toLowerCase().includes(search.toLowerCase()));
|
||||||
|
|
||||||
|
const defaultRender: ShowSearchType['render'] = ({ path, fieldNames }) =>
|
||||||
|
path.map(opt => opt[fieldNames.label]).join(' / ');
|
||||||
|
|
||||||
|
export default (
|
||||||
|
search: Ref<string>,
|
||||||
|
options: Ref<DefaultOptionType[]>,
|
||||||
|
fieldNames: Ref<InternalFieldNames>,
|
||||||
|
prefixCls: Ref<string>,
|
||||||
|
config: Ref<ShowSearchType>,
|
||||||
|
changeOnSelect: Ref<boolean>,
|
||||||
|
) => {
|
||||||
|
return computed(() => {
|
||||||
|
const { filter = defaultFilter, render = defaultRender, limit = 50, sort } = config.value;
|
||||||
|
const filteredOptions: DefaultOptionType[] = [];
|
||||||
|
if (!search.value) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function dig(list: DefaultOptionType[], pathOptions: DefaultOptionType[]) {
|
||||||
|
list.forEach(option => {
|
||||||
|
// Perf saving when `sort` is disabled and `limit` is provided
|
||||||
|
if (!sort && limit > 0 && filteredOptions.length >= limit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectedPathOptions = [...pathOptions, option];
|
||||||
|
const children = option[fieldNames.value.children];
|
||||||
|
|
||||||
|
// If current option is filterable
|
||||||
|
if (
|
||||||
|
// If is leaf option
|
||||||
|
!children ||
|
||||||
|
// If is changeOnSelect
|
||||||
|
changeOnSelect.value
|
||||||
|
) {
|
||||||
|
if (filter(search.value, connectedPathOptions, { label: fieldNames.value.label })) {
|
||||||
|
filteredOptions.push({
|
||||||
|
...option,
|
||||||
|
[fieldNames.value.label as 'label']: render({
|
||||||
|
inputValue: search.value,
|
||||||
|
path: connectedPathOptions,
|
||||||
|
prefixCls: prefixCls.value,
|
||||||
|
fieldNames: fieldNames.value,
|
||||||
|
}),
|
||||||
|
[SEARCH_MARK]: connectedPathOptions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (children) {
|
||||||
|
dig(option[fieldNames.value.children] as DefaultOptionType[], connectedPathOptions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dig(options.value, []);
|
||||||
|
|
||||||
|
// Do sort
|
||||||
|
if (sort) {
|
||||||
|
filteredOptions.sort((a, b) => {
|
||||||
|
return sort(a[SEARCH_MARK], b[SEARCH_MARK], search.value, fieldNames.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return limit > 0 ? filteredOptions.slice(0, limit as number) : filteredOptions;
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,35 @@
|
||||||
|
import type {
|
||||||
|
DefaultOptionType,
|
||||||
|
FieldNames,
|
||||||
|
InternalFieldNames,
|
||||||
|
SingleValueType,
|
||||||
|
} from '../Cascader';
|
||||||
|
|
||||||
|
export const VALUE_SPLIT = '__RC_CASCADER_SPLIT__';
|
||||||
|
|
||||||
|
export function toPathKey(value: SingleValueType) {
|
||||||
|
return value.join(VALUE_SPLIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toPathKeys(value: SingleValueType[]) {
|
||||||
|
return value.map(toPathKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toPathValueStr(pathKey: string) {
|
||||||
|
return pathKey.split(VALUE_SPLIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fillFieldNames(fieldNames?: FieldNames): InternalFieldNames {
|
||||||
|
const { label, value, children } = fieldNames || {};
|
||||||
|
const val = value || 'value';
|
||||||
|
return {
|
||||||
|
label: label || 'label',
|
||||||
|
value: val,
|
||||||
|
key: val,
|
||||||
|
children: children || 'children',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLeaf(option: DefaultOptionType, fieldNames: FieldNames) {
|
||||||
|
return option.isLeaf ?? !option[fieldNames.children]?.length;
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
import type { Key } from '../../_util/type';
|
||||||
|
import type { SingleValueType, DefaultOptionType, InternalFieldNames } from '../Cascader';
|
||||||
|
import type { GetEntities } from '../hooks/useEntities';
|
||||||
|
|
||||||
|
export function formatStrategyValues(pathKeys: Key[], getKeyPathEntities: GetEntities) {
|
||||||
|
const valueSet = new Set(pathKeys);
|
||||||
|
const keyPathEntities = getKeyPathEntities();
|
||||||
|
|
||||||
|
return pathKeys.filter(key => {
|
||||||
|
const entity = keyPathEntities[key];
|
||||||
|
const parent = entity ? entity.parent : null;
|
||||||
|
|
||||||
|
if (parent && !parent.node.disabled && valueSet.has(parent.key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toPathOptions(
|
||||||
|
valueCells: SingleValueType,
|
||||||
|
options: DefaultOptionType[],
|
||||||
|
fieldNames: InternalFieldNames,
|
||||||
|
// Used for loadingKeys which saved loaded keys as string
|
||||||
|
stringMode = false,
|
||||||
|
) {
|
||||||
|
let currentList = options;
|
||||||
|
const valueOptions: {
|
||||||
|
value: SingleValueType[number];
|
||||||
|
index: number;
|
||||||
|
option: DefaultOptionType;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < valueCells.length; i += 1) {
|
||||||
|
const valueCell = valueCells[i];
|
||||||
|
const foundIndex = currentList?.findIndex(option => {
|
||||||
|
const val = option[fieldNames.value];
|
||||||
|
return stringMode ? String(val) === String(valueCell) : val === valueCell;
|
||||||
|
});
|
||||||
|
const foundOption = foundIndex !== -1 ? currentList?.[foundIndex] : null;
|
||||||
|
|
||||||
|
valueOptions.push({
|
||||||
|
value: foundOption?.[fieldNames.value] ?? valueCell,
|
||||||
|
index: foundIndex,
|
||||||
|
option: foundOption,
|
||||||
|
});
|
||||||
|
|
||||||
|
currentList = foundOption?.[fieldNames.children];
|
||||||
|
}
|
||||||
|
|
||||||
|
return valueOptions;
|
||||||
|
}
|
|
@ -0,0 +1,863 @@
|
||||||
|
import { getSeparatedContent } from './utils/valueUtil';
|
||||||
|
import type { RefTriggerProps } from './SelectTrigger';
|
||||||
|
import SelectTrigger from './SelectTrigger';
|
||||||
|
import type { RefSelectorProps } from './Selector';
|
||||||
|
import Selector from './Selector';
|
||||||
|
import useSelectTriggerControl from './hooks/useSelectTriggerControl';
|
||||||
|
import useDelayReset from './hooks/useDelayReset';
|
||||||
|
import TransBtn from './TransBtn';
|
||||||
|
import useLock from './hooks/useLock';
|
||||||
|
import type { BaseSelectContextProps } from './hooks/useBaseProps';
|
||||||
|
import { useProvideBaseSelectProps } from './hooks/useBaseProps';
|
||||||
|
import type { Key, VueNode } from '../_util/type';
|
||||||
|
import type {
|
||||||
|
FocusEventHandler,
|
||||||
|
KeyboardEventHandler,
|
||||||
|
MouseEventHandler,
|
||||||
|
} from '../_util/EventInterface';
|
||||||
|
import type { ScrollTo } from '../vc-virtual-list/List';
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
defineComponent,
|
||||||
|
getCurrentInstance,
|
||||||
|
onBeforeUnmount,
|
||||||
|
onMounted,
|
||||||
|
ref,
|
||||||
|
toRefs,
|
||||||
|
watch,
|
||||||
|
watchEffect,
|
||||||
|
} from 'vue';
|
||||||
|
import type { CSSProperties, ExtractPropTypes, PropType, VNode } from 'vue';
|
||||||
|
import PropTypes from '../_util/vue-types';
|
||||||
|
import { initDefaultProps } from '../_util/props-util';
|
||||||
|
import isMobile from '../vc-util/isMobile';
|
||||||
|
import KeyCode from '../_util/KeyCode';
|
||||||
|
import { toReactive } from '../_util/toReactive';
|
||||||
|
import classNames from '../_util/classNames';
|
||||||
|
import OptionList from './OptionList';
|
||||||
|
import createRef from '../_util/createRef';
|
||||||
|
|
||||||
|
const DEFAULT_OMIT_PROPS = [
|
||||||
|
'value',
|
||||||
|
'onChange',
|
||||||
|
'removeIcon',
|
||||||
|
'placeholder',
|
||||||
|
'autofocus',
|
||||||
|
'maxTagCount',
|
||||||
|
'maxTagTextLength',
|
||||||
|
'maxTagPlaceholder',
|
||||||
|
'choiceTransitionName',
|
||||||
|
'onInputKeyDown',
|
||||||
|
'onPopupScroll',
|
||||||
|
'tabindex',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type RenderNode = VueNode | ((props: any) => VueNode);
|
||||||
|
|
||||||
|
export type RenderDOMFunc = (props: any) => HTMLElement;
|
||||||
|
|
||||||
|
export type Mode = 'multiple' | 'tags' | 'combobox';
|
||||||
|
|
||||||
|
export type Placement = 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topRight';
|
||||||
|
|
||||||
|
export type RawValueType = string | number;
|
||||||
|
|
||||||
|
export interface RefOptionListProps {
|
||||||
|
onKeydown: KeyboardEventHandler;
|
||||||
|
onKeyup: KeyboardEventHandler;
|
||||||
|
scrollTo?: (index: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CustomTagProps = {
|
||||||
|
label: any;
|
||||||
|
value: any;
|
||||||
|
disabled: boolean;
|
||||||
|
onClose: (event?: MouseEvent) => void;
|
||||||
|
closable: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DisplayValueType {
|
||||||
|
key?: Key;
|
||||||
|
value?: RawValueType;
|
||||||
|
label?: any;
|
||||||
|
disabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseSelectRef {
|
||||||
|
focus: () => void;
|
||||||
|
blur: () => void;
|
||||||
|
scrollTo: ScrollTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseSelectPrivateProps = () => {
|
||||||
|
return {
|
||||||
|
prefixCls: String,
|
||||||
|
id: String,
|
||||||
|
omitDomProps: Array as PropType<string[]>,
|
||||||
|
|
||||||
|
// >>> Value
|
||||||
|
displayValues: Array as PropType<DisplayValueType[]>,
|
||||||
|
onDisplayValuesChange: Function as PropType<
|
||||||
|
(
|
||||||
|
values: DisplayValueType[],
|
||||||
|
info: {
|
||||||
|
type: 'add' | 'remove' | 'clear';
|
||||||
|
values: DisplayValueType[];
|
||||||
|
},
|
||||||
|
) => void
|
||||||
|
>,
|
||||||
|
|
||||||
|
// >>> Active
|
||||||
|
/** Current dropdown list active item string value */
|
||||||
|
activeValue: String,
|
||||||
|
/** Link search input with target element */
|
||||||
|
activeDescendantId: String,
|
||||||
|
onActiveValueChange: Function as PropType<(value: string | null) => void>,
|
||||||
|
|
||||||
|
// >>> Search
|
||||||
|
searchValue: String,
|
||||||
|
/** Trigger onSearch, return false to prevent trigger open event */
|
||||||
|
onSearch: Function as PropType<
|
||||||
|
(
|
||||||
|
searchValue: string,
|
||||||
|
info: {
|
||||||
|
source:
|
||||||
|
| 'typing' //User typing
|
||||||
|
| 'effect' // Code logic trigger
|
||||||
|
| 'submit' // tag mode only
|
||||||
|
| 'blur'; // Not trigger event
|
||||||
|
},
|
||||||
|
) => void
|
||||||
|
>,
|
||||||
|
/** Trigger when search text match the `tokenSeparators`. Will provide split content */
|
||||||
|
onSearchSplit: Function as PropType<(words: string[]) => void>,
|
||||||
|
maxLength: Number,
|
||||||
|
|
||||||
|
OptionList: PropTypes.any,
|
||||||
|
|
||||||
|
/** Tell if provided `options` is empty */
|
||||||
|
emptyOptions: Boolean,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DropdownObject = {
|
||||||
|
menuNode?: VueNode;
|
||||||
|
props?: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DropdownRender = (opt?: DropdownObject) => VueNode;
|
||||||
|
export const baseSelectPropsWithoutPrivate = () => {
|
||||||
|
return {
|
||||||
|
showSearch: { type: Boolean, default: undefined },
|
||||||
|
tagRender: { type: Function as PropType<(props: CustomTagProps) => any> },
|
||||||
|
direction: { type: String as PropType<'ltr' | 'rtl'> },
|
||||||
|
|
||||||
|
// MISC
|
||||||
|
tabindex: Number,
|
||||||
|
autofocus: Boolean,
|
||||||
|
notFoundContent: PropTypes.any,
|
||||||
|
placeholder: PropTypes.any,
|
||||||
|
onClear: Function as PropType<() => void>,
|
||||||
|
|
||||||
|
choiceTransitionName: String,
|
||||||
|
|
||||||
|
// >>> Mode
|
||||||
|
mode: String as PropType<Mode>,
|
||||||
|
|
||||||
|
// >>> Status
|
||||||
|
disabled: { type: Boolean, default: undefined },
|
||||||
|
loading: { type: Boolean, default: undefined },
|
||||||
|
|
||||||
|
// >>> Open
|
||||||
|
open: { type: Boolean, default: undefined },
|
||||||
|
defaultOpen: { type: Boolean, default: undefined },
|
||||||
|
onDropdownVisibleChange: { type: Function as PropType<(open: boolean) => void> },
|
||||||
|
|
||||||
|
// >>> Customize Input
|
||||||
|
/** @private Internal usage. Do not use in your production. */
|
||||||
|
getInputElement: { type: Function as PropType<() => any> },
|
||||||
|
/** @private Internal usage. Do not use in your production. */
|
||||||
|
getRawInputElement: { type: Function as PropType<() => any> },
|
||||||
|
|
||||||
|
// >>> Selector
|
||||||
|
maxTagTextLength: Number,
|
||||||
|
maxTagCount: { type: [String, Number] as PropType<number | 'responsive'> },
|
||||||
|
maxTagPlaceholder: PropTypes.any,
|
||||||
|
|
||||||
|
// >>> Search
|
||||||
|
tokenSeparators: { type: Array as PropType<string[]> },
|
||||||
|
|
||||||
|
// >>> Icons
|
||||||
|
allowClear: { type: Boolean, default: undefined },
|
||||||
|
showArrow: { type: Boolean, default: undefined },
|
||||||
|
inputIcon: PropTypes.any,
|
||||||
|
/** Clear all icon */
|
||||||
|
clearIcon: PropTypes.any,
|
||||||
|
/** Selector remove icon */
|
||||||
|
removeIcon: PropTypes.any,
|
||||||
|
|
||||||
|
// >>> Dropdown
|
||||||
|
animation: String,
|
||||||
|
transitionName: String,
|
||||||
|
dropdownStyle: { type: Object as PropType<CSSProperties> },
|
||||||
|
dropdownClassName: String,
|
||||||
|
dropdownMatchSelectWidth: {
|
||||||
|
type: [Boolean, Number] as PropType<boolean | number>,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
dropdownRender: { type: Function as PropType<DropdownRender> },
|
||||||
|
dropdownAlign: PropTypes.any,
|
||||||
|
placement: {
|
||||||
|
type: String as PropType<Placement>,
|
||||||
|
},
|
||||||
|
getPopupContainer: { type: Function as PropType<RenderDOMFunc> },
|
||||||
|
|
||||||
|
// >>> Focus
|
||||||
|
showAction: { type: Array as PropType<('focus' | 'click')[]> },
|
||||||
|
onBlur: { type: Function as PropType<(e: FocusEvent) => void> },
|
||||||
|
onFocus: { type: Function as PropType<(e: FocusEvent) => void> },
|
||||||
|
|
||||||
|
// >>> Rest Events
|
||||||
|
onKeyup: Function as PropType<(e: KeyboardEvent) => void>,
|
||||||
|
onKeydown: Function as PropType<(e: KeyboardEvent) => void>,
|
||||||
|
onMousedown: Function as PropType<(e: MouseEvent) => void>,
|
||||||
|
onPopupScroll: Function as PropType<(e: UIEvent) => void>,
|
||||||
|
onInputKeyDown: Function as PropType<(e: KeyboardEvent) => void>,
|
||||||
|
onMouseenter: Function as PropType<(e: MouseEvent) => void>,
|
||||||
|
onMouseleave: Function as PropType<(e: MouseEvent) => void>,
|
||||||
|
onClick: Function as PropType<(e: MouseEvent) => void>,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const baseSelectProps = () => {
|
||||||
|
return {
|
||||||
|
...baseSelectPrivateProps(),
|
||||||
|
...baseSelectPropsWithoutPrivate(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BaseSelectPrivateProps = Partial<
|
||||||
|
ExtractPropTypes<ReturnType<typeof baseSelectPrivateProps>>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type BaseSelectProps = Partial<ExtractPropTypes<ReturnType<typeof baseSelectProps>>>;
|
||||||
|
|
||||||
|
export type BaseSelectPropsWithoutPrivate = Omit<BaseSelectProps, keyof BaseSelectPrivateProps>;
|
||||||
|
|
||||||
|
export function isMultiple(mode: Mode) {
|
||||||
|
return mode === 'tags' || mode === 'multiple';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'BaseSelect',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: initDefaultProps(baseSelectProps(), { showAction: [], notFoundContent: 'Not Found' }),
|
||||||
|
setup(props, { attrs, expose }) {
|
||||||
|
const multiple = computed(() => isMultiple(props.mode));
|
||||||
|
|
||||||
|
const mergedShowSearch = computed(() =>
|
||||||
|
props.showSearch !== undefined
|
||||||
|
? props.showSearch
|
||||||
|
: multiple.value || props.mode === 'combobox',
|
||||||
|
);
|
||||||
|
const mobile = ref(false);
|
||||||
|
onMounted(() => {
|
||||||
|
mobile.value = isMobile();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================== Refs ==============================
|
||||||
|
const containerRef = ref<HTMLDivElement>(null);
|
||||||
|
const selectorDomRef = createRef();
|
||||||
|
const triggerRef = ref<RefTriggerProps>(null);
|
||||||
|
const selectorRef = ref<RefSelectorProps>(null);
|
||||||
|
const listRef = ref<RefOptionListProps>(null);
|
||||||
|
|
||||||
|
/** Used for component focused management */
|
||||||
|
const [mockFocused, setMockFocused, cancelSetMockFocused] = useDelayReset();
|
||||||
|
|
||||||
|
expose({
|
||||||
|
focus,
|
||||||
|
blur,
|
||||||
|
scrollTo: arg => listRef.value?.scrollTo(arg),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mergedSearchValue = computed(() => {
|
||||||
|
if (props.mode !== 'combobox') {
|
||||||
|
return props.searchValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const val = props.displayValues[0]?.value;
|
||||||
|
|
||||||
|
return typeof val === 'string' || typeof val === 'number' ? String(val) : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================== Open ==============================
|
||||||
|
const initOpen = props.open !== undefined ? props.open : props.defaultOpen;
|
||||||
|
const innerOpen = ref(initOpen);
|
||||||
|
const mergedOpen = ref(initOpen);
|
||||||
|
const setInnerOpen = (val: boolean) => {
|
||||||
|
innerOpen.value = props.open !== undefined ? props.open : val;
|
||||||
|
mergedOpen.value = innerOpen.value;
|
||||||
|
};
|
||||||
|
watch(
|
||||||
|
() => props.open,
|
||||||
|
() => {
|
||||||
|
setInnerOpen(props.open);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Not trigger `open` in `combobox` when `notFoundContent` is empty
|
||||||
|
const emptyListContent = computed(() => !props.notFoundContent && props.emptyOptions);
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
mergedOpen.value = innerOpen.value;
|
||||||
|
if (
|
||||||
|
props.disabled ||
|
||||||
|
(emptyListContent.value && mergedOpen.value && props.mode === 'combobox')
|
||||||
|
) {
|
||||||
|
mergedOpen.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const triggerOpen = computed(() => (emptyListContent.value ? false : mergedOpen.value));
|
||||||
|
|
||||||
|
const onToggleOpen = (newOpen?: boolean) => {
|
||||||
|
const nextOpen = newOpen !== undefined ? newOpen : !mergedOpen.value;
|
||||||
|
|
||||||
|
if (innerOpen.value !== nextOpen && !props.disabled) {
|
||||||
|
setInnerOpen(nextOpen);
|
||||||
|
if (props.onDropdownVisibleChange) {
|
||||||
|
props.onDropdownVisibleChange(nextOpen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tokenWithEnter = computed(() =>
|
||||||
|
(props.tokenSeparators || []).some(tokenSeparator => ['\n', '\r\n'].includes(tokenSeparator)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const onInternalSearch = (searchText: string, fromTyping: boolean, isCompositing: boolean) => {
|
||||||
|
let ret = true;
|
||||||
|
let newSearchText = searchText;
|
||||||
|
props.onActiveValueChange?.(null);
|
||||||
|
|
||||||
|
// Check if match the `tokenSeparators`
|
||||||
|
const patchLabels: string[] = isCompositing
|
||||||
|
? null
|
||||||
|
: getSeparatedContent(searchText, props.tokenSeparators);
|
||||||
|
|
||||||
|
// Ignore combobox since it's not split-able
|
||||||
|
if (props.mode !== 'combobox' && patchLabels) {
|
||||||
|
newSearchText = '';
|
||||||
|
|
||||||
|
props.onSearchSplit?.(patchLabels);
|
||||||
|
|
||||||
|
// Should close when paste finish
|
||||||
|
onToggleOpen(false);
|
||||||
|
|
||||||
|
// Tell Selector that break next actions
|
||||||
|
ret = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.onSearch && mergedSearchValue.value !== newSearchText) {
|
||||||
|
props.onSearch(newSearchText, {
|
||||||
|
source: fromTyping ? 'typing' : 'effect',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only triggered when menu is closed & mode is tags
|
||||||
|
// If menu is open, OptionList will take charge
|
||||||
|
// If mode isn't tags, press enter is not meaningful when you can't see any option
|
||||||
|
const onInternalSearchSubmit = (searchText: string) => {
|
||||||
|
// prevent empty tags from appearing when you click the Enter button
|
||||||
|
if (!searchText || !searchText.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
props.onSearch?.(searchText, { source: 'submit' });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close will clean up single mode search text
|
||||||
|
watch(
|
||||||
|
mergedOpen,
|
||||||
|
() => {
|
||||||
|
if (!mergedOpen.value && !multiple.value && props.mode !== 'combobox') {
|
||||||
|
onInternalSearch('', false, false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================ Disabled ============================
|
||||||
|
// Close dropdown & remove focus state when disabled change
|
||||||
|
watch(
|
||||||
|
() => props.disabled,
|
||||||
|
() => {
|
||||||
|
if (innerOpen.value && !!props.disabled) {
|
||||||
|
setInnerOpen(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================ Keyboard ============================
|
||||||
|
/**
|
||||||
|
* We record input value here to check if can press to clean up by backspace
|
||||||
|
* - null: Key is not down, this is reset by key up
|
||||||
|
* - true: Search text is empty when first time backspace down
|
||||||
|
* - false: Search text is not empty when first time backspace down
|
||||||
|
*/
|
||||||
|
const [getClearLock, setClearLock] = useLock();
|
||||||
|
|
||||||
|
// KeyDown
|
||||||
|
const onInternalKeyDown: KeyboardEventHandler = (event, ...rest) => {
|
||||||
|
const clearLock = getClearLock();
|
||||||
|
const { which } = event;
|
||||||
|
|
||||||
|
if (which === KeyCode.ENTER) {
|
||||||
|
// Do not submit form when type in the input
|
||||||
|
if (props.mode !== 'combobox') {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only manage open state here, close logic should handle by list component
|
||||||
|
if (!mergedOpen.value) {
|
||||||
|
onToggleOpen(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setClearLock(!!mergedSearchValue.value);
|
||||||
|
|
||||||
|
// Remove value by `backspace`
|
||||||
|
if (
|
||||||
|
which === KeyCode.BACKSPACE &&
|
||||||
|
!clearLock &&
|
||||||
|
multiple.value &&
|
||||||
|
!mergedSearchValue.value &&
|
||||||
|
props.displayValues.length
|
||||||
|
) {
|
||||||
|
const cloneDisplayValues = [...props.displayValues];
|
||||||
|
let removedDisplayValue = null;
|
||||||
|
|
||||||
|
for (let i = cloneDisplayValues.length - 1; i >= 0; i -= 1) {
|
||||||
|
const current = cloneDisplayValues[i];
|
||||||
|
|
||||||
|
if (!current.disabled) {
|
||||||
|
cloneDisplayValues.splice(i, 1);
|
||||||
|
removedDisplayValue = current;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removedDisplayValue) {
|
||||||
|
props.onDisplayValuesChange(cloneDisplayValues, {
|
||||||
|
type: 'remove',
|
||||||
|
values: [removedDisplayValue],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mergedOpen.value && listRef.value) {
|
||||||
|
listRef.value.onKeydown(event, ...rest);
|
||||||
|
}
|
||||||
|
|
||||||
|
props.onKeydown?.(event, ...rest);
|
||||||
|
};
|
||||||
|
|
||||||
|
// KeyUp
|
||||||
|
const onInternalKeyUp: KeyboardEventHandler = (event: KeyboardEvent, ...rest) => {
|
||||||
|
if (mergedOpen.value && listRef.value) {
|
||||||
|
listRef.value.onKeyup(event, ...rest);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.onKeyup) {
|
||||||
|
props.onKeyup(event, ...rest);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================ Selector ============================
|
||||||
|
const onSelectorRemove = (val: DisplayValueType) => {
|
||||||
|
const newValues = props.displayValues.filter(i => i !== val);
|
||||||
|
|
||||||
|
props.onDisplayValuesChange(newValues, {
|
||||||
|
type: 'remove',
|
||||||
|
values: [val],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========================== Focus / Blur ==========================
|
||||||
|
/** Record real focus status */
|
||||||
|
const focusRef = ref(false);
|
||||||
|
|
||||||
|
const onContainerFocus: FocusEventHandler = (...args) => {
|
||||||
|
setMockFocused(true);
|
||||||
|
|
||||||
|
if (!props.disabled) {
|
||||||
|
if (props.onFocus && !focusRef.value) {
|
||||||
|
props.onFocus(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// `showAction` should handle `focus` if set
|
||||||
|
if (props.showAction && props.showAction.includes('focus')) {
|
||||||
|
onToggleOpen(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
focusRef.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onContainerBlur: FocusEventHandler = (...args) => {
|
||||||
|
setMockFocused(false, () => {
|
||||||
|
focusRef.value = false;
|
||||||
|
onToggleOpen(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (props.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const searchVal = mergedSearchValue.value;
|
||||||
|
if (searchVal) {
|
||||||
|
// `tags` mode should move `searchValue` into values
|
||||||
|
if (props.mode === 'tags') {
|
||||||
|
props.onSearch(searchVal, { source: 'submit' });
|
||||||
|
} else if (props.mode === 'multiple') {
|
||||||
|
// `multiple` mode only clean the search value but not trigger event
|
||||||
|
props.onSearch('', {
|
||||||
|
source: 'blur',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.onBlur) {
|
||||||
|
props.onBlur(...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeTimeoutIds: any[] = [];
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
activeTimeoutIds.forEach(timeoutId => clearTimeout(timeoutId));
|
||||||
|
activeTimeoutIds.splice(0, activeTimeoutIds.length);
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
activeTimeoutIds.forEach(timeoutId => clearTimeout(timeoutId));
|
||||||
|
activeTimeoutIds.splice(0, activeTimeoutIds.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
const onInternalMouseDown: MouseEventHandler = (event, ...restArgs) => {
|
||||||
|
const { target } = event;
|
||||||
|
const popupElement: HTMLDivElement = triggerRef.value?.getPopupElement();
|
||||||
|
|
||||||
|
// We should give focus back to selector if clicked item is not focusable
|
||||||
|
if (popupElement && popupElement.contains(target as HTMLElement)) {
|
||||||
|
const timeoutId: any = setTimeout(() => {
|
||||||
|
const index = activeTimeoutIds.indexOf(timeoutId);
|
||||||
|
if (index !== -1) {
|
||||||
|
activeTimeoutIds.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelSetMockFocused();
|
||||||
|
|
||||||
|
if (!mobile.value && !popupElement.contains(document.activeElement)) {
|
||||||
|
selectorRef.value?.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
activeTimeoutIds.push(timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
props.onMousedown?.(event, ...restArgs);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================= Dropdown ==============================
|
||||||
|
const containerWidth = ref<number>(null);
|
||||||
|
const instance = getCurrentInstance();
|
||||||
|
const onPopupMouseEnter = () => {
|
||||||
|
// We need force update here since popup dom is render async
|
||||||
|
instance.update();
|
||||||
|
};
|
||||||
|
onMounted(() => {
|
||||||
|
watch(
|
||||||
|
triggerOpen,
|
||||||
|
() => {
|
||||||
|
if (triggerOpen.value) {
|
||||||
|
const newWidth = Math.ceil(containerRef.value.offsetWidth);
|
||||||
|
if (containerWidth.value !== newWidth && !Number.isNaN(newWidth)) {
|
||||||
|
containerWidth.value = newWidth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close when click on non-select element
|
||||||
|
useSelectTriggerControl([containerRef, triggerRef], triggerOpen, onToggleOpen);
|
||||||
|
useProvideBaseSelectProps(
|
||||||
|
toReactive({
|
||||||
|
...toRefs(props),
|
||||||
|
open: mergedOpen,
|
||||||
|
triggerOpen,
|
||||||
|
showSearch: mergedShowSearch,
|
||||||
|
multiple,
|
||||||
|
toggleOpen: onToggleOpen,
|
||||||
|
} as unknown as BaseSelectContextProps),
|
||||||
|
);
|
||||||
|
return () => {
|
||||||
|
const {
|
||||||
|
prefixCls,
|
||||||
|
id,
|
||||||
|
|
||||||
|
open,
|
||||||
|
defaultOpen,
|
||||||
|
|
||||||
|
mode,
|
||||||
|
|
||||||
|
// Search related
|
||||||
|
showSearch,
|
||||||
|
searchValue,
|
||||||
|
onSearch,
|
||||||
|
|
||||||
|
// Icons
|
||||||
|
allowClear,
|
||||||
|
clearIcon,
|
||||||
|
showArrow,
|
||||||
|
inputIcon,
|
||||||
|
|
||||||
|
// Others
|
||||||
|
disabled,
|
||||||
|
loading,
|
||||||
|
getInputElement,
|
||||||
|
getPopupContainer,
|
||||||
|
placement,
|
||||||
|
|
||||||
|
// Dropdown
|
||||||
|
animation,
|
||||||
|
transitionName,
|
||||||
|
dropdownStyle,
|
||||||
|
dropdownClassName,
|
||||||
|
dropdownMatchSelectWidth,
|
||||||
|
dropdownRender,
|
||||||
|
dropdownAlign,
|
||||||
|
showAction,
|
||||||
|
direction,
|
||||||
|
|
||||||
|
// Tags
|
||||||
|
tokenSeparators,
|
||||||
|
tagRender,
|
||||||
|
|
||||||
|
// Events
|
||||||
|
onPopupScroll,
|
||||||
|
onDropdownVisibleChange,
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
|
onKeyup,
|
||||||
|
onKeydown,
|
||||||
|
onMousedown,
|
||||||
|
|
||||||
|
onClear,
|
||||||
|
omitDomProps,
|
||||||
|
getRawInputElement,
|
||||||
|
displayValues,
|
||||||
|
onDisplayValuesChange,
|
||||||
|
emptyOptions,
|
||||||
|
activeDescendantId,
|
||||||
|
activeValue,
|
||||||
|
|
||||||
|
...restProps
|
||||||
|
} = { ...props, ...attrs } as BaseSelectProps;
|
||||||
|
// ============================= Input ==============================
|
||||||
|
// Only works in `combobox`
|
||||||
|
const customizeInputElement: any =
|
||||||
|
(mode === 'combobox' && getInputElement && getInputElement()) || null;
|
||||||
|
|
||||||
|
// Used for customize replacement for `vc-cascader`
|
||||||
|
const customizeRawInputElement: any =
|
||||||
|
typeof getRawInputElement === 'function' && getRawInputElement();
|
||||||
|
const domProps = {
|
||||||
|
...restProps,
|
||||||
|
} as Omit<keyof typeof restProps, typeof DEFAULT_OMIT_PROPS[number]>;
|
||||||
|
|
||||||
|
// Used for raw custom input trigger
|
||||||
|
let onTriggerVisibleChange: null | ((newOpen: boolean) => void);
|
||||||
|
if (customizeRawInputElement) {
|
||||||
|
onTriggerVisibleChange = (newOpen: boolean) => {
|
||||||
|
onToggleOpen(newOpen);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFAULT_OMIT_PROPS.forEach(propName => {
|
||||||
|
delete domProps[propName];
|
||||||
|
});
|
||||||
|
|
||||||
|
omitDomProps?.forEach(propName => {
|
||||||
|
delete domProps[propName];
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================= Arrow ==============================
|
||||||
|
const mergedShowArrow =
|
||||||
|
showArrow !== undefined ? showArrow : loading || (!multiple.value && mode !== 'combobox');
|
||||||
|
let arrowNode: VNode | JSX.Element;
|
||||||
|
|
||||||
|
if (mergedShowArrow) {
|
||||||
|
arrowNode = (
|
||||||
|
<TransBtn
|
||||||
|
class={classNames(`${prefixCls}-arrow`, {
|
||||||
|
[`${prefixCls}-arrow-loading`]: loading,
|
||||||
|
})}
|
||||||
|
customizeIcon={inputIcon}
|
||||||
|
customizeIconProps={{
|
||||||
|
loading,
|
||||||
|
searchValue: mergedSearchValue.value,
|
||||||
|
open: mergedOpen.value,
|
||||||
|
focused: mockFocused.value,
|
||||||
|
showSearch: mergedShowSearch.value,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================= Clear ==============================
|
||||||
|
let clearNode: VNode | JSX.Element;
|
||||||
|
const onClearMouseDown: MouseEventHandler = () => {
|
||||||
|
onClear?.();
|
||||||
|
|
||||||
|
onDisplayValuesChange([], {
|
||||||
|
type: 'clear',
|
||||||
|
values: displayValues,
|
||||||
|
});
|
||||||
|
onInternalSearch('', false, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!disabled && allowClear && (displayValues.length || mergedSearchValue)) {
|
||||||
|
clearNode = (
|
||||||
|
<TransBtn
|
||||||
|
class={`${prefixCls}-clear`}
|
||||||
|
onMousedown={onClearMouseDown}
|
||||||
|
customizeIcon={clearIcon}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</TransBtn>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================== OptionList ===========================
|
||||||
|
const optionList = <OptionList ref={listRef} />;
|
||||||
|
|
||||||
|
// ============================= Select =============================
|
||||||
|
const mergedClassName = classNames(prefixCls, attrs.class, {
|
||||||
|
[`${prefixCls}-focused`]: mockFocused.value,
|
||||||
|
[`${prefixCls}-multiple`]: multiple.value,
|
||||||
|
[`${prefixCls}-single`]: !multiple.value,
|
||||||
|
[`${prefixCls}-allow-clear`]: allowClear,
|
||||||
|
[`${prefixCls}-show-arrow`]: mergedShowArrow,
|
||||||
|
[`${prefixCls}-disabled`]: disabled,
|
||||||
|
[`${prefixCls}-loading`]: loading,
|
||||||
|
[`${prefixCls}-open`]: mergedOpen.value,
|
||||||
|
[`${prefixCls}-customize-input`]: customizeInputElement,
|
||||||
|
[`${prefixCls}-show-search`]: mergedShowSearch.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
// >>> Selector
|
||||||
|
const selectorNode = (
|
||||||
|
<SelectTrigger
|
||||||
|
ref={triggerRef}
|
||||||
|
disabled={disabled}
|
||||||
|
prefixCls={prefixCls}
|
||||||
|
visible={triggerOpen.value}
|
||||||
|
popupElement={optionList}
|
||||||
|
containerWidth={containerWidth.value}
|
||||||
|
animation={animation}
|
||||||
|
transitionName={transitionName}
|
||||||
|
dropdownStyle={dropdownStyle}
|
||||||
|
dropdownClassName={dropdownClassName}
|
||||||
|
direction={direction}
|
||||||
|
dropdownMatchSelectWidth={dropdownMatchSelectWidth}
|
||||||
|
dropdownRender={dropdownRender}
|
||||||
|
dropdownAlign={dropdownAlign}
|
||||||
|
placement={placement}
|
||||||
|
getPopupContainer={getPopupContainer}
|
||||||
|
empty={emptyOptions}
|
||||||
|
getTriggerDOMNode={() => selectorDomRef.current}
|
||||||
|
onPopupVisibleChange={onTriggerVisibleChange}
|
||||||
|
onPopupMouseEnter={onPopupMouseEnter}
|
||||||
|
v-slots={{
|
||||||
|
default: () => {
|
||||||
|
return customizeRawInputElement ? (
|
||||||
|
customizeRawInputElement
|
||||||
|
) : (
|
||||||
|
<Selector
|
||||||
|
{...props}
|
||||||
|
domRef={selectorDomRef}
|
||||||
|
prefixCls={prefixCls}
|
||||||
|
inputElement={customizeInputElement}
|
||||||
|
ref={selectorRef}
|
||||||
|
id={id}
|
||||||
|
showSearch={mergedShowSearch.value}
|
||||||
|
mode={mode}
|
||||||
|
activeDescendantId={activeDescendantId}
|
||||||
|
tagRender={tagRender}
|
||||||
|
values={displayValues}
|
||||||
|
open={mergedOpen.value}
|
||||||
|
onToggleOpen={onToggleOpen}
|
||||||
|
activeValue={activeValue}
|
||||||
|
searchValue={mergedSearchValue.value}
|
||||||
|
onSearch={onInternalSearch}
|
||||||
|
onSearchSubmit={onInternalSearchSubmit}
|
||||||
|
onRemove={onSelectorRemove}
|
||||||
|
tokenWithEnter={tokenWithEnter.value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
></SelectTrigger>
|
||||||
|
);
|
||||||
|
// >>> Render
|
||||||
|
let renderNode: VNode | JSX.Element;
|
||||||
|
|
||||||
|
// Render raw
|
||||||
|
if (customizeRawInputElement) {
|
||||||
|
renderNode = selectorNode;
|
||||||
|
} else {
|
||||||
|
renderNode = (
|
||||||
|
<div
|
||||||
|
class={mergedClassName}
|
||||||
|
{...domProps}
|
||||||
|
ref={containerRef}
|
||||||
|
onMousedown={onInternalMouseDown}
|
||||||
|
onKeydown={onInternalKeyDown}
|
||||||
|
onKeyup={onInternalKeyUp}
|
||||||
|
onFocus={onContainerFocus}
|
||||||
|
onBlur={onContainerBlur}
|
||||||
|
>
|
||||||
|
{mockFocused && !mergedOpen.value && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
display: 'flex',
|
||||||
|
overflow: 'hidden',
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
{/* Merge into one string to make screen reader work as expect */}
|
||||||
|
{`${displayValues
|
||||||
|
.map(({ label, value }) =>
|
||||||
|
['number', 'string'].includes(typeof label) ? label : value,
|
||||||
|
)
|
||||||
|
.join(', ')}`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{selectorNode}
|
||||||
|
|
||||||
|
{arrowNode}
|
||||||
|
{clearNode}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return renderNode;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,15 @@
|
||||||
|
import type { FunctionalComponent } from 'vue';
|
||||||
|
|
||||||
|
import type { DefaultOptionType } from './Select';
|
||||||
|
|
||||||
|
export type OptGroupProps = Omit<DefaultOptionType, 'options'>;
|
||||||
|
|
||||||
|
export interface OptionGroupFC extends FunctionalComponent<OptGroupProps> {
|
||||||
|
/** Legacy for check if is a Option Group */
|
||||||
|
isSelectOptGroup: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OptGroup: OptionGroupFC = () => null;
|
||||||
|
OptGroup.isSelectOptGroup = true;
|
||||||
|
OptGroup.displayName = 'ASelectOptGroup';
|
||||||
|
export default OptGroup;
|
|
@ -0,0 +1,18 @@
|
||||||
|
import type { FunctionalComponent } from 'vue';
|
||||||
|
|
||||||
|
import type { DefaultOptionType } from './Select';
|
||||||
|
|
||||||
|
export interface OptionProps extends Omit<DefaultOptionType, 'label'> {
|
||||||
|
/** Save for customize data */
|
||||||
|
[prop: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OptionFC extends FunctionalComponent<OptionProps> {
|
||||||
|
/** Legacy for check if is a Option Group */
|
||||||
|
isSelectOption: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Option: OptionFC = () => null;
|
||||||
|
Option.isSelectOption = true;
|
||||||
|
Option.displayName = 'ASelectOption';
|
||||||
|
export default Option;
|
|
@ -0,0 +1,372 @@
|
||||||
|
import TransBtn from './TransBtn';
|
||||||
|
|
||||||
|
import KeyCode from '../_util/KeyCode';
|
||||||
|
import classNames from '../_util/classNames';
|
||||||
|
import pickAttrs from '../_util/pickAttrs';
|
||||||
|
import { isValidElement } from '../_util/props-util';
|
||||||
|
import createRef from '../_util/createRef';
|
||||||
|
import { computed, defineComponent, nextTick, reactive, watch } from 'vue';
|
||||||
|
import List from '../vc-virtual-list';
|
||||||
|
import useMemo from '../_util/hooks/useMemo';
|
||||||
|
import { isPlatformMac } from './utils/platformUtil';
|
||||||
|
|
||||||
|
export interface RefOptionListProps {
|
||||||
|
onKeydown: (e?: KeyboardEvent) => void;
|
||||||
|
onKeyup: (e?: KeyboardEvent) => void;
|
||||||
|
scrollTo?: (index: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
import type { EventHandler } from '../_util/EventInterface';
|
||||||
|
import omit from '../_util/omit';
|
||||||
|
import useBaseProps from './hooks/useBaseProps';
|
||||||
|
import type { RawValueType } from './Select';
|
||||||
|
import useSelectProps from './SelectContext';
|
||||||
|
// export interface OptionListProps<OptionsType extends object[]> {
|
||||||
|
export type OptionListProps = Record<string, never>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Using virtual list of option display.
|
||||||
|
* Will fallback to dom if use customize render.
|
||||||
|
*/
|
||||||
|
const OptionList = defineComponent({
|
||||||
|
name: 'OptionList',
|
||||||
|
inheritAttrs: false,
|
||||||
|
slots: ['option'],
|
||||||
|
setup(_, { expose, slots }) {
|
||||||
|
const baseProps = useBaseProps();
|
||||||
|
const props = useSelectProps();
|
||||||
|
const itemPrefixCls = computed(() => `${baseProps.prefixCls}-item`);
|
||||||
|
|
||||||
|
const memoFlattenOptions = useMemo(
|
||||||
|
() => props.flattenOptions,
|
||||||
|
[() => baseProps.open, () => props.flattenOptions],
|
||||||
|
next => next[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
// =========================== List ===========================
|
||||||
|
const listRef = createRef();
|
||||||
|
|
||||||
|
const onListMouseDown: EventHandler = event => {
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollIntoView = (index: number) => {
|
||||||
|
if (listRef.current) {
|
||||||
|
listRef.current.scrollTo({ index });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========================== Active ==========================
|
||||||
|
const getEnabledActiveIndex = (index: number, offset = 1) => {
|
||||||
|
const len = memoFlattenOptions.value.length;
|
||||||
|
|
||||||
|
for (let i = 0; i < len; i += 1) {
|
||||||
|
const current = (index + i * offset + len) % len;
|
||||||
|
|
||||||
|
const { group, data } = memoFlattenOptions.value[current];
|
||||||
|
if (!group && !data.disabled) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
const state = reactive({
|
||||||
|
activeIndex: getEnabledActiveIndex(0),
|
||||||
|
});
|
||||||
|
|
||||||
|
const setActive = (index: number, fromKeyboard = false) => {
|
||||||
|
state.activeIndex = index;
|
||||||
|
const info = { source: fromKeyboard ? ('keyboard' as const) : ('mouse' as const) };
|
||||||
|
|
||||||
|
// Trigger active event
|
||||||
|
const flattenItem = memoFlattenOptions.value[index];
|
||||||
|
if (!flattenItem) {
|
||||||
|
props.onActiveValue(null, -1, info);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
props.onActiveValue(flattenItem.data.value, index, info);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto active first item when list length or searchValue changed
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[() => memoFlattenOptions.value.length, () => baseProps.searchValue],
|
||||||
|
() => {
|
||||||
|
setActive(props.defaultActiveFirstOption !== false ? getEnabledActiveIndex(0) : -1);
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
// Auto scroll to item position in single mode
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[() => baseProps.open, () => baseProps.searchValue],
|
||||||
|
() => {
|
||||||
|
if (!baseProps.multiple && baseProps.open && props.rawValues.size === 1) {
|
||||||
|
const value = Array.from(props.rawValues)[0];
|
||||||
|
const index = memoFlattenOptions.value.findIndex(({ data }) => data.value === value);
|
||||||
|
if (index !== -1) {
|
||||||
|
setActive(index);
|
||||||
|
nextTick(() => {
|
||||||
|
scrollIntoView(index);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Force trigger scrollbar visible when open
|
||||||
|
if (baseProps.open) {
|
||||||
|
nextTick(() => {
|
||||||
|
listRef.current?.scrollTo(undefined);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true, flush: 'post' },
|
||||||
|
);
|
||||||
|
|
||||||
|
// ========================== Values ==========================
|
||||||
|
const onSelectValue = (value?: RawValueType) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
props.onSelect(value, { selected: !props.rawValues.has(value) });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single mode should always close by select
|
||||||
|
if (!baseProps.multiple) {
|
||||||
|
baseProps.toggleOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getLabel = (item: Record<string, any>) => item.label;
|
||||||
|
function renderItem(index: number) {
|
||||||
|
const item = memoFlattenOptions.value[index];
|
||||||
|
if (!item) return null;
|
||||||
|
|
||||||
|
const itemData = item.data || {};
|
||||||
|
const { value } = itemData;
|
||||||
|
const { group } = item;
|
||||||
|
const attrs = pickAttrs(itemData, true);
|
||||||
|
const mergedLabel = getLabel(item);
|
||||||
|
return item ? (
|
||||||
|
<div
|
||||||
|
aria-label={typeof mergedLabel === 'string' && !group ? mergedLabel : null}
|
||||||
|
{...attrs}
|
||||||
|
key={index}
|
||||||
|
role={group ? 'presentation' : 'option'}
|
||||||
|
id={`${baseProps.id}_list_${index}`}
|
||||||
|
aria-selected={props.rawValues.has(value)}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
const onKeydown = (event: KeyboardEvent) => {
|
||||||
|
const { which, ctrlKey } = event;
|
||||||
|
switch (which) {
|
||||||
|
// >>> Arrow keys & ctrl + n/p on Mac
|
||||||
|
case KeyCode.N:
|
||||||
|
case KeyCode.P:
|
||||||
|
case KeyCode.UP:
|
||||||
|
case KeyCode.DOWN: {
|
||||||
|
let offset = 0;
|
||||||
|
if (which === KeyCode.UP) {
|
||||||
|
offset = -1;
|
||||||
|
} else if (which === KeyCode.DOWN) {
|
||||||
|
offset = 1;
|
||||||
|
} else if (isPlatformMac() && ctrlKey) {
|
||||||
|
if (which === KeyCode.N) {
|
||||||
|
offset = 1;
|
||||||
|
} else if (which === KeyCode.P) {
|
||||||
|
offset = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset !== 0) {
|
||||||
|
const nextActiveIndex = getEnabledActiveIndex(state.activeIndex + offset, offset);
|
||||||
|
scrollIntoView(nextActiveIndex);
|
||||||
|
setActive(nextActiveIndex, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// >>> Select
|
||||||
|
case KeyCode.ENTER: {
|
||||||
|
// value
|
||||||
|
const item = memoFlattenOptions.value[state.activeIndex];
|
||||||
|
if (item && !item.data.disabled) {
|
||||||
|
onSelectValue(item.data.value);
|
||||||
|
} else {
|
||||||
|
onSelectValue(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseProps.open) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// >>> Close
|
||||||
|
case KeyCode.ESC: {
|
||||||
|
baseProps.toggleOpen(false);
|
||||||
|
if (baseProps.open) {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onKeyup = () => {};
|
||||||
|
|
||||||
|
const scrollTo = (index: number) => {
|
||||||
|
scrollIntoView(index);
|
||||||
|
};
|
||||||
|
expose({
|
||||||
|
onKeydown,
|
||||||
|
onKeyup,
|
||||||
|
scrollTo,
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
// const {
|
||||||
|
// renderItem,
|
||||||
|
// listRef,
|
||||||
|
// onListMouseDown,
|
||||||
|
// itemPrefixCls,
|
||||||
|
// setActive,
|
||||||
|
// onSelectValue,
|
||||||
|
// memoFlattenOptions,
|
||||||
|
// $slots,
|
||||||
|
// } = this as any;
|
||||||
|
const { id, notFoundContent, onPopupScroll } = baseProps;
|
||||||
|
const { menuItemSelectedIcon, rawValues, fieldNames, virtual, listHeight, listItemHeight } =
|
||||||
|
props;
|
||||||
|
|
||||||
|
const renderOption = slots.option;
|
||||||
|
const { activeIndex } = state;
|
||||||
|
const omitFieldNameList = Object.keys(fieldNames).map(key => fieldNames[key]);
|
||||||
|
// ========================== Render ==========================
|
||||||
|
if (memoFlattenOptions.value.length === 0) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="listbox"
|
||||||
|
id={`${id}_list`}
|
||||||
|
class={`${itemPrefixCls.value}-empty`}
|
||||||
|
onMousedown={onListMouseDown}
|
||||||
|
>
|
||||||
|
{notFoundContent}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div role="listbox" id={`${id}_list`} style={{ height: 0, width: 0, overflow: 'hidden' }}>
|
||||||
|
{renderItem(activeIndex - 1)}
|
||||||
|
{renderItem(activeIndex)}
|
||||||
|
{renderItem(activeIndex + 1)}
|
||||||
|
</div>
|
||||||
|
<List
|
||||||
|
itemKey="key"
|
||||||
|
ref={listRef}
|
||||||
|
data={memoFlattenOptions.value}
|
||||||
|
height={listHeight}
|
||||||
|
itemHeight={listItemHeight}
|
||||||
|
fullHeight={false}
|
||||||
|
onMousedown={onListMouseDown}
|
||||||
|
onScroll={onPopupScroll}
|
||||||
|
virtual={virtual}
|
||||||
|
v-slots={{
|
||||||
|
default: (item, itemIndex) => {
|
||||||
|
const { group, groupOption, data, label, value } = item;
|
||||||
|
const { key } = data;
|
||||||
|
// Group
|
||||||
|
if (group) {
|
||||||
|
return (
|
||||||
|
<div class={classNames(itemPrefixCls, `${itemPrefixCls.value}-group`)}>
|
||||||
|
{renderOption ? renderOption(data) : label !== undefined ? label : key}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
disabled,
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
class: cls,
|
||||||
|
className,
|
||||||
|
...otherProps
|
||||||
|
} = data;
|
||||||
|
const passedProps = omit(otherProps, omitFieldNameList);
|
||||||
|
// Option
|
||||||
|
const selected = rawValues.has(value);
|
||||||
|
|
||||||
|
const optionPrefixCls = `${itemPrefixCls.value}-option`;
|
||||||
|
const optionClassName = classNames(itemPrefixCls, optionPrefixCls, cls, className, {
|
||||||
|
[`${optionPrefixCls}-grouped`]: groupOption,
|
||||||
|
[`${optionPrefixCls}-active`]: activeIndex === itemIndex && !disabled,
|
||||||
|
[`${optionPrefixCls}-disabled`]: disabled,
|
||||||
|
[`${optionPrefixCls}-selected`]: selected,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mergedLabel = getLabel(item);
|
||||||
|
|
||||||
|
const iconVisible =
|
||||||
|
!menuItemSelectedIcon || typeof menuItemSelectedIcon === 'function' || selected;
|
||||||
|
|
||||||
|
const content = mergedLabel || value;
|
||||||
|
// https://github.com/ant-design/ant-design/issues/26717
|
||||||
|
let optionTitle =
|
||||||
|
typeof content === 'string' || typeof content === 'number'
|
||||||
|
? content.toString()
|
||||||
|
: undefined;
|
||||||
|
if (title !== undefined) {
|
||||||
|
optionTitle = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...passedProps}
|
||||||
|
aria-selected={selected}
|
||||||
|
class={optionClassName}
|
||||||
|
title={optionTitle}
|
||||||
|
onMousemove={e => {
|
||||||
|
if (otherProps.onMousemove) {
|
||||||
|
otherProps.onMousemove(e);
|
||||||
|
}
|
||||||
|
if (activeIndex === itemIndex || disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setActive(itemIndex);
|
||||||
|
}}
|
||||||
|
onClick={e => {
|
||||||
|
if (!disabled) {
|
||||||
|
onSelectValue(value);
|
||||||
|
}
|
||||||
|
if (otherProps.onClick) {
|
||||||
|
otherProps.onClick(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<div class={`${optionPrefixCls}-content`}>
|
||||||
|
{renderOption ? renderOption(data) : content}
|
||||||
|
</div>
|
||||||
|
{isValidElement(menuItemSelectedIcon) || selected}
|
||||||
|
{iconVisible && (
|
||||||
|
<TransBtn
|
||||||
|
class={`${itemPrefixCls.value}-option-state`}
|
||||||
|
customizeIcon={menuItemSelectedIcon}
|
||||||
|
customizeIconProps={{ isSelected: selected }}
|
||||||
|
>
|
||||||
|
{selected ? '✓' : null}
|
||||||
|
</TransBtn>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
></List>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default OptionList;
|
|
@ -0,0 +1,162 @@
|
||||||
|
/**
|
||||||
|
* To match accessibility requirement, we always provide an input in the component.
|
||||||
|
* Other element will not set `tabIndex` to avoid `onBlur` sequence problem.
|
||||||
|
* For focused select, we set `aria-live="polite"` to update the accessibility content.
|
||||||
|
*
|
||||||
|
* ref:
|
||||||
|
* - keyboard: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role#Keyboard_interactions
|
||||||
|
*
|
||||||
|
* New api:
|
||||||
|
* - listHeight
|
||||||
|
* - listItemHeight
|
||||||
|
* - component
|
||||||
|
*
|
||||||
|
* Remove deprecated api:
|
||||||
|
* - multiple
|
||||||
|
* - tags
|
||||||
|
* - combobox
|
||||||
|
* - firstActiveValue
|
||||||
|
* - dropdownMenuStyle
|
||||||
|
* - openClassName (Not list in api)
|
||||||
|
*
|
||||||
|
* Update:
|
||||||
|
* - `backfill` only support `combobox` mode
|
||||||
|
* - `combobox` mode not support `labelInValue` since it's meaningless
|
||||||
|
* - `getInputElement` only support `combobox` mode
|
||||||
|
* - `onChange` return OptionData instead of ReactNode
|
||||||
|
* - `filterOption` `onChange` `onSelect` accept OptionData instead of ReactNode
|
||||||
|
* - `combobox` mode trigger `onChange` will get `undefined` if no `value` match in Option
|
||||||
|
* - `combobox` mode not support `optionLabelProp`
|
||||||
|
*/
|
||||||
|
|
||||||
|
import BaseSelect, { baseSelectPropsWithoutPrivate, isMultiple } from './BaseSelect';
|
||||||
|
import type {
|
||||||
|
DisplayValueType,
|
||||||
|
RenderNode,
|
||||||
|
BaseSelectRef,
|
||||||
|
BaseSelectPropsWithoutPrivate,
|
||||||
|
BaseSelectProps,
|
||||||
|
} from './BaseSelect';
|
||||||
|
import OptionList from './OptionList';
|
||||||
|
import Option from './Option';
|
||||||
|
import OptGroup from './OptGroup';
|
||||||
|
import useOptions from './hooks/useOptions';
|
||||||
|
import SelectContext from './SelectContext';
|
||||||
|
import useId from './hooks/useId';
|
||||||
|
import useRefFunc from './hooks/useRefFunc';
|
||||||
|
import { fillFieldNames, flattenOptions, injectPropsWithOption } from './utils/valueUtil';
|
||||||
|
import warningProps from './utils/warningPropsUtil';
|
||||||
|
import { toArray } from './utils/commonUtil';
|
||||||
|
import useFilterOptions from './hooks/useFilterOptions';
|
||||||
|
import useCache from './hooks/useCache';
|
||||||
|
import type { Key } from '../_util/type';
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import type { ExtractPropTypes, PropType } from 'vue';
|
||||||
|
import PropTypes from '../_util/vue-types';
|
||||||
|
|
||||||
|
const OMIT_DOM_PROPS = ['inputValue'];
|
||||||
|
|
||||||
|
export type OnActiveValue = (
|
||||||
|
active: RawValueType,
|
||||||
|
index: number,
|
||||||
|
info?: { source?: 'keyboard' | 'mouse' },
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export type OnInternalSelect = (value: RawValueType, info: { selected: boolean }) => void;
|
||||||
|
|
||||||
|
export type RawValueType = string | number;
|
||||||
|
export interface LabelInValueType {
|
||||||
|
label: any;
|
||||||
|
value: RawValueType;
|
||||||
|
/** @deprecated `key` is useless since it should always same as `value` */
|
||||||
|
key?: Key;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DraftValueType =
|
||||||
|
| RawValueType
|
||||||
|
| LabelInValueType
|
||||||
|
| DisplayValueType
|
||||||
|
| (RawValueType | LabelInValueType | DisplayValueType)[];
|
||||||
|
|
||||||
|
export type FilterFunc<OptionType> = (inputValue: string, option?: OptionType) => boolean;
|
||||||
|
|
||||||
|
export interface FieldNames {
|
||||||
|
value?: string;
|
||||||
|
label?: string;
|
||||||
|
options?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseOptionType {
|
||||||
|
disabled?: boolean;
|
||||||
|
[name: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DefaultOptionType extends BaseOptionType {
|
||||||
|
label: any;
|
||||||
|
value?: string | number | null;
|
||||||
|
children?: Omit<DefaultOptionType, 'children'>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SelectHandler<ValueType = any, OptionType extends BaseOptionType = DefaultOptionType> =
|
||||||
|
| ((value: RawValueType | LabelInValueType, option: OptionType) => void)
|
||||||
|
| ((value: ValueType, option: OptionType) => void);
|
||||||
|
|
||||||
|
export function selectProps<
|
||||||
|
ValueType = any,
|
||||||
|
OptionType extends BaseOptionType = DefaultOptionType,
|
||||||
|
>() {
|
||||||
|
return {
|
||||||
|
...baseSelectPropsWithoutPrivate(),
|
||||||
|
prefixCls: String,
|
||||||
|
id: String,
|
||||||
|
|
||||||
|
backfill: { type: Boolean, default: undefined },
|
||||||
|
|
||||||
|
// >>> Field Names
|
||||||
|
fieldNames: Object as PropType<FieldNames>,
|
||||||
|
|
||||||
|
// >>> Search
|
||||||
|
/** @deprecated Use `searchValue` instead */
|
||||||
|
inputValue: String,
|
||||||
|
searchValue: String,
|
||||||
|
onSearch: Function as PropType<(value: string) => void>,
|
||||||
|
autoClearSearchValue: { type: Boolean, default: undefined },
|
||||||
|
|
||||||
|
// >>> Select
|
||||||
|
onSelect: Function as PropType<SelectHandler<ValueType, OptionType>>,
|
||||||
|
onDeselect: Function as PropType<SelectHandler<ValueType, OptionType>>,
|
||||||
|
|
||||||
|
// >>> Options
|
||||||
|
/**
|
||||||
|
* In Select, `false` means do nothing.
|
||||||
|
* In TreeSelect, `false` will highlight match item.
|
||||||
|
* It's by design.
|
||||||
|
*/
|
||||||
|
filterOption: {
|
||||||
|
type: [Boolean, Function] as PropType<boolean | FilterFunc<OptionType>>,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
filterSort: Function as PropType<(optionA: OptionType, optionB: OptionType) => number>,
|
||||||
|
optionFilterProp: String,
|
||||||
|
optionLabelProp: String,
|
||||||
|
children: PropTypes.any,
|
||||||
|
options: Array as PropType<OptionType[]>,
|
||||||
|
defaultActiveFirstOption: { type: Boolean, default: undefined },
|
||||||
|
virtual: { type: Boolean, default: undefined },
|
||||||
|
listHeight: Number,
|
||||||
|
listItemHeight: Number,
|
||||||
|
|
||||||
|
// >>> Icon
|
||||||
|
menuItemSelectedIcon: PropTypes.any,
|
||||||
|
|
||||||
|
mode: String as PropType<'combobox' | 'multiple' | 'tags'>,
|
||||||
|
labelInValue: { type: Boolean, default: undefined },
|
||||||
|
value: PropTypes.any,
|
||||||
|
defaultValue: PropTypes.any,
|
||||||
|
onChange: Function as PropType<(value: ValueType, option: OptionType | OptionType[]) => void>,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SelectProps = Partial<ExtractPropTypes<ReturnType<typeof selectProps>>>;
|
||||||
|
|
||||||
|
export default defineComponent({});
|
|
@ -0,0 +1,36 @@
|
||||||
|
/**
|
||||||
|
* BaseSelect provide some parsed data into context.
|
||||||
|
* You can use this hooks to get them.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { InjectionKey } from 'vue';
|
||||||
|
import { inject, provide } from 'vue';
|
||||||
|
import type { RawValueType, RenderNode } from './BaseSelect';
|
||||||
|
import type { FlattenOptionData } from './interface';
|
||||||
|
import type { BaseOptionType, FieldNames, OnActiveValue, OnInternalSelect } from './Select';
|
||||||
|
|
||||||
|
// Use any here since we do not get the type during compilation
|
||||||
|
export interface SelectContextProps {
|
||||||
|
options: BaseOptionType[];
|
||||||
|
flattenOptions: FlattenOptionData<BaseOptionType>[];
|
||||||
|
onActiveValue: OnActiveValue;
|
||||||
|
defaultActiveFirstOption?: boolean;
|
||||||
|
onSelect: OnInternalSelect;
|
||||||
|
menuItemSelectedIcon?: RenderNode;
|
||||||
|
rawValues: Set<RawValueType>;
|
||||||
|
fieldNames?: FieldNames;
|
||||||
|
virtual?: boolean;
|
||||||
|
listHeight?: number;
|
||||||
|
listItemHeight?: number;
|
||||||
|
childrenAsData?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SelectContextKey: InjectionKey<SelectContextProps> = Symbol('SelectContextKey');
|
||||||
|
|
||||||
|
export function useProvideSelectProps(props: SelectContextProps) {
|
||||||
|
return provide(SelectContextKey, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useSelectProps() {
|
||||||
|
return inject(SelectContextKey, {} as SelectContextProps);
|
||||||
|
}
|
|
@ -0,0 +1,193 @@
|
||||||
|
import Trigger from '../vc-trigger';
|
||||||
|
import PropTypes from '../_util/vue-types';
|
||||||
|
import classNames from '../_util/classNames';
|
||||||
|
import type { CSSProperties } from 'vue';
|
||||||
|
import { computed, ref, defineComponent } from 'vue';
|
||||||
|
import type { VueNode } from '../_util/type';
|
||||||
|
import type { DropdownRender, Placement, RenderDOMFunc } from './BaseSelect';
|
||||||
|
|
||||||
|
const getBuiltInPlacements = (adjustX: number) => {
|
||||||
|
return {
|
||||||
|
bottomLeft: {
|
||||||
|
points: ['tl', 'bl'],
|
||||||
|
offset: [0, 4],
|
||||||
|
overflow: {
|
||||||
|
adjustX,
|
||||||
|
adjustY: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bottomRight: {
|
||||||
|
points: ['tr', 'br'],
|
||||||
|
offset: [0, 4],
|
||||||
|
overflow: {
|
||||||
|
adjustX,
|
||||||
|
adjustY: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
topLeft: {
|
||||||
|
points: ['bl', 'tl'],
|
||||||
|
offset: [0, -4],
|
||||||
|
overflow: {
|
||||||
|
adjustX,
|
||||||
|
adjustY: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
topRight: {
|
||||||
|
points: ['br', 'tr'],
|
||||||
|
offset: [0, -4],
|
||||||
|
overflow: {
|
||||||
|
adjustX,
|
||||||
|
adjustY: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAdjustX = (
|
||||||
|
adjustXDependencies: Pick<SelectTriggerProps, 'autoAdjustOverflow' | 'dropdownMatchSelectWidth'>,
|
||||||
|
) => {
|
||||||
|
const { autoAdjustOverflow, dropdownMatchSelectWidth } = adjustXDependencies;
|
||||||
|
if (!!autoAdjustOverflow) return 1;
|
||||||
|
// Enable horizontal overflow auto-adjustment when a custom dropdown width is provided
|
||||||
|
return typeof dropdownMatchSelectWidth !== 'number' ? 0 : 1;
|
||||||
|
};
|
||||||
|
export interface RefTriggerProps {
|
||||||
|
getPopupElement: () => HTMLDivElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectTriggerProps {
|
||||||
|
prefixCls: string;
|
||||||
|
disabled: boolean;
|
||||||
|
visible: boolean;
|
||||||
|
popupElement: VueNode;
|
||||||
|
animation?: string;
|
||||||
|
transitionName?: string;
|
||||||
|
containerWidth: number;
|
||||||
|
placement?: Placement;
|
||||||
|
dropdownStyle: CSSProperties;
|
||||||
|
dropdownClassName: string;
|
||||||
|
direction: string;
|
||||||
|
dropdownMatchSelectWidth?: boolean | number;
|
||||||
|
dropdownRender?: DropdownRender;
|
||||||
|
getPopupContainer?: RenderDOMFunc;
|
||||||
|
dropdownAlign: object;
|
||||||
|
empty: boolean;
|
||||||
|
autoAdjustOverflow?: boolean;
|
||||||
|
getTriggerDOMNode: () => any;
|
||||||
|
onPopupVisibleChange?: (visible: boolean) => void;
|
||||||
|
|
||||||
|
onPopupMouseEnter: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SelectTrigger = defineComponent<SelectTriggerProps, { popupRef: any }>({
|
||||||
|
name: 'SelectTrigger',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: {
|
||||||
|
dropdownAlign: PropTypes.object,
|
||||||
|
visible: PropTypes.looseBool,
|
||||||
|
disabled: PropTypes.looseBool,
|
||||||
|
dropdownClassName: PropTypes.string,
|
||||||
|
dropdownStyle: PropTypes.object,
|
||||||
|
placement: PropTypes.string,
|
||||||
|
empty: PropTypes.looseBool,
|
||||||
|
autoAdjustOverflow: PropTypes.looseBool,
|
||||||
|
prefixCls: PropTypes.string,
|
||||||
|
popupClassName: PropTypes.string,
|
||||||
|
animation: PropTypes.string,
|
||||||
|
transitionName: PropTypes.string,
|
||||||
|
getPopupContainer: PropTypes.func,
|
||||||
|
dropdownRender: PropTypes.func,
|
||||||
|
containerWidth: PropTypes.number,
|
||||||
|
dropdownMatchSelectWidth: PropTypes.oneOfType([Number, Boolean]).def(true),
|
||||||
|
popupElement: PropTypes.any,
|
||||||
|
direction: PropTypes.string,
|
||||||
|
getTriggerDOMNode: PropTypes.func,
|
||||||
|
onPopupVisibleChange: PropTypes.func,
|
||||||
|
onPopupMouseEnter: PropTypes.func,
|
||||||
|
} as any,
|
||||||
|
setup(props, { slots, attrs, expose }) {
|
||||||
|
const builtInPlacements = computed(() => {
|
||||||
|
const { autoAdjustOverflow, dropdownMatchSelectWidth } = props;
|
||||||
|
return getBuiltInPlacements(
|
||||||
|
getAdjustX({
|
||||||
|
autoAdjustOverflow,
|
||||||
|
dropdownMatchSelectWidth,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const popupRef = ref();
|
||||||
|
expose({
|
||||||
|
getPopupElement: () => {
|
||||||
|
return popupRef.value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
const { empty = false, ...restProps } = { ...props, ...attrs };
|
||||||
|
const {
|
||||||
|
visible,
|
||||||
|
dropdownAlign,
|
||||||
|
prefixCls,
|
||||||
|
popupElement,
|
||||||
|
dropdownClassName,
|
||||||
|
dropdownStyle,
|
||||||
|
direction = 'ltr',
|
||||||
|
placement,
|
||||||
|
dropdownMatchSelectWidth,
|
||||||
|
containerWidth,
|
||||||
|
dropdownRender,
|
||||||
|
animation,
|
||||||
|
transitionName,
|
||||||
|
getPopupContainer,
|
||||||
|
getTriggerDOMNode,
|
||||||
|
onPopupVisibleChange,
|
||||||
|
onPopupMouseEnter,
|
||||||
|
} = restProps as SelectTriggerProps;
|
||||||
|
const dropdownPrefixCls = `${prefixCls}-dropdown`;
|
||||||
|
|
||||||
|
let popupNode = popupElement;
|
||||||
|
if (dropdownRender) {
|
||||||
|
popupNode = dropdownRender({ menuNode: popupElement, props });
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedTransitionName = animation ? `${dropdownPrefixCls}-${animation}` : transitionName;
|
||||||
|
|
||||||
|
const popupStyle = { minWidth: `${containerWidth}px`, ...dropdownStyle };
|
||||||
|
|
||||||
|
if (typeof dropdownMatchSelectWidth === 'number') {
|
||||||
|
popupStyle.width = `${dropdownMatchSelectWidth}px`;
|
||||||
|
} else if (dropdownMatchSelectWidth) {
|
||||||
|
popupStyle.width = `${containerWidth}px`;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Trigger
|
||||||
|
{...props}
|
||||||
|
showAction={[]}
|
||||||
|
hideAction={[]}
|
||||||
|
popupPlacement={placement || (direction === 'rtl' ? 'bottomRight' : 'bottomLeft')}
|
||||||
|
builtinPlacements={builtInPlacements.value}
|
||||||
|
prefixCls={dropdownPrefixCls}
|
||||||
|
popupTransitionName={mergedTransitionName}
|
||||||
|
popupAlign={dropdownAlign}
|
||||||
|
popupVisible={visible}
|
||||||
|
getPopupContainer={getPopupContainer}
|
||||||
|
popupClassName={classNames(dropdownClassName, {
|
||||||
|
[`${dropdownPrefixCls}-empty`]: empty,
|
||||||
|
})}
|
||||||
|
popupStyle={popupStyle}
|
||||||
|
getTriggerDOMNode={getTriggerDOMNode}
|
||||||
|
onPopupVisibleChange={onPopupVisibleChange}
|
||||||
|
v-slots={{
|
||||||
|
default: slots.default,
|
||||||
|
popup: () => (
|
||||||
|
<div ref={popupRef} onMouseenter={onPopupMouseEnter}>
|
||||||
|
{popupNode}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
></Trigger>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default SelectTrigger;
|
|
@ -0,0 +1,218 @@
|
||||||
|
import { cloneElement } from '../../_util/vnode';
|
||||||
|
import type { VNode } from 'vue';
|
||||||
|
import { defineComponent, getCurrentInstance, inject, onMounted, withDirectives } from 'vue';
|
||||||
|
import PropTypes from '../../_util/vue-types';
|
||||||
|
import type { RefObject } from '../../_util/createRef';
|
||||||
|
import antInput from '../../_util/antInputDirective';
|
||||||
|
import classNames from '../../_util/classNames';
|
||||||
|
import type { EventHandler } from '../../_util/EventInterface';
|
||||||
|
import type { VueNode } from '../../_util/type';
|
||||||
|
|
||||||
|
interface InputProps {
|
||||||
|
prefixCls: string;
|
||||||
|
id: string;
|
||||||
|
inputElement: VueNode;
|
||||||
|
disabled: boolean;
|
||||||
|
autofocus: boolean;
|
||||||
|
autocomplete: string;
|
||||||
|
editable: boolean;
|
||||||
|
activeDescendantId?: string;
|
||||||
|
value: string;
|
||||||
|
open: boolean;
|
||||||
|
tabindex: number | string;
|
||||||
|
/** Pass accessibility props to input */
|
||||||
|
attrs: object;
|
||||||
|
inputRef: RefObject;
|
||||||
|
onKeydown: EventHandler;
|
||||||
|
onMousedown: EventHandler;
|
||||||
|
onChange: EventHandler;
|
||||||
|
onPaste: EventHandler;
|
||||||
|
onCompositionstart: EventHandler;
|
||||||
|
onCompositionend: EventHandler;
|
||||||
|
onFocus: EventHandler;
|
||||||
|
onBlur: EventHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Input = defineComponent({
|
||||||
|
name: 'Input',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: {
|
||||||
|
inputRef: PropTypes.any,
|
||||||
|
prefixCls: PropTypes.string,
|
||||||
|
id: PropTypes.string,
|
||||||
|
inputElement: PropTypes.any,
|
||||||
|
disabled: PropTypes.looseBool,
|
||||||
|
autofocus: PropTypes.looseBool,
|
||||||
|
autocomplete: PropTypes.string,
|
||||||
|
editable: PropTypes.looseBool,
|
||||||
|
activeDescendantId: PropTypes.string,
|
||||||
|
value: PropTypes.string,
|
||||||
|
open: PropTypes.looseBool,
|
||||||
|
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||||
|
/** Pass accessibility props to input */
|
||||||
|
attrs: PropTypes.object,
|
||||||
|
onKeydown: PropTypes.func,
|
||||||
|
onMousedown: PropTypes.func,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
onPaste: PropTypes.func,
|
||||||
|
onCompositionstart: PropTypes.func,
|
||||||
|
onCompositionend: PropTypes.func,
|
||||||
|
onFocus: PropTypes.func,
|
||||||
|
onBlur: PropTypes.func,
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
if (process.env.NODE_ENV === 'test') {
|
||||||
|
onMounted(() => {
|
||||||
|
const ins = getCurrentInstance();
|
||||||
|
if (props.autofocus) {
|
||||||
|
if (ins.vnode && ins.vnode.el) {
|
||||||
|
ins.vnode.el.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
blurTimeout: null,
|
||||||
|
VCSelectContainerEvent: inject('VCSelectContainerEvent') as any,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
prefixCls,
|
||||||
|
id,
|
||||||
|
inputElement,
|
||||||
|
disabled,
|
||||||
|
tabindex,
|
||||||
|
autofocus,
|
||||||
|
autocomplete,
|
||||||
|
editable,
|
||||||
|
activeDescendantId,
|
||||||
|
value,
|
||||||
|
onKeydown,
|
||||||
|
onMousedown,
|
||||||
|
onChange,
|
||||||
|
onPaste,
|
||||||
|
onCompositionstart,
|
||||||
|
onCompositionend,
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
|
open,
|
||||||
|
inputRef,
|
||||||
|
attrs,
|
||||||
|
} = this.$props as InputProps;
|
||||||
|
let inputNode: any = inputElement || withDirectives((<input />) as VNode, [[antInput]]);
|
||||||
|
|
||||||
|
const inputProps = inputNode.props || {};
|
||||||
|
const {
|
||||||
|
onKeydown: onOriginKeyDown,
|
||||||
|
onInput: onOriginInput,
|
||||||
|
onFocus: onOriginFocus,
|
||||||
|
onBlur: onOriginBlur,
|
||||||
|
onMousedown: onOriginMouseDown,
|
||||||
|
onCompositionstart: onOriginCompositionStart,
|
||||||
|
onCompositionend: onOriginCompositionEnd,
|
||||||
|
style,
|
||||||
|
} = inputProps;
|
||||||
|
inputNode = cloneElement(
|
||||||
|
inputNode,
|
||||||
|
Object.assign(
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
ref: inputRef,
|
||||||
|
disabled,
|
||||||
|
tabindex,
|
||||||
|
autocomplete: autocomplete || 'off',
|
||||||
|
autofocus,
|
||||||
|
class: classNames(`${prefixCls}-selection-search-input`, inputNode?.props?.className),
|
||||||
|
style: { ...style, opacity: editable ? null : 0 },
|
||||||
|
role: 'combobox',
|
||||||
|
'aria-expanded': open,
|
||||||
|
'aria-haspopup': 'listbox',
|
||||||
|
'aria-owns': `${id}_list`,
|
||||||
|
'aria-autocomplete': 'list',
|
||||||
|
'aria-controls': `${id}_list`,
|
||||||
|
'aria-activedescendant': activeDescendantId,
|
||||||
|
...attrs,
|
||||||
|
value: editable ? value : '',
|
||||||
|
readonly: !editable,
|
||||||
|
unselectable: !editable ? 'on' : null,
|
||||||
|
onKeydown: (event: KeyboardEvent) => {
|
||||||
|
onKeydown(event);
|
||||||
|
if (onOriginKeyDown) {
|
||||||
|
onOriginKeyDown(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMousedown: (event: MouseEvent) => {
|
||||||
|
onMousedown(event);
|
||||||
|
if (onOriginMouseDown) {
|
||||||
|
onOriginMouseDown(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onInput: (event: Event) => {
|
||||||
|
onChange(event);
|
||||||
|
if (onOriginInput) {
|
||||||
|
onOriginInput(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCompositionstart(event: CompositionEvent) {
|
||||||
|
onCompositionstart(event);
|
||||||
|
if (onOriginCompositionStart) {
|
||||||
|
onOriginCompositionStart(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCompositionend(event: CompositionEvent) {
|
||||||
|
onCompositionend(event);
|
||||||
|
if (onOriginCompositionEnd) {
|
||||||
|
onOriginCompositionEnd(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPaste,
|
||||||
|
onFocus: (...args: any[]) => {
|
||||||
|
clearTimeout(this.blurTimeout);
|
||||||
|
onOriginFocus && onOriginFocus(args[0]);
|
||||||
|
onFocus && onFocus(args[0]);
|
||||||
|
this.VCSelectContainerEvent?.focus(args[0]);
|
||||||
|
},
|
||||||
|
onBlur: (...args: any[]) => {
|
||||||
|
this.blurTimeout = setTimeout(() => {
|
||||||
|
onOriginBlur && onOriginBlur(args[0]);
|
||||||
|
onBlur && onBlur(args[0]);
|
||||||
|
this.VCSelectContainerEvent?.blur(args[0]);
|
||||||
|
}, 200);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputNode.type === 'textarea' ? {} : { type: 'search' },
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
) as VNode;
|
||||||
|
return inputNode;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Input.props = {
|
||||||
|
// inputRef: PropTypes.any,
|
||||||
|
// prefixCls: PropTypes.string,
|
||||||
|
// id: PropTypes.string,
|
||||||
|
// inputElement: PropTypes.any,
|
||||||
|
// disabled: PropTypes.looseBool,
|
||||||
|
// autofocus: PropTypes.looseBool,
|
||||||
|
// autocomplete: PropTypes.string,
|
||||||
|
// editable: PropTypes.looseBool,
|
||||||
|
// accessibilityIndex: PropTypes.number,
|
||||||
|
// value: PropTypes.string,
|
||||||
|
// open: PropTypes.looseBool,
|
||||||
|
// tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||||
|
// /** Pass accessibility props to input */
|
||||||
|
// attrs: PropTypes.object,
|
||||||
|
// onKeydown: PropTypes.func,
|
||||||
|
// onMousedown: PropTypes.func,
|
||||||
|
// onChange: PropTypes.func,
|
||||||
|
// onPaste: PropTypes.func,
|
||||||
|
// onCompositionstart: PropTypes.func,
|
||||||
|
// onCompositionend: PropTypes.func,
|
||||||
|
// onFocus: PropTypes.func,
|
||||||
|
// onBlur: PropTypes.func,
|
||||||
|
// };
|
||||||
|
|
||||||
|
export default Input;
|
|
@ -0,0 +1,281 @@
|
||||||
|
import TransBtn from '../TransBtn';
|
||||||
|
import type { InnerSelectorProps } from './interface';
|
||||||
|
import Input from './Input';
|
||||||
|
import type { Ref, PropType } from 'vue';
|
||||||
|
import { computed, defineComponent, onMounted, ref, watch } from 'vue';
|
||||||
|
import classNames from '../../_util/classNames';
|
||||||
|
import pickAttrs from '../../_util/pickAttrs';
|
||||||
|
import PropTypes from '../../_util/vue-types';
|
||||||
|
import type { VueNode } from '../../_util/type';
|
||||||
|
import Overflow from '../../vc-overflow';
|
||||||
|
import type { DisplayValueType, RenderNode, CustomTagProps, RawValueType } from '../BaseSelect';
|
||||||
|
|
||||||
|
type SelectorProps = InnerSelectorProps & {
|
||||||
|
// Icon
|
||||||
|
removeIcon?: RenderNode;
|
||||||
|
|
||||||
|
// Tags
|
||||||
|
maxTagCount?: number | 'responsive';
|
||||||
|
maxTagTextLength?: number;
|
||||||
|
maxTagPlaceholder?: VueNode | ((omittedValues: DisplayValueType[]) => VueNode);
|
||||||
|
tokenSeparators?: string[];
|
||||||
|
tagRender?: (props: CustomTagProps) => VueNode;
|
||||||
|
onToggleOpen: any;
|
||||||
|
|
||||||
|
// Motion
|
||||||
|
choiceTransitionName?: string;
|
||||||
|
|
||||||
|
// Event
|
||||||
|
onRemove: (value: DisplayValueType) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
id: PropTypes.string,
|
||||||
|
prefixCls: PropTypes.string,
|
||||||
|
values: PropTypes.array,
|
||||||
|
open: PropTypes.looseBool,
|
||||||
|
searchValue: PropTypes.string,
|
||||||
|
inputRef: PropTypes.any,
|
||||||
|
placeholder: PropTypes.any,
|
||||||
|
disabled: PropTypes.looseBool,
|
||||||
|
mode: PropTypes.string,
|
||||||
|
showSearch: PropTypes.looseBool,
|
||||||
|
autofocus: PropTypes.looseBool,
|
||||||
|
autocomplete: PropTypes.string,
|
||||||
|
activeDescendantId: PropTypes.string,
|
||||||
|
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||||
|
|
||||||
|
removeIcon: PropTypes.any,
|
||||||
|
choiceTransitionName: PropTypes.string,
|
||||||
|
|
||||||
|
maxTagCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||||
|
maxTagTextLength: PropTypes.number,
|
||||||
|
maxTagPlaceholder: PropTypes.any.def(
|
||||||
|
() => (omittedValues: DisplayValueType[]) => `+ ${omittedValues.length} ...`,
|
||||||
|
),
|
||||||
|
tagRender: PropTypes.func,
|
||||||
|
|
||||||
|
onToggleOpen: { type: Function as PropType<(open?: boolean) => void> },
|
||||||
|
onRemove: PropTypes.func,
|
||||||
|
onInputChange: PropTypes.func,
|
||||||
|
onInputPaste: PropTypes.func,
|
||||||
|
onInputKeyDown: PropTypes.func,
|
||||||
|
onInputMouseDown: PropTypes.func,
|
||||||
|
onInputCompositionStart: PropTypes.func,
|
||||||
|
onInputCompositionEnd: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPreventMouseDown = (event: MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const SelectSelector = defineComponent<SelectorProps>({
|
||||||
|
name: 'MultipleSelectSelector',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: props as any,
|
||||||
|
setup(props) {
|
||||||
|
const measureRef = ref();
|
||||||
|
const inputWidth = ref(0);
|
||||||
|
const focused = ref(false);
|
||||||
|
|
||||||
|
const selectionPrefixCls = computed(() => `${props.prefixCls}-selection`);
|
||||||
|
|
||||||
|
// ===================== Search ======================
|
||||||
|
const inputValue = computed(() =>
|
||||||
|
props.open || props.mode === 'tags' ? props.searchValue : '',
|
||||||
|
);
|
||||||
|
const inputEditable: Ref<boolean> = computed(
|
||||||
|
() =>
|
||||||
|
props.mode === 'tags' || ((props.showSearch && (props.open || focused.value)) as boolean),
|
||||||
|
);
|
||||||
|
|
||||||
|
// We measure width and set to the input immediately
|
||||||
|
onMounted(() => {
|
||||||
|
watch(
|
||||||
|
inputValue,
|
||||||
|
() => {
|
||||||
|
inputWidth.value = measureRef.value.scrollWidth;
|
||||||
|
},
|
||||||
|
{ flush: 'post', immediate: true },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===================== Render ======================
|
||||||
|
// >>> Render Selector Node. Includes Item & Rest
|
||||||
|
function defaultRenderSelector(
|
||||||
|
title: VueNode,
|
||||||
|
content: VueNode,
|
||||||
|
itemDisabled: boolean,
|
||||||
|
closable?: boolean,
|
||||||
|
onClose?: (e: MouseEvent) => void,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
class={classNames(`${selectionPrefixCls.value}-item`, {
|
||||||
|
[`${selectionPrefixCls.value}-item-disabled`]: itemDisabled,
|
||||||
|
})}
|
||||||
|
title={
|
||||||
|
typeof title === 'string' || typeof title === 'number' ? title.toString() : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span class={`${selectionPrefixCls.value}-item-content`}>{content}</span>
|
||||||
|
{closable && (
|
||||||
|
<TransBtn
|
||||||
|
class={`${selectionPrefixCls.value}-item-remove`}
|
||||||
|
onMousedown={onPreventMouseDown}
|
||||||
|
onClick={onClose}
|
||||||
|
customizeIcon={props.removeIcon}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</TransBtn>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function customizeRenderSelector(
|
||||||
|
value: RawValueType,
|
||||||
|
content: VueNode,
|
||||||
|
itemDisabled: boolean,
|
||||||
|
closable: boolean,
|
||||||
|
onClose: (e: MouseEvent) => void,
|
||||||
|
) {
|
||||||
|
const onMouseDown = (e: MouseEvent) => {
|
||||||
|
onPreventMouseDown(e);
|
||||||
|
props.onToggleOpen(!open);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span onMousedown={onMouseDown}>
|
||||||
|
{props.tagRender({
|
||||||
|
label: content,
|
||||||
|
value,
|
||||||
|
disabled: itemDisabled,
|
||||||
|
closable,
|
||||||
|
onClose,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderItem(valueItem: DisplayValueType) {
|
||||||
|
const { disabled: itemDisabled, label, value } = valueItem;
|
||||||
|
const closable = !props.disabled && !itemDisabled;
|
||||||
|
|
||||||
|
let displayLabel = label;
|
||||||
|
|
||||||
|
if (typeof props.maxTagTextLength === 'number') {
|
||||||
|
if (typeof label === 'string' || typeof label === 'number') {
|
||||||
|
const strLabel = String(displayLabel);
|
||||||
|
|
||||||
|
if (strLabel.length > props.maxTagTextLength) {
|
||||||
|
displayLabel = `${strLabel.slice(0, props.maxTagTextLength)}...`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const onClose = (event?: MouseEvent) => {
|
||||||
|
if (event) event.stopPropagation();
|
||||||
|
props.onRemove?.(valueItem);
|
||||||
|
};
|
||||||
|
|
||||||
|
return typeof props.tagRender === 'function'
|
||||||
|
? customizeRenderSelector(value, displayLabel, itemDisabled, closable, onClose)
|
||||||
|
: defaultRenderSelector(label, displayLabel, itemDisabled, closable, onClose);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRest(omittedValues: DisplayValueType[]) {
|
||||||
|
const { maxTagPlaceholder = omittedValues => `+ ${omittedValues.length} ...` } = props;
|
||||||
|
const content =
|
||||||
|
typeof maxTagPlaceholder === 'function'
|
||||||
|
? maxTagPlaceholder(omittedValues)
|
||||||
|
: maxTagPlaceholder;
|
||||||
|
|
||||||
|
return defaultRenderSelector(content, content, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
prefixCls,
|
||||||
|
values,
|
||||||
|
open,
|
||||||
|
inputRef,
|
||||||
|
placeholder,
|
||||||
|
disabled,
|
||||||
|
autofocus,
|
||||||
|
autocomplete,
|
||||||
|
activeDescendantId,
|
||||||
|
tabindex,
|
||||||
|
onInputChange,
|
||||||
|
onInputPaste,
|
||||||
|
onInputKeyDown,
|
||||||
|
onInputMouseDown,
|
||||||
|
onInputCompositionStart,
|
||||||
|
onInputCompositionEnd,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
// >>> Input Node
|
||||||
|
const inputNode = (
|
||||||
|
<div
|
||||||
|
class={`${selectionPrefixCls.value}-search`}
|
||||||
|
style={{ width: inputWidth.value + 'px' }}
|
||||||
|
key="input"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
inputRef={inputRef}
|
||||||
|
open={open}
|
||||||
|
prefixCls={prefixCls}
|
||||||
|
id={id}
|
||||||
|
inputElement={null}
|
||||||
|
disabled={disabled}
|
||||||
|
autofocus={autofocus}
|
||||||
|
autocomplete={autocomplete}
|
||||||
|
editable={inputEditable.value}
|
||||||
|
activeDescendantId={activeDescendantId}
|
||||||
|
value={inputValue.value}
|
||||||
|
onKeydown={onInputKeyDown}
|
||||||
|
onMousedown={onInputMouseDown}
|
||||||
|
onChange={onInputChange}
|
||||||
|
onPaste={onInputPaste}
|
||||||
|
onCompositionstart={onInputCompositionStart}
|
||||||
|
onCompositionend={onInputCompositionEnd}
|
||||||
|
tabindex={tabindex}
|
||||||
|
attrs={pickAttrs(props, true)}
|
||||||
|
onFocus={() => (focused.value = true)}
|
||||||
|
onBlur={() => (focused.value = false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Measure Node */}
|
||||||
|
<span ref={measureRef} class={`${selectionPrefixCls.value}-search-mirror`} aria-hidden>
|
||||||
|
{inputValue.value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// >>> Selections
|
||||||
|
const selectionNode = (
|
||||||
|
<Overflow
|
||||||
|
prefixCls={`${selectionPrefixCls.value}-overflow`}
|
||||||
|
data={values}
|
||||||
|
renderItem={renderItem}
|
||||||
|
renderRest={renderRest}
|
||||||
|
suffix={inputNode}
|
||||||
|
itemKey="key"
|
||||||
|
maxCount={props.maxTagCount}
|
||||||
|
key="overflow"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{selectionNode}
|
||||||
|
{!values.length && !inputValue.value && (
|
||||||
|
<span class={`${selectionPrefixCls.value}-placeholder`}>{placeholder}</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default SelectSelector;
|
|
@ -0,0 +1,172 @@
|
||||||
|
import pickAttrs from '../../_util/pickAttrs';
|
||||||
|
import Input from './Input';
|
||||||
|
import type { InnerSelectorProps } from './interface';
|
||||||
|
import { Fragment, computed, defineComponent, ref, watch } from 'vue';
|
||||||
|
import PropTypes from '../../_util/vue-types';
|
||||||
|
import { useInjectTreeSelectContext } from '../../vc-tree-select/Context';
|
||||||
|
import type { VueNode } from '../../_util/type';
|
||||||
|
|
||||||
|
interface SelectorProps extends InnerSelectorProps {
|
||||||
|
inputElement: VueNode;
|
||||||
|
activeValue: string;
|
||||||
|
}
|
||||||
|
const props = {
|
||||||
|
inputElement: PropTypes.any,
|
||||||
|
id: PropTypes.string,
|
||||||
|
prefixCls: PropTypes.string,
|
||||||
|
values: PropTypes.array,
|
||||||
|
open: PropTypes.looseBool,
|
||||||
|
searchValue: PropTypes.string,
|
||||||
|
inputRef: PropTypes.any,
|
||||||
|
placeholder: PropTypes.any,
|
||||||
|
disabled: PropTypes.looseBool,
|
||||||
|
mode: PropTypes.string,
|
||||||
|
showSearch: PropTypes.looseBool,
|
||||||
|
autofocus: PropTypes.looseBool,
|
||||||
|
autocomplete: PropTypes.string,
|
||||||
|
activeDescendantId: PropTypes.string,
|
||||||
|
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||||
|
activeValue: PropTypes.string,
|
||||||
|
backfill: PropTypes.looseBool,
|
||||||
|
onInputChange: PropTypes.func,
|
||||||
|
onInputPaste: PropTypes.func,
|
||||||
|
onInputKeyDown: PropTypes.func,
|
||||||
|
onInputMouseDown: PropTypes.func,
|
||||||
|
onInputCompositionStart: PropTypes.func,
|
||||||
|
onInputCompositionEnd: PropTypes.func,
|
||||||
|
};
|
||||||
|
const SingleSelector = defineComponent<SelectorProps>({
|
||||||
|
name: 'SingleSelector',
|
||||||
|
setup(props) {
|
||||||
|
const inputChanged = ref(false);
|
||||||
|
|
||||||
|
const combobox = computed(() => props.mode === 'combobox');
|
||||||
|
const inputEditable = computed(() => combobox.value || props.showSearch);
|
||||||
|
|
||||||
|
const inputValue = computed(() => {
|
||||||
|
let inputValue: string = props.searchValue || '';
|
||||||
|
if (combobox.value && props.activeValue && !inputChanged.value) {
|
||||||
|
inputValue = props.activeValue;
|
||||||
|
}
|
||||||
|
return inputValue;
|
||||||
|
});
|
||||||
|
const treeSelectContext = useInjectTreeSelectContext();
|
||||||
|
watch(
|
||||||
|
[combobox, () => props.activeValue],
|
||||||
|
() => {
|
||||||
|
if (combobox.value) {
|
||||||
|
inputChanged.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Not show text when closed expect combobox mode
|
||||||
|
const hasTextInput = computed(() =>
|
||||||
|
props.mode !== 'combobox' && !props.open && !props.showSearch ? false : !!inputValue.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
const title = computed(() => {
|
||||||
|
const item = props.values[0];
|
||||||
|
return item && (typeof item.label === 'string' || typeof item.label === 'number')
|
||||||
|
? item.label.toString()
|
||||||
|
: undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderPlaceholder = () => {
|
||||||
|
if (props.values[0]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const hiddenStyle = hasTextInput.value ? { visibility: 'hidden' as const } : undefined;
|
||||||
|
return (
|
||||||
|
<span class={`${props.prefixCls}-selection-placeholder`} style={hiddenStyle}>
|
||||||
|
{props.placeholder}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const {
|
||||||
|
inputElement,
|
||||||
|
prefixCls,
|
||||||
|
id,
|
||||||
|
values,
|
||||||
|
inputRef,
|
||||||
|
disabled,
|
||||||
|
autofocus,
|
||||||
|
autocomplete,
|
||||||
|
activeDescendantId,
|
||||||
|
open,
|
||||||
|
tabindex,
|
||||||
|
onInputKeyDown,
|
||||||
|
onInputMouseDown,
|
||||||
|
onInputChange,
|
||||||
|
onInputPaste,
|
||||||
|
onInputCompositionStart,
|
||||||
|
onInputCompositionEnd,
|
||||||
|
} = props;
|
||||||
|
const item = values[0];
|
||||||
|
let titleNode = null;
|
||||||
|
// custom tree-select title by slot
|
||||||
|
if (item && treeSelectContext.value.slots) {
|
||||||
|
titleNode =
|
||||||
|
treeSelectContext.value.slots[item?.option?.data?.slots?.title] ||
|
||||||
|
treeSelectContext.value.slots.title ||
|
||||||
|
item.label;
|
||||||
|
if (typeof titleNode === 'function') {
|
||||||
|
titleNode = titleNode(item.option?.data || {});
|
||||||
|
}
|
||||||
|
// else if (treeSelectContext.value.slots.titleRender) {
|
||||||
|
// // 因历史 title 是覆盖逻辑,新增 titleRender,所有的 title 都走一遍 titleRender
|
||||||
|
// titleNode = treeSelectContext.value.slots.titleRender(item.option?.data || {});
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
titleNode = item?.label;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span class={`${prefixCls}-selection-search`}>
|
||||||
|
<Input
|
||||||
|
inputRef={inputRef}
|
||||||
|
prefixCls={prefixCls}
|
||||||
|
id={id}
|
||||||
|
open={open}
|
||||||
|
inputElement={inputElement}
|
||||||
|
disabled={disabled}
|
||||||
|
autofocus={autofocus}
|
||||||
|
autocomplete={autocomplete}
|
||||||
|
editable={inputEditable.value}
|
||||||
|
activeDescendantId={activeDescendantId}
|
||||||
|
value={inputValue.value}
|
||||||
|
onKeydown={onInputKeyDown}
|
||||||
|
onMousedown={onInputMouseDown}
|
||||||
|
onChange={e => {
|
||||||
|
inputChanged.value = true;
|
||||||
|
onInputChange(e as any);
|
||||||
|
}}
|
||||||
|
onPaste={onInputPaste}
|
||||||
|
onCompositionstart={onInputCompositionStart}
|
||||||
|
onCompositionend={onInputCompositionEnd}
|
||||||
|
tabindex={tabindex}
|
||||||
|
attrs={pickAttrs(props, true)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Display value */}
|
||||||
|
{!combobox.value && item && !hasTextInput.value && (
|
||||||
|
<span class={`${prefixCls}-selection-item`} title={title.value}>
|
||||||
|
<Fragment key={item.key || item.value}>{titleNode}</Fragment>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Display placeholder */}
|
||||||
|
{renderPlaceholder()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
SingleSelector.props = props;
|
||||||
|
SingleSelector.inheritAttrs = false;
|
||||||
|
|
||||||
|
export default SingleSelector;
|
|
@ -0,0 +1,275 @@
|
||||||
|
/**
|
||||||
|
* Cursor rule:
|
||||||
|
* 1. Only `showSearch` enabled
|
||||||
|
* 2. Only `open` is `true`
|
||||||
|
* 3. When typing, set `open` to `true` which hit rule of 2
|
||||||
|
*
|
||||||
|
* Accessibility:
|
||||||
|
* - https://www.w3.org/TR/wai-aria-practices/examples/combobox/aria1.1pattern/listbox-combo.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
import KeyCode from '../../_util/KeyCode';
|
||||||
|
import MultipleSelector from './MultipleSelector';
|
||||||
|
import SingleSelector from './SingleSelector';
|
||||||
|
import type { CustomTagProps, DisplayValueType, Mode, RenderNode } from '../BaseSelect';
|
||||||
|
import { isValidateOpenKey } from '../utils/keyUtil';
|
||||||
|
import useLock from '../hooks/useLock';
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import createRef from '../../_util/createRef';
|
||||||
|
import PropTypes from '../../_util/vue-types';
|
||||||
|
import type { VueNode } from '../../_util/type';
|
||||||
|
import type { EventHandler } from '../../_util/EventInterface';
|
||||||
|
import type { ScrollTo } from '../../vc-virtual-list/List';
|
||||||
|
|
||||||
|
export interface SelectorProps {
|
||||||
|
id: string;
|
||||||
|
prefixCls: string;
|
||||||
|
showSearch?: boolean;
|
||||||
|
open: boolean;
|
||||||
|
values: DisplayValueType[];
|
||||||
|
multiple?: boolean;
|
||||||
|
mode: Mode;
|
||||||
|
searchValue: string;
|
||||||
|
activeValue: string;
|
||||||
|
inputElement: VueNode;
|
||||||
|
|
||||||
|
autofocus?: boolean;
|
||||||
|
activeDescendantId?: string;
|
||||||
|
tabindex?: number | string;
|
||||||
|
disabled?: boolean;
|
||||||
|
placeholder?: VueNode;
|
||||||
|
removeIcon?: RenderNode;
|
||||||
|
|
||||||
|
// Tags
|
||||||
|
maxTagCount?: number | 'responsive';
|
||||||
|
maxTagTextLength?: number;
|
||||||
|
maxTagPlaceholder?: VueNode | ((omittedValues: DisplayValueType[]) => VueNode);
|
||||||
|
tagRender?: (props: CustomTagProps) => VueNode;
|
||||||
|
|
||||||
|
/** Check if `tokenSeparators` contains `\n` or `\r\n` */
|
||||||
|
tokenWithEnter?: boolean;
|
||||||
|
|
||||||
|
// Motion
|
||||||
|
choiceTransitionName?: string;
|
||||||
|
|
||||||
|
onToggleOpen: (open?: boolean) => void | any;
|
||||||
|
/** `onSearch` returns go next step boolean to check if need do toggle open */
|
||||||
|
onSearch: (searchText: string, fromTyping: boolean, isCompositing: boolean) => boolean;
|
||||||
|
onSearchSubmit: (searchText: string) => void;
|
||||||
|
onRemove: (value: DisplayValueType) => void;
|
||||||
|
onInputKeyDown?: (e: KeyboardEvent) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private get real dom for trigger align.
|
||||||
|
* This may be removed after React provides replacement of `findDOMNode`
|
||||||
|
*/
|
||||||
|
domRef: () => HTMLDivElement;
|
||||||
|
}
|
||||||
|
export interface RefSelectorProps {
|
||||||
|
focus: () => void;
|
||||||
|
blur: () => void;
|
||||||
|
scrollTo?: ScrollTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Selector = defineComponent<SelectorProps>({
|
||||||
|
name: 'Selector',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: {
|
||||||
|
id: PropTypes.string,
|
||||||
|
prefixCls: PropTypes.string,
|
||||||
|
showSearch: PropTypes.looseBool,
|
||||||
|
open: PropTypes.looseBool,
|
||||||
|
/** Display in the Selector value, it's not same as `value` prop */
|
||||||
|
values: PropTypes.array,
|
||||||
|
multiple: PropTypes.looseBool,
|
||||||
|
mode: PropTypes.string,
|
||||||
|
searchValue: PropTypes.string,
|
||||||
|
activeValue: PropTypes.string,
|
||||||
|
inputElement: PropTypes.any,
|
||||||
|
|
||||||
|
autofocus: PropTypes.looseBool,
|
||||||
|
activeDescendantId: PropTypes.string,
|
||||||
|
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||||
|
disabled: PropTypes.looseBool,
|
||||||
|
placeholder: PropTypes.any,
|
||||||
|
removeIcon: PropTypes.any,
|
||||||
|
|
||||||
|
// Tags
|
||||||
|
maxTagCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||||
|
maxTagTextLength: PropTypes.number,
|
||||||
|
maxTagPlaceholder: PropTypes.any,
|
||||||
|
tagRender: PropTypes.func,
|
||||||
|
|
||||||
|
/** Check if `tokenSeparators` contains `\n` or `\r\n` */
|
||||||
|
tokenWithEnter: PropTypes.looseBool,
|
||||||
|
|
||||||
|
// Motion
|
||||||
|
choiceTransitionName: PropTypes.string,
|
||||||
|
|
||||||
|
onToggleOpen: { type: Function as PropType<(open?: boolean) => void> },
|
||||||
|
/** `onSearch` returns go next step boolean to check if need do toggle open */
|
||||||
|
onSearch: PropTypes.func,
|
||||||
|
onSearchSubmit: PropTypes.func,
|
||||||
|
onRemove: PropTypes.func,
|
||||||
|
onInputKeyDown: { type: Function as PropType<EventHandler> },
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private get real dom for trigger align.
|
||||||
|
* This may be removed after React provides replacement of `findDOMNode`
|
||||||
|
*/
|
||||||
|
domRef: PropTypes.func,
|
||||||
|
} as any,
|
||||||
|
setup(props, { expose }) {
|
||||||
|
const inputRef = createRef();
|
||||||
|
let compositionStatus = false;
|
||||||
|
|
||||||
|
// ====================== Input ======================
|
||||||
|
const [getInputMouseDown, setInputMouseDown] = useLock(0);
|
||||||
|
|
||||||
|
const onInternalInputKeyDown = (event: KeyboardEvent) => {
|
||||||
|
const { which } = event;
|
||||||
|
|
||||||
|
if (which === KeyCode.UP || which === KeyCode.DOWN) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.onInputKeyDown) {
|
||||||
|
props.onInputKeyDown(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (which === KeyCode.ENTER && props.mode === 'tags' && !compositionStatus && !props.open) {
|
||||||
|
// When menu isn't open, OptionList won't trigger a value change
|
||||||
|
// So when enter is pressed, the tag's input value should be emitted here to let selector know
|
||||||
|
props.onSearchSubmit((event.target as HTMLInputElement).value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValidateOpenKey(which)) {
|
||||||
|
props.onToggleOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We can not use `findDOMNode` sine it will get warning,
|
||||||
|
* have to use timer to check if is input element.
|
||||||
|
*/
|
||||||
|
const onInternalInputMouseDown = () => {
|
||||||
|
setInputMouseDown(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// When paste come, ignore next onChange
|
||||||
|
let pastedText = null;
|
||||||
|
|
||||||
|
const triggerOnSearch = (value: string) => {
|
||||||
|
if (props.onSearch(value, true, compositionStatus) !== false) {
|
||||||
|
props.onToggleOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInputCompositionStart = () => {
|
||||||
|
compositionStatus = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInputCompositionEnd = (e: InputEvent) => {
|
||||||
|
compositionStatus = false;
|
||||||
|
// Trigger search again to support `tokenSeparators` with typewriting
|
||||||
|
if (props.mode !== 'combobox') {
|
||||||
|
triggerOnSearch((e.target as HTMLInputElement).value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInputChange = (event: { target: { value: any } }) => {
|
||||||
|
let {
|
||||||
|
target: { value },
|
||||||
|
} = event;
|
||||||
|
|
||||||
|
// Pasted text should replace back to origin content
|
||||||
|
if (props.tokenWithEnter && pastedText && /[\r\n]/.test(pastedText)) {
|
||||||
|
// CRLF will be treated as a single space for input element
|
||||||
|
const replacedText = pastedText
|
||||||
|
.replace(/[\r\n]+$/, '')
|
||||||
|
.replace(/\r\n/g, ' ')
|
||||||
|
.replace(/[\r\n]/g, ' ');
|
||||||
|
value = value.replace(replacedText, pastedText);
|
||||||
|
}
|
||||||
|
|
||||||
|
pastedText = null;
|
||||||
|
|
||||||
|
triggerOnSearch(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInputPaste = (e: ClipboardEvent) => {
|
||||||
|
const { clipboardData } = e;
|
||||||
|
const value = clipboardData.getData('text');
|
||||||
|
|
||||||
|
pastedText = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClick = ({ target }) => {
|
||||||
|
if (target !== inputRef.current) {
|
||||||
|
// Should focus input if click the selector
|
||||||
|
const isIE = (document.body.style as any).msTouchAction !== undefined;
|
||||||
|
if (isIE) {
|
||||||
|
setTimeout(() => {
|
||||||
|
inputRef.current.focus();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMousedown = (event: MouseEvent) => {
|
||||||
|
const inputMouseDown = getInputMouseDown();
|
||||||
|
if (event.target !== inputRef.current && !inputMouseDown) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((props.mode !== 'combobox' && (!props.showSearch || !inputMouseDown)) || !props.open) {
|
||||||
|
if (props.open) {
|
||||||
|
props.onSearch('', true, false);
|
||||||
|
}
|
||||||
|
props.onToggleOpen();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expose({
|
||||||
|
focus: () => {
|
||||||
|
inputRef.current.focus();
|
||||||
|
},
|
||||||
|
blur: () => {
|
||||||
|
inputRef.current.blur();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const { prefixCls, domRef, mode } = props as SelectorProps;
|
||||||
|
const sharedProps = {
|
||||||
|
inputRef,
|
||||||
|
onInputKeyDown: onInternalInputKeyDown,
|
||||||
|
onInputMouseDown: onInternalInputMouseDown,
|
||||||
|
onInputChange,
|
||||||
|
onInputPaste,
|
||||||
|
onInputCompositionStart,
|
||||||
|
onInputCompositionEnd,
|
||||||
|
};
|
||||||
|
const selectNode =
|
||||||
|
mode === 'multiple' || mode === 'tags' ? (
|
||||||
|
<MultipleSelector {...props} {...sharedProps} />
|
||||||
|
) : (
|
||||||
|
<SingleSelector {...props} {...sharedProps} />
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={domRef}
|
||||||
|
class={`${prefixCls}-selector`}
|
||||||
|
onClick={onClick}
|
||||||
|
onMousedown={onMousedown}
|
||||||
|
>
|
||||||
|
{selectNode}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Selector;
|
|
@ -0,0 +1,28 @@
|
||||||
|
import type { RefObject } from '../../_util/createRef';
|
||||||
|
|
||||||
|
import type { EventHandler } from '../../_util/EventInterface';
|
||||||
|
import type { VueNode } from '../../_util/type';
|
||||||
|
import type { Mode, DisplayValueType } from '../BaseSelect';
|
||||||
|
|
||||||
|
export interface InnerSelectorProps {
|
||||||
|
prefixCls: string;
|
||||||
|
id: string;
|
||||||
|
mode: Mode;
|
||||||
|
inputRef: RefObject;
|
||||||
|
placeholder?: VueNode;
|
||||||
|
disabled?: boolean;
|
||||||
|
autofocus?: boolean;
|
||||||
|
autocomplete?: string;
|
||||||
|
values: DisplayValueType[];
|
||||||
|
showSearch?: boolean;
|
||||||
|
searchValue: string;
|
||||||
|
activeDescendantId: string;
|
||||||
|
open: boolean;
|
||||||
|
tabindex?: number | string;
|
||||||
|
onInputKeyDown: EventHandler;
|
||||||
|
onInputMouseDown: EventHandler;
|
||||||
|
onInputChange: EventHandler;
|
||||||
|
onInputPaste: EventHandler;
|
||||||
|
onInputCompositionStart: EventHandler;
|
||||||
|
onInputCompositionEnd: EventHandler;
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
import type { FunctionalComponent } from 'vue';
|
||||||
|
import type { VueNode } from '../_util/type';
|
||||||
|
import PropTypes from '../_util/vue-types';
|
||||||
|
import type { RenderNode } from './BaseSelect';
|
||||||
|
|
||||||
|
export interface TransBtnProps {
|
||||||
|
class: string;
|
||||||
|
customizeIcon: RenderNode;
|
||||||
|
customizeIconProps?: any;
|
||||||
|
onMousedown?: (payload: MouseEvent) => void;
|
||||||
|
onClick?: (payload: MouseEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransBtnType extends FunctionalComponent<TransBtnProps> {
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TransBtn: TransBtnType = (props, { slots }) => {
|
||||||
|
const { class: className, customizeIcon, customizeIconProps, onMousedown, onClick } = props;
|
||||||
|
let icon: VueNode;
|
||||||
|
|
||||||
|
if (typeof customizeIcon === 'function') {
|
||||||
|
icon = customizeIcon(customizeIconProps);
|
||||||
|
} else {
|
||||||
|
icon = customizeIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
class={className}
|
||||||
|
onMousedown={event => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (onMousedown) {
|
||||||
|
onMousedown(event);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
userSelect: 'none',
|
||||||
|
WebkitUserSelect: 'none',
|
||||||
|
}}
|
||||||
|
unselectable="on"
|
||||||
|
onClick={onClick}
|
||||||
|
aria-hidden
|
||||||
|
>
|
||||||
|
{icon !== undefined ? (
|
||||||
|
icon
|
||||||
|
) : (
|
||||||
|
<span class={className.split(/\s+/).map((cls: any) => `${cls}-icon`)}>
|
||||||
|
{slots.default?.()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
TransBtn.inheritAttrs = false;
|
||||||
|
TransBtn.displayName = 'TransBtn';
|
||||||
|
TransBtn.props = {
|
||||||
|
class: PropTypes.string,
|
||||||
|
customizeIcon: PropTypes.any,
|
||||||
|
customizeIconProps: PropTypes.any,
|
||||||
|
onMousedown: PropTypes.func,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TransBtn;
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,24 @@
|
||||||
|
/**
|
||||||
|
* BaseSelect provide some parsed data into context.
|
||||||
|
* You can use this hooks to get them.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { InjectionKey } from 'vue';
|
||||||
|
import { inject, provide } from 'vue';
|
||||||
|
import type { BaseSelectProps } from '../BaseSelect';
|
||||||
|
|
||||||
|
export interface BaseSelectContextProps extends BaseSelectProps {
|
||||||
|
triggerOpen: boolean;
|
||||||
|
multiple: boolean;
|
||||||
|
toggleOpen: (open?: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BaseSelectContextKey: InjectionKey<BaseSelectContextProps> = Symbol('BaseSelectContextKey');
|
||||||
|
|
||||||
|
export function useProvideBaseSelectProps(props: BaseSelectContextProps) {
|
||||||
|
return provide(BaseSelectContextKey, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useBaseProps() {
|
||||||
|
return inject(BaseSelectContextKey, {} as BaseSelectContextProps);
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import { shallowRef, computed } from 'vue';
|
||||||
|
import type { RawValueType } from '../BaseSelect';
|
||||||
|
import type { DefaultOptionType, LabelInValueType } from '../Select';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache `value` related LabeledValue & options.
|
||||||
|
*/
|
||||||
|
export default (
|
||||||
|
labeledValues: Ref<LabelInValueType[]>,
|
||||||
|
valueOptions: Ref<Map<RawValueType, DefaultOptionType>>,
|
||||||
|
): [Ref<LabelInValueType[]>, (val: RawValueType) => DefaultOptionType] => {
|
||||||
|
const cacheRef = shallowRef({
|
||||||
|
values: new Map<RawValueType, LabelInValueType>(),
|
||||||
|
options: new Map<RawValueType, DefaultOptionType>(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const filledLabeledValues = computed(() => {
|
||||||
|
const { values: prevValueCache, options: prevOptionCache } = cacheRef.value;
|
||||||
|
|
||||||
|
// Fill label by cache
|
||||||
|
const patchedValues = labeledValues.value.map(item => {
|
||||||
|
if (item.label === undefined) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
label: prevValueCache.get(item.value)?.label,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh cache
|
||||||
|
const valueCache = new Map<RawValueType, LabelInValueType>();
|
||||||
|
const optionCache = new Map<RawValueType, DefaultOptionType>();
|
||||||
|
|
||||||
|
patchedValues.forEach(item => {
|
||||||
|
valueCache.set(item.value, item);
|
||||||
|
optionCache.set(
|
||||||
|
item.value,
|
||||||
|
valueOptions.value.get(item.value) || prevOptionCache.get(item.value),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
cacheRef.value.values = valueCache;
|
||||||
|
cacheRef.value.options = optionCache;
|
||||||
|
|
||||||
|
return patchedValues;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getOption = (val: RawValueType) =>
|
||||||
|
valueOptions.value.get(val) || cacheRef.value.options.get(val);
|
||||||
|
|
||||||
|
return [filledLabeledValues, getOption];
|
||||||
|
};
|
|
@ -0,0 +1,32 @@
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar with `useLock`, but this hook will always execute last value.
|
||||||
|
* When set to `true`, it will keep `true` for a short time even if `false` is set.
|
||||||
|
*/
|
||||||
|
export default function useDelayReset(
|
||||||
|
timeout = 10,
|
||||||
|
): [Ref<Boolean>, (val: boolean, callback?: () => void) => void, () => void] {
|
||||||
|
const bool = ref(false);
|
||||||
|
let delay: any;
|
||||||
|
|
||||||
|
const cancelLatest = () => {
|
||||||
|
clearTimeout(delay);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
cancelLatest();
|
||||||
|
});
|
||||||
|
const delaySetBool = (value: boolean, callback: () => void) => {
|
||||||
|
cancelLatest();
|
||||||
|
delay = setTimeout(() => {
|
||||||
|
bool.value = value;
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}, timeout);
|
||||||
|
};
|
||||||
|
|
||||||
|
return [bool, delaySetBool, cancelLatest];
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { toArray } from '../utils/commonUtil';
|
||||||
|
import type {
|
||||||
|
FieldNames,
|
||||||
|
DefaultOptionType,
|
||||||
|
SelectProps,
|
||||||
|
FilterFunc,
|
||||||
|
BaseOptionType,
|
||||||
|
} from '../Select';
|
||||||
|
import { injectPropsWithOption } from '../utils/valueUtil';
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
function includes(test: any, search: string) {
|
||||||
|
return toArray(test).join('').toUpperCase().includes(search);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (
|
||||||
|
options: Ref<DefaultOptionType[]>,
|
||||||
|
fieldNames: Ref<FieldNames>,
|
||||||
|
searchValue?: Ref<string>,
|
||||||
|
filterOption?: Ref<SelectProps['filterOption']>,
|
||||||
|
optionFilterProp?: Ref<string>,
|
||||||
|
) =>
|
||||||
|
computed(() => {
|
||||||
|
if (!searchValue.value || filterOption.value === false) {
|
||||||
|
return options.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { options: fieldOptions, label: fieldLabel, value: fieldValue } = fieldNames.value;
|
||||||
|
const filteredOptions: DefaultOptionType[] = [];
|
||||||
|
|
||||||
|
const customizeFilter = typeof filterOption.value === 'function';
|
||||||
|
|
||||||
|
const upperSearch = searchValue.value.toUpperCase();
|
||||||
|
const filterFunc = customizeFilter
|
||||||
|
? (filterOption.value as FilterFunc<BaseOptionType>)
|
||||||
|
: (_: string, option: DefaultOptionType) => {
|
||||||
|
// Use provided `optionFilterProp`
|
||||||
|
if (optionFilterProp.value) {
|
||||||
|
return includes(option[optionFilterProp.value], upperSearch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto select `label` or `value` by option type
|
||||||
|
if (option[fieldOptions]) {
|
||||||
|
// hack `fieldLabel` since `OptionGroup` children is not `label`
|
||||||
|
return includes(option[fieldLabel !== 'children' ? fieldLabel : 'label'], upperSearch);
|
||||||
|
}
|
||||||
|
|
||||||
|
return includes(option[fieldValue], upperSearch);
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapOption: (opt: DefaultOptionType) => DefaultOptionType = customizeFilter
|
||||||
|
? opt => injectPropsWithOption(opt)
|
||||||
|
: opt => opt;
|
||||||
|
|
||||||
|
options.value.forEach(item => {
|
||||||
|
// Group should check child options
|
||||||
|
if (item[fieldOptions]) {
|
||||||
|
// Check group first
|
||||||
|
const matchGroup = filterFunc(searchValue.value, wrapOption(item));
|
||||||
|
if (matchGroup) {
|
||||||
|
filteredOptions.push(item);
|
||||||
|
} else {
|
||||||
|
// Check option
|
||||||
|
const subOptions = item[fieldOptions].filter((subItem: DefaultOptionType) =>
|
||||||
|
filterFunc(searchValue.value, wrapOption(subItem)),
|
||||||
|
);
|
||||||
|
if (subOptions.length) {
|
||||||
|
filteredOptions.push({
|
||||||
|
...item,
|
||||||
|
[fieldOptions]: subOptions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterFunc(searchValue.value, wrapOption(item))) {
|
||||||
|
filteredOptions.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return filteredOptions;
|
||||||
|
});
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import canUseDom from '../../_util/canUseDom';
|
||||||
|
|
||||||
|
let uuid = 0;
|
||||||
|
|
||||||
|
/** Is client side and not jsdom */
|
||||||
|
export const isBrowserClient = process.env.NODE_ENV !== 'test' && canUseDom();
|
||||||
|
|
||||||
|
/** Get unique id for accessibility usage */
|
||||||
|
export function getUUID(): number | string {
|
||||||
|
let retId: string | number;
|
||||||
|
|
||||||
|
// Test never reach
|
||||||
|
/* istanbul ignore if */
|
||||||
|
if (isBrowserClient) {
|
||||||
|
retId = uuid;
|
||||||
|
uuid += 1;
|
||||||
|
} else {
|
||||||
|
retId = 'TEST_OR_SSR';
|
||||||
|
}
|
||||||
|
|
||||||
|
return retId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useId(id = ref('')) {
|
||||||
|
// Inner id for accessibility usage. Only work in client side
|
||||||
|
const innerId = `rc_select_${getUUID()}`;
|
||||||
|
|
||||||
|
return id.value || innerId;
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { onBeforeUpdate } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locker return cached mark.
|
||||||
|
* If set to `true`, will return `true` in a short time even if set `false`.
|
||||||
|
* If set to `false` and then set to `true`, will change to `true`.
|
||||||
|
* And after time duration, it will back to `null` automatically.
|
||||||
|
*/
|
||||||
|
export default function useLock(duration = 250): [() => boolean | null, (lock: boolean) => void] {
|
||||||
|
let lock: boolean | null = null;
|
||||||
|
let timeout: any;
|
||||||
|
|
||||||
|
onBeforeUpdate(() => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
});
|
||||||
|
|
||||||
|
function doLock(locked: boolean) {
|
||||||
|
if (locked || lock === null) {
|
||||||
|
lock = locked;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
lock = null;
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [() => lock, doLock];
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import type { FieldNames, RawValueType } from '../Select';
|
||||||
|
import { convertChildrenToData } from '../utils/legacyUtil';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse `children` to `options` if `options` is not provided.
|
||||||
|
* Then flatten the `options`.
|
||||||
|
*/
|
||||||
|
export default function useOptions<OptionType>(
|
||||||
|
options: Ref<OptionType[]>,
|
||||||
|
children: Ref<any>,
|
||||||
|
fieldNames: Ref<FieldNames>,
|
||||||
|
) {
|
||||||
|
return computed(() => {
|
||||||
|
let mergedOptions = options.value;
|
||||||
|
const childrenAsData = !options.value;
|
||||||
|
|
||||||
|
if (childrenAsData) {
|
||||||
|
mergedOptions = convertChildrenToData(children.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueOptions = new Map<RawValueType, OptionType>();
|
||||||
|
const labelOptions = new Map<any, OptionType>();
|
||||||
|
|
||||||
|
function dig(optionList: OptionType[], isChildren = false) {
|
||||||
|
// for loop to speed up collection speed
|
||||||
|
for (let i = 0; i < optionList.length; i += 1) {
|
||||||
|
const option = optionList[i];
|
||||||
|
if (!option[fieldNames.value.options] || isChildren) {
|
||||||
|
valueOptions.set(option[fieldNames.value.value], option);
|
||||||
|
labelOptions.set(option[fieldNames.value.label], option);
|
||||||
|
} else {
|
||||||
|
dig(option[fieldNames.value.options], true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dig(mergedOptions);
|
||||||
|
|
||||||
|
return {
|
||||||
|
options: mergedOptions,
|
||||||
|
valueOptions,
|
||||||
|
labelOptions,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import { onBeforeUnmount, onMounted } from 'vue';
|
||||||
|
|
||||||
|
export default function useSelectTriggerControl(
|
||||||
|
refs: Ref[],
|
||||||
|
open: Ref<boolean>,
|
||||||
|
triggerOpen: (open: boolean) => void,
|
||||||
|
) {
|
||||||
|
function onGlobalMouseDown(event: MouseEvent) {
|
||||||
|
let target = event.target as HTMLElement;
|
||||||
|
|
||||||
|
if (target.shadowRoot && event.composed) {
|
||||||
|
target = (event.composedPath()[0] || target) as HTMLElement;
|
||||||
|
}
|
||||||
|
const elements = [refs[0]?.value, refs[1]?.value?.getPopupElement()];
|
||||||
|
if (
|
||||||
|
open.value &&
|
||||||
|
elements.every(element => element && !element.contains(target) && element !== target)
|
||||||
|
) {
|
||||||
|
// Should trigger close
|
||||||
|
triggerOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('mousedown', onGlobalMouseDown);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('mousedown', onGlobalMouseDown);
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import type { SelectProps } from './Select';
|
||||||
|
import Select, { selectProps } from './Select';
|
||||||
|
import Option from './Option';
|
||||||
|
import OptGroup from './OptGroup';
|
||||||
|
import BaseSelect from './BaseSelect';
|
||||||
|
import type { BaseSelectProps, BaseSelectRef, BaseSelectPropsWithoutPrivate } from './BaseSelect';
|
||||||
|
import useBaseProps from './hooks/useBaseProps';
|
||||||
|
|
||||||
|
export { Option, OptGroup, selectProps, BaseSelect, useBaseProps };
|
||||||
|
export type { BaseSelectProps, BaseSelectRef, BaseSelectPropsWithoutPrivate, SelectProps };
|
||||||
|
|
||||||
|
export default Select;
|
|
@ -0,0 +1,11 @@
|
||||||
|
import type { Key } from '../_util/type';
|
||||||
|
import type { RawValueType } from './BaseSelect';
|
||||||
|
|
||||||
|
export interface FlattenOptionData<OptionType> {
|
||||||
|
label?: any;
|
||||||
|
data: OptionType;
|
||||||
|
key: Key;
|
||||||
|
value?: RawValueType;
|
||||||
|
groupOption?: boolean;
|
||||||
|
group?: boolean;
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
import type { VueNode } from '../../_util/type';
|
||||||
|
|
||||||
|
export type SelectSource = 'option' | 'selection' | 'input';
|
||||||
|
|
||||||
|
export const INTERNAL_PROPS_MARK = 'RC_SELECT_INTERNAL_PROPS_MARK';
|
||||||
|
|
||||||
|
// =================================== Shared Type ===================================
|
||||||
|
export type Key = string | number;
|
||||||
|
|
||||||
|
export type RawValueType = string | number | null;
|
||||||
|
|
||||||
|
export interface LabelValueType extends Record<string, any> {
|
||||||
|
key?: Key;
|
||||||
|
value?: RawValueType;
|
||||||
|
label?: any;
|
||||||
|
isCacheable?: boolean;
|
||||||
|
}
|
||||||
|
export type DefaultValueType = RawValueType | RawValueType[] | LabelValueType | LabelValueType[];
|
||||||
|
|
||||||
|
export interface DisplayLabelValueType extends LabelValueType {
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SingleType<MixType> = MixType extends (infer Single)[] ? Single : MixType;
|
||||||
|
|
||||||
|
export type OnClear = () => any;
|
||||||
|
|
||||||
|
export type CustomTagProps = {
|
||||||
|
label: any;
|
||||||
|
value: DefaultValueType;
|
||||||
|
disabled: boolean;
|
||||||
|
onClose: (event?: MouseEvent) => void;
|
||||||
|
closable: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================================== Generator ====================================
|
||||||
|
export type GetLabeledValue<FOT extends FlattenOptionsType> = (
|
||||||
|
value: RawValueType,
|
||||||
|
config: {
|
||||||
|
options: FOT;
|
||||||
|
prevValueMap: Map<RawValueType, LabelValueType>;
|
||||||
|
labelInValue: boolean;
|
||||||
|
optionLabelProp: string;
|
||||||
|
},
|
||||||
|
) => LabelValueType;
|
||||||
|
|
||||||
|
export type FilterOptions<OptionsType extends object[]> = (
|
||||||
|
searchValue: string,
|
||||||
|
options: OptionsType,
|
||||||
|
/** Component props, since Select & TreeSelect use different prop name, use any here */
|
||||||
|
config: {
|
||||||
|
optionFilterProp: string;
|
||||||
|
filterOption: boolean | FilterFunc<OptionsType[number]>;
|
||||||
|
},
|
||||||
|
) => OptionsType;
|
||||||
|
|
||||||
|
export type FilterFunc<OptionType> = (inputValue: string, option?: OptionType) => boolean;
|
||||||
|
|
||||||
|
export type FlattenOptionsType<OptionType = object> = {
|
||||||
|
key: Key;
|
||||||
|
data: OptionType;
|
||||||
|
label?: any;
|
||||||
|
value?: RawValueType;
|
||||||
|
/** Used for customize data */
|
||||||
|
[name: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
}[];
|
||||||
|
|
||||||
|
export type DropdownObject = {
|
||||||
|
menuNode?: VueNode;
|
||||||
|
props?: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DropdownRender = (opt?: DropdownObject) => VueNode;
|
|
@ -0,0 +1,62 @@
|
||||||
|
import type { VueNode } from '../../_util/type';
|
||||||
|
import type { VNode, CSSProperties } from 'vue';
|
||||||
|
import type { Key, RawValueType } from './generator';
|
||||||
|
|
||||||
|
export type RenderDOMFunc = (props: any) => HTMLElement;
|
||||||
|
|
||||||
|
export type RenderNode = VueNode | ((props: any) => VueNode);
|
||||||
|
|
||||||
|
export type Mode = 'multiple' | 'tags' | 'combobox';
|
||||||
|
|
||||||
|
// ======================== Option ========================
|
||||||
|
|
||||||
|
export interface FieldNames {
|
||||||
|
value?: string;
|
||||||
|
label?: string;
|
||||||
|
options?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OnActiveValue = (
|
||||||
|
active: RawValueType,
|
||||||
|
index: number,
|
||||||
|
info?: { source?: 'keyboard' | 'mouse' },
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export interface OptionCoreData {
|
||||||
|
key?: Key;
|
||||||
|
disabled?: boolean;
|
||||||
|
value?: Key;
|
||||||
|
title?: string;
|
||||||
|
class?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
label?: VueNode;
|
||||||
|
/** @deprecated Only works when use `children` as option data */
|
||||||
|
children?: VNode[] | JSX.Element[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OptionData extends OptionCoreData {
|
||||||
|
/** Save for customize data */
|
||||||
|
[prop: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OptionGroupData {
|
||||||
|
key?: Key;
|
||||||
|
label?: VueNode;
|
||||||
|
options: OptionData[];
|
||||||
|
class?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
|
||||||
|
/** Save for customize data */
|
||||||
|
[prop: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OptionsType = (OptionData | OptionGroupData)[];
|
||||||
|
|
||||||
|
export interface FlattenOptionData {
|
||||||
|
group?: boolean;
|
||||||
|
groupOption?: boolean;
|
||||||
|
key: string | number;
|
||||||
|
data: OptionData | OptionGroupData;
|
||||||
|
label?: any;
|
||||||
|
value?: RawValueType;
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
export function toArray<T>(value: T | T[]): T[] {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return value !== undefined ? [value] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isClient =
|
||||||
|
typeof window !== 'undefined' && window.document && window.document.documentElement;
|
||||||
|
|
||||||
|
/** Is client side and not jsdom */
|
||||||
|
export const isBrowserClient = process.env.NODE_ENV !== 'test' && isClient;
|
|
@ -0,0 +1,34 @@
|
||||||
|
import KeyCode from '../../_util/KeyCode';
|
||||||
|
|
||||||
|
/** keyCode Judgment function */
|
||||||
|
export function isValidateOpenKey(currentKeyCode: number): boolean {
|
||||||
|
return ![
|
||||||
|
// System function button
|
||||||
|
KeyCode.ESC,
|
||||||
|
KeyCode.SHIFT,
|
||||||
|
KeyCode.BACKSPACE,
|
||||||
|
KeyCode.TAB,
|
||||||
|
KeyCode.WIN_KEY,
|
||||||
|
KeyCode.ALT,
|
||||||
|
KeyCode.META,
|
||||||
|
KeyCode.WIN_KEY_RIGHT,
|
||||||
|
KeyCode.CTRL,
|
||||||
|
KeyCode.SEMICOLON,
|
||||||
|
KeyCode.EQUALS,
|
||||||
|
KeyCode.CAPS_LOCK,
|
||||||
|
KeyCode.CONTEXT_MENU,
|
||||||
|
// F1-F12
|
||||||
|
KeyCode.F1,
|
||||||
|
KeyCode.F2,
|
||||||
|
KeyCode.F3,
|
||||||
|
KeyCode.F4,
|
||||||
|
KeyCode.F5,
|
||||||
|
KeyCode.F6,
|
||||||
|
KeyCode.F7,
|
||||||
|
KeyCode.F8,
|
||||||
|
KeyCode.F9,
|
||||||
|
KeyCode.F10,
|
||||||
|
KeyCode.F11,
|
||||||
|
KeyCode.F12,
|
||||||
|
].includes(currentKeyCode);
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { flattenChildren, isValidElement } from '../../_util/props-util';
|
||||||
|
import type { VNode } from 'vue';
|
||||||
|
import type { BaseOptionType, DefaultOptionType } from '../Select';
|
||||||
|
import type { VueNode } from '../../_util/type';
|
||||||
|
|
||||||
|
function convertNodeToOption<OptionType extends BaseOptionType = DefaultOptionType>(
|
||||||
|
node: VNode,
|
||||||
|
): OptionType {
|
||||||
|
const {
|
||||||
|
key,
|
||||||
|
children,
|
||||||
|
props: { value, disabled, ...restProps },
|
||||||
|
} = node as Omit<VNode, 'key'> & {
|
||||||
|
children: { default?: () => any };
|
||||||
|
key: string | number;
|
||||||
|
};
|
||||||
|
const child = children && children.default ? children.default() : undefined;
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
value: value !== undefined ? value : key,
|
||||||
|
children: child,
|
||||||
|
disabled: disabled || disabled === '', // support <a-select-option disabled />
|
||||||
|
...(restProps as any),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertChildrenToData<OptionType extends BaseOptionType = DefaultOptionType>(
|
||||||
|
nodes: VueNode,
|
||||||
|
optionOnly = false,
|
||||||
|
): OptionType[] {
|
||||||
|
const dd = flattenChildren(nodes as [])
|
||||||
|
.map((node: VNode, index: number): OptionType | null => {
|
||||||
|
if (!isValidElement(node) || !node.type) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
type: { isSelectOptGroup },
|
||||||
|
key,
|
||||||
|
children,
|
||||||
|
props,
|
||||||
|
} = node as VNode & {
|
||||||
|
type: { isSelectOptGroup?: boolean };
|
||||||
|
children: { default?: () => any; label?: () => any };
|
||||||
|
};
|
||||||
|
|
||||||
|
if (optionOnly || !isSelectOptGroup) {
|
||||||
|
return convertNodeToOption(node);
|
||||||
|
}
|
||||||
|
const child = children && children.default ? children.default() : undefined;
|
||||||
|
const label = props?.label || children.label?.() || key;
|
||||||
|
return {
|
||||||
|
key: `__RC_SELECT_GRP__${key === null ? index : String(key)}__`,
|
||||||
|
...props,
|
||||||
|
label,
|
||||||
|
options: convertChildrenToData(child || []),
|
||||||
|
} as any;
|
||||||
|
})
|
||||||
|
.filter(data => data);
|
||||||
|
return dd;
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
/* istanbul ignore file */
|
||||||
|
export function isPlatformMac(): boolean {
|
||||||
|
return /(mac\sos|macintosh)/i.test(navigator.appVersion);
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
import type { BaseOptionType, DefaultOptionType, RawValueType, FieldNames } from '../Select';
|
||||||
|
import { warning } from '../../vc-util/warning';
|
||||||
|
import type { FlattenOptionData } from '../interface';
|
||||||
|
|
||||||
|
function getKey(data: BaseOptionType, index: number) {
|
||||||
|
const { key } = data;
|
||||||
|
let value: RawValueType;
|
||||||
|
|
||||||
|
if ('value' in data) {
|
||||||
|
({ value } = data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key !== null && key !== undefined) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
if (value !== undefined) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return `rc-index-key-${index}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fillFieldNames(fieldNames: FieldNames | undefined, childrenAsData: boolean) {
|
||||||
|
const { label, value, options } = fieldNames || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: label || (childrenAsData ? 'children' : 'label'),
|
||||||
|
value: value || 'value',
|
||||||
|
options: options || 'options',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flat options into flatten list.
|
||||||
|
* We use `optionOnly` here is aim to avoid user use nested option group.
|
||||||
|
* Here is simply set `key` to the index if not provided.
|
||||||
|
*/
|
||||||
|
export function flattenOptions<OptionType extends BaseOptionType = DefaultOptionType>(
|
||||||
|
options: OptionType[],
|
||||||
|
{ fieldNames, childrenAsData }: { fieldNames?: FieldNames; childrenAsData?: boolean } = {},
|
||||||
|
): FlattenOptionData<OptionType>[] {
|
||||||
|
const flattenList: FlattenOptionData<OptionType>[] = [];
|
||||||
|
|
||||||
|
const {
|
||||||
|
label: fieldLabel,
|
||||||
|
value: fieldValue,
|
||||||
|
options: fieldOptions,
|
||||||
|
} = fillFieldNames(fieldNames, false);
|
||||||
|
|
||||||
|
function dig(list: OptionType[], isGroupOption: boolean) {
|
||||||
|
list.forEach(data => {
|
||||||
|
const label = data[fieldLabel];
|
||||||
|
|
||||||
|
if (isGroupOption || !(fieldOptions in data)) {
|
||||||
|
const value = data[fieldValue];
|
||||||
|
// Option
|
||||||
|
flattenList.push({
|
||||||
|
key: getKey(data, flattenList.length),
|
||||||
|
groupOption: isGroupOption,
|
||||||
|
data,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let grpLabel = label;
|
||||||
|
if (grpLabel === undefined && childrenAsData) {
|
||||||
|
grpLabel = data.label;
|
||||||
|
}
|
||||||
|
// Option Group
|
||||||
|
flattenList.push({
|
||||||
|
key: getKey(data, flattenList.length),
|
||||||
|
group: true,
|
||||||
|
data,
|
||||||
|
label: grpLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
dig(data[fieldOptions], true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dig(options, false);
|
||||||
|
|
||||||
|
return flattenList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject `props` into `option` for legacy usage
|
||||||
|
*/
|
||||||
|
export function injectPropsWithOption<T>(option: T): T {
|
||||||
|
const newOption = { ...option };
|
||||||
|
if (!('props' in newOption)) {
|
||||||
|
Object.defineProperty(newOption, 'props', {
|
||||||
|
get() {
|
||||||
|
warning(
|
||||||
|
false,
|
||||||
|
'Return type is option instead of Option instance. Please read value directly instead of reading from `props`.',
|
||||||
|
);
|
||||||
|
return newOption;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return newOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSeparatedContent(text: string, tokens: string[]): string[] {
|
||||||
|
if (!tokens || !tokens.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let match = false;
|
||||||
|
|
||||||
|
function separate(str: string, [token, ...restTokens]: string[]) {
|
||||||
|
if (!token) {
|
||||||
|
return [str];
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = str.split(token);
|
||||||
|
match = match || list.length > 1;
|
||||||
|
|
||||||
|
return list
|
||||||
|
.reduce((prevList, unitStr) => [...prevList, ...separate(unitStr, restTokens)], [])
|
||||||
|
.filter(unit => unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = separate(text, tokens);
|
||||||
|
return match ? list : null;
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
import warning, { noteOnce } from '../../vc-util/warning';
|
||||||
|
import { convertChildrenToData } from './legacyUtil';
|
||||||
|
import { toArray } from './commonUtil';
|
||||||
|
import { isValidElement } from '../../_util/props-util';
|
||||||
|
import type { VNode } from 'vue';
|
||||||
|
import type { RawValueType, LabelInValueType, SelectProps } from '../Select';
|
||||||
|
import { isMultiple } from '../BaseSelect';
|
||||||
|
|
||||||
|
function warningProps(props: SelectProps) {
|
||||||
|
const {
|
||||||
|
mode,
|
||||||
|
options,
|
||||||
|
children,
|
||||||
|
backfill,
|
||||||
|
allowClear,
|
||||||
|
placeholder,
|
||||||
|
getInputElement,
|
||||||
|
showSearch,
|
||||||
|
onSearch,
|
||||||
|
defaultOpen,
|
||||||
|
autofocus,
|
||||||
|
labelInValue,
|
||||||
|
value,
|
||||||
|
inputValue,
|
||||||
|
optionLabelProp,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const multiple = isMultiple(mode);
|
||||||
|
const mergedShowSearch = showSearch !== undefined ? showSearch : multiple || mode === 'combobox';
|
||||||
|
const mergedOptions = options || convertChildrenToData(children);
|
||||||
|
|
||||||
|
// `tags` should not set option as disabled
|
||||||
|
warning(
|
||||||
|
mode !== 'tags' || mergedOptions.every((opt: { disabled?: boolean }) => !opt.disabled),
|
||||||
|
'Please avoid setting option to disabled in tags mode since user can always type text as tag.',
|
||||||
|
);
|
||||||
|
|
||||||
|
// `combobox` should not use `optionLabelProp`
|
||||||
|
warning(
|
||||||
|
mode !== 'combobox' || !optionLabelProp,
|
||||||
|
'`combobox` mode not support `optionLabelProp`. Please set `value` on Option directly.',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only `combobox` support `backfill`
|
||||||
|
warning(mode === 'combobox' || !backfill, '`backfill` only works with `combobox` mode.');
|
||||||
|
|
||||||
|
// Only `combobox` support `getInputElement`
|
||||||
|
warning(
|
||||||
|
mode === 'combobox' || !getInputElement,
|
||||||
|
'`getInputElement` only work with `combobox` mode.',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Customize `getInputElement` should not use `allowClear` & `placeholder`
|
||||||
|
noteOnce(
|
||||||
|
mode !== 'combobox' || !getInputElement || !allowClear || !placeholder,
|
||||||
|
'Customize `getInputElement` should customize clear and placeholder logic instead of configuring `allowClear` and `placeholder`.',
|
||||||
|
);
|
||||||
|
|
||||||
|
// `onSearch` should use in `combobox` or `showSearch`
|
||||||
|
if (onSearch && !mergedShowSearch && mode !== 'combobox' && mode !== 'tags') {
|
||||||
|
warning(false, '`onSearch` should work with `showSearch` instead of use alone.');
|
||||||
|
}
|
||||||
|
|
||||||
|
noteOnce(
|
||||||
|
!defaultOpen || autofocus,
|
||||||
|
'`defaultOpen` makes Select open without focus which means it will not close by click outside. You can set `autofocus` if needed.',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
const values = toArray<RawValueType | LabelInValueType>(value);
|
||||||
|
warning(
|
||||||
|
!labelInValue ||
|
||||||
|
values.every(val => typeof val === 'object' && ('key' in val || 'value' in val)),
|
||||||
|
'`value` should in shape of `{ value: string | number, label?: any }` when you set `labelInValue` to `true`',
|
||||||
|
);
|
||||||
|
|
||||||
|
warning(
|
||||||
|
!multiple || Array.isArray(value),
|
||||||
|
'`value` should be array when `mode` is `multiple` or `tags`',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Syntactic sugar should use correct children type
|
||||||
|
if (children) {
|
||||||
|
let invalidateChildType = null;
|
||||||
|
children.some((node: any) => {
|
||||||
|
if (!isValidElement(node) || !node.type) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type } = node as { type: { isSelectOption?: boolean; isSelectOptGroup?: boolean } };
|
||||||
|
|
||||||
|
if (type.isSelectOption) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (type.isSelectOptGroup) {
|
||||||
|
const childs = node.children?.default() || [];
|
||||||
|
const allChildrenValid = childs.every((subNode: VNode) => {
|
||||||
|
if (
|
||||||
|
!isValidElement(subNode) ||
|
||||||
|
!node.type ||
|
||||||
|
(subNode.type as { isSelectOption?: boolean }).isSelectOption
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
invalidateChildType = subNode.type;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allChildrenValid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
invalidateChildType = type;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (invalidateChildType) {
|
||||||
|
warning(
|
||||||
|
false,
|
||||||
|
`\`children\` should be \`Select.Option\` or \`Select.OptGroup\` instead of \`${
|
||||||
|
invalidateChildType.displayName || invalidateChildType.name || invalidateChildType
|
||||||
|
}\`.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
warning(
|
||||||
|
inputValue === undefined,
|
||||||
|
'`inputValue` is deprecated, please use `searchValue` instead.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default warningProps;
|
|
@ -1,136 +0,0 @@
|
||||||
import createRef from '../../_util/createRef';
|
|
||||||
/* eslint-disable no-console */
|
|
||||||
import Select, { Option } from '..';
|
|
||||||
import '../assets/index.less';
|
|
||||||
import { nextTick } from 'vue';
|
|
||||||
|
|
||||||
const Combobox = {
|
|
||||||
data() {
|
|
||||||
this.textareaRef = createRef();
|
|
||||||
|
|
||||||
this.timeoutId;
|
|
||||||
return {
|
|
||||||
disabled: false,
|
|
||||||
value: '',
|
|
||||||
options: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
nextTick(() => {
|
|
||||||
console.log('Ref:', this.textareaRef.current);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onChange(value, option) {
|
|
||||||
console.log('onChange', value, option);
|
|
||||||
|
|
||||||
this.value = value;
|
|
||||||
},
|
|
||||||
|
|
||||||
onKeyDown(e) {
|
|
||||||
const { value } = this;
|
|
||||||
if (e.keyCode === 13) {
|
|
||||||
console.log('onEnter', value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onSelect(v, option) {
|
|
||||||
console.log('onSelect', v, option);
|
|
||||||
},
|
|
||||||
|
|
||||||
onSearch(text) {
|
|
||||||
console.log('onSearch:', text);
|
|
||||||
},
|
|
||||||
|
|
||||||
onAsyncChange(value) {
|
|
||||||
window.clearTimeout(this.timeoutId);
|
|
||||||
console.log(value);
|
|
||||||
this.options = [];
|
|
||||||
//const value = String(Math.random());
|
|
||||||
this.timeoutId = window.setTimeout(() => {
|
|
||||||
this.options = [{ value }, { value: `${value}-${value}` }];
|
|
||||||
}, 1000);
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleDisabled() {
|
|
||||||
const { disabled } = this;
|
|
||||||
|
|
||||||
this.disabled = !disabled;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { value, disabled } = this;
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2>combobox</h2>
|
|
||||||
<p>
|
|
||||||
<button type="button" onClick={this.toggleDisabled}>
|
|
||||||
toggle disabled
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
this.value = '';
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
reset
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<Select
|
|
||||||
disabled={disabled}
|
|
||||||
style={{ width: '500px' }}
|
|
||||||
onChange={this.onChange}
|
|
||||||
onSelect={this.onSelect}
|
|
||||||
onSearch={this.onSearch}
|
|
||||||
onInputKeyDown={this.onKeyDown}
|
|
||||||
notFoundContent=""
|
|
||||||
allowClear
|
|
||||||
placeholder="please select"
|
|
||||||
value={value}
|
|
||||||
mode="combobox"
|
|
||||||
backfill
|
|
||||||
onFocus={() => console.log('focus')}
|
|
||||||
onBlur={() => console.log('blur')}
|
|
||||||
>
|
|
||||||
<Option value="jack">
|
|
||||||
<b style={{ color: 'red' }}>jack</b>
|
|
||||||
</Option>
|
|
||||||
<Option value="lucy">lucy</Option>
|
|
||||||
<Option value="disabled" disabled>
|
|
||||||
disabled
|
|
||||||
</Option>
|
|
||||||
<Option value="yiminghe">yiminghe</Option>
|
|
||||||
<Option value="竹林星光">竹林星光</Option>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<h3>Customize Input Element</h3>
|
|
||||||
<Select
|
|
||||||
mode="combobox"
|
|
||||||
style={{ width: '200px' }}
|
|
||||||
getInputElement={() => (
|
|
||||||
<textarea style={{ background: 'red' }} rows={3} ref={this.textareaRef} />
|
|
||||||
)}
|
|
||||||
options={[{ value: 'light' }, { value: 'bamboo' }]}
|
|
||||||
allowClear
|
|
||||||
placeholder="2333"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<h3>Async Input Element</h3>
|
|
||||||
<Select
|
|
||||||
mode="combobox"
|
|
||||||
notFoundContent={null}
|
|
||||||
style={{ width: '200px' }}
|
|
||||||
options={this.options}
|
|
||||||
onChange={this.onAsyncChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Combobox;
|
|
||||||
/* eslint-enable */
|
|
|
@ -1,10 +0,0 @@
|
||||||
// input {
|
|
||||||
// // height: 24px;
|
|
||||||
// // line-height: 24px;
|
|
||||||
// border: 1px solid #333;
|
|
||||||
// border-radius: 4px;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// button {
|
|
||||||
// border: 1px solid #333;
|
|
||||||
// }
|
|
|
@ -1,35 +0,0 @@
|
||||||
import jsonp from 'jsonp';
|
|
||||||
import querystring from 'querystring';
|
|
||||||
|
|
||||||
let timeout;
|
|
||||||
let currentValue;
|
|
||||||
|
|
||||||
export function fetch(value, callback) {
|
|
||||||
if (timeout) {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = null;
|
|
||||||
}
|
|
||||||
currentValue = value;
|
|
||||||
|
|
||||||
function fake() {
|
|
||||||
const str = querystring.encode({
|
|
||||||
code: 'utf-8',
|
|
||||||
q: value,
|
|
||||||
});
|
|
||||||
jsonp(`http://suggest.taobao.com/sug?${str}`, (err, d) => {
|
|
||||||
if (currentValue === value) {
|
|
||||||
const { result } = d;
|
|
||||||
const data = [];
|
|
||||||
result.forEach(r => {
|
|
||||||
data.push({
|
|
||||||
value: r[0],
|
|
||||||
text: r[0],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
callback(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
timeout = setTimeout(fake, 300);
|
|
||||||
}
|
|
|
@ -1,99 +0,0 @@
|
||||||
/* eslint-disable no-console */
|
|
||||||
|
|
||||||
import Select, { Option } from '..';
|
|
||||||
import '../assets/index.less';
|
|
||||||
|
|
||||||
const Controlled = {
|
|
||||||
data: () => ({
|
|
||||||
destroy: false,
|
|
||||||
value: 9,
|
|
||||||
open: true,
|
|
||||||
}),
|
|
||||||
methods: {
|
|
||||||
onChange(e) {
|
|
||||||
let value;
|
|
||||||
if (e && e.target) {
|
|
||||||
({ value } = e.target);
|
|
||||||
} else {
|
|
||||||
value = e;
|
|
||||||
}
|
|
||||||
console.log('onChange', value);
|
|
||||||
this.value = value;
|
|
||||||
},
|
|
||||||
|
|
||||||
onDestroy() {
|
|
||||||
this.destroy = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
onBlur(v) {
|
|
||||||
console.log('onBlur', v);
|
|
||||||
},
|
|
||||||
|
|
||||||
onFocus() {
|
|
||||||
console.log('onFocus');
|
|
||||||
},
|
|
||||||
|
|
||||||
onDropdownVisibleChange(open) {
|
|
||||||
this.open = open;
|
|
||||||
},
|
|
||||||
getPopupContainer(node) {
|
|
||||||
return node.parentNode;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { open, destroy, value } = this;
|
|
||||||
if (destroy) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div style={{ margin: '20px' }}>
|
|
||||||
<h2>controlled Select</h2>
|
|
||||||
<div style={{ width: '300px' }}>
|
|
||||||
<Select
|
|
||||||
id="my-select"
|
|
||||||
value={value}
|
|
||||||
placeholder="placeholder"
|
|
||||||
listHeight={200}
|
|
||||||
style={{ width: '500px' }}
|
|
||||||
onBlur={this.onBlur}
|
|
||||||
onFocus={this.onFocus}
|
|
||||||
open={open}
|
|
||||||
optionLabelProp="children"
|
|
||||||
optionFilterProp="text"
|
|
||||||
onChange={this.onChange}
|
|
||||||
onDropdownVisibleChange={this.onDropdownVisibleChange}
|
|
||||||
//getPopupContainer={this.getPopupContainer}
|
|
||||||
>
|
|
||||||
<Option value="01" text="jack" title="jack">
|
|
||||||
<b
|
|
||||||
style={{
|
|
||||||
color: 'red',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
jack
|
|
||||||
</b>
|
|
||||||
</Option>
|
|
||||||
<Option value="11" text="lucy">
|
|
||||||
lucy
|
|
||||||
</Option>
|
|
||||||
<Option value="21" disabled text="disabled">
|
|
||||||
disabled
|
|
||||||
</Option>
|
|
||||||
<Option value="31" text="yiminghe">
|
|
||||||
yiminghe
|
|
||||||
</Option>
|
|
||||||
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(i => (
|
|
||||||
<Option key={i} value={i} text={String(i)}>
|
|
||||||
{i}-text
|
|
||||||
</Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Controlled;
|
|
||||||
/* eslint-enable */
|
|
|
@ -1,118 +0,0 @@
|
||||||
/* eslint-disable no-console */
|
|
||||||
|
|
||||||
import Select, { Option } from '..';
|
|
||||||
import '../assets/index.less';
|
|
||||||
|
|
||||||
const children = [];
|
|
||||||
for (let i = 10; i < 36; i += 1) {
|
|
||||||
children.push(
|
|
||||||
<Option
|
|
||||||
key={i.toString(36) + i}
|
|
||||||
disabled={i === 10}
|
|
||||||
title={`中文${i}`}
|
|
||||||
v-slots={{ default: () => `中文${i}` }}
|
|
||||||
></Option>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const Test = {
|
|
||||||
data: () => ({
|
|
||||||
state: {
|
|
||||||
useAnim: true,
|
|
||||||
showArrow: false,
|
|
||||||
loading: false,
|
|
||||||
value: ['a10'],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
methods: {
|
|
||||||
setState(state) {
|
|
||||||
Object.assign(this.state, state);
|
|
||||||
},
|
|
||||||
onChange(value, options) {
|
|
||||||
console.log('onChange', value, options);
|
|
||||||
this.setState({
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onSelect(...args) {
|
|
||||||
console.log(args);
|
|
||||||
},
|
|
||||||
|
|
||||||
onDeselect(...args) {
|
|
||||||
console.log(args);
|
|
||||||
},
|
|
||||||
|
|
||||||
useAnim(e) {
|
|
||||||
this.setState({
|
|
||||||
useAnim: e.target.checked,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
showArrow(e) {
|
|
||||||
this.setState({
|
|
||||||
showArrow: e.target.checked,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
loading(e) {
|
|
||||||
this.setState({
|
|
||||||
loading: e.target.checked,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { useAnim, showArrow, loading, value } = this.state;
|
|
||||||
return (
|
|
||||||
<div style="margin: 20px">
|
|
||||||
<h2>multiple select(scroll the menu)</h2>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<label html-for="useAnim">
|
|
||||||
anim
|
|
||||||
<input id="useAnim" checked={useAnim} type="checkbox" onChange={this.useAnim} />
|
|
||||||
</label>
|
|
||||||
<p />
|
|
||||||
<label html-for="showArrow">
|
|
||||||
showArrow
|
|
||||||
<input id="showArrow" checked={showArrow} type="checkbox" onChange={this.showArrow} />
|
|
||||||
</label>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<label html-for="loading">
|
|
||||||
loading
|
|
||||||
<input id="loading" checked={loading} type="checkbox" onChange={this.loading} />
|
|
||||||
</label>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div style={{ width: '300px' }}>
|
|
||||||
<Select
|
|
||||||
value={value}
|
|
||||||
animation={useAnim ? 'slide-up' : null}
|
|
||||||
choiceTransitionName="rc-select-selection__choice-zoom"
|
|
||||||
style={{ width: '500px' }}
|
|
||||||
mode="multiple"
|
|
||||||
loading={loading}
|
|
||||||
showArrow={showArrow}
|
|
||||||
allowClear
|
|
||||||
optionFilterProp="children"
|
|
||||||
optionLabelProp="children"
|
|
||||||
onSelect={this.onSelect}
|
|
||||||
onDeselect={this.onDeselect}
|
|
||||||
placeholder="please select"
|
|
||||||
onChange={this.onChange}
|
|
||||||
onFocus={() => console.log('focus')}
|
|
||||||
onBlur={v => console.log('blur', v)}
|
|
||||||
tokenSeparators={[' ', ',']}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Test;
|
|
||||||
/* eslint-enable */
|
|
|
@ -1,154 +0,0 @@
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
/* eslint-disable no-console */
|
|
||||||
|
|
||||||
import Select, { Option } from '..';
|
|
||||||
import '../assets/index.less';
|
|
||||||
import './single.less';
|
|
||||||
|
|
||||||
const Test = defineComponent({
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
destroy: false,
|
|
||||||
value: '9',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onChange(e) {
|
|
||||||
let value;
|
|
||||||
if (e && e.target) {
|
|
||||||
({ value } = e.target);
|
|
||||||
} else {
|
|
||||||
value = e;
|
|
||||||
}
|
|
||||||
console.log('onChange', value);
|
|
||||||
|
|
||||||
this.value = value;
|
|
||||||
},
|
|
||||||
|
|
||||||
onDestroy() {
|
|
||||||
this.destroy = 1;
|
|
||||||
},
|
|
||||||
|
|
||||||
onBlur(v) {
|
|
||||||
console.log('onBlur', v);
|
|
||||||
},
|
|
||||||
|
|
||||||
onFocus() {
|
|
||||||
console.log('onFocus');
|
|
||||||
},
|
|
||||||
|
|
||||||
onSearch(val) {
|
|
||||||
console.log('Search:', val);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { value, destroy } = this;
|
|
||||||
if (destroy) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ margin: '20px' }}>
|
|
||||||
<div
|
|
||||||
style={{ height: '150px', background: 'rgba(0, 255, 0, 0.1)' }}
|
|
||||||
onMousedown={e => {
|
|
||||||
e.preventDefault();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Prevent Default
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Single Select</h2>
|
|
||||||
|
|
||||||
<div style={{ width: '300px' }}>
|
|
||||||
<Select
|
|
||||||
autofocus
|
|
||||||
id="my-select"
|
|
||||||
value={value}
|
|
||||||
placeholder="placeholder"
|
|
||||||
showSearch
|
|
||||||
style={{ width: '500px' }}
|
|
||||||
onBlur={this.onBlur}
|
|
||||||
onFocus={this.onFocus}
|
|
||||||
onSearch={this.onSearch}
|
|
||||||
allowClear
|
|
||||||
optionFilterProp="text"
|
|
||||||
onChange={this.onChange}
|
|
||||||
onPopupScroll={() => {
|
|
||||||
console.log('Scroll!');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Option value={null}>不选择</Option>,
|
|
||||||
<Option value="01" text="jack" title="jack">
|
|
||||||
<b
|
|
||||||
style={{
|
|
||||||
color: 'red',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
jack
|
|
||||||
</b>
|
|
||||||
</Option>
|
|
||||||
,
|
|
||||||
<Option value="11" text="lucy">
|
|
||||||
<span>lucy</span>
|
|
||||||
</Option>
|
|
||||||
,
|
|
||||||
<Option value="21" disabled text="disabled">
|
|
||||||
disabled
|
|
||||||
</Option>
|
|
||||||
,
|
|
||||||
<Option value="31" text="yiminghe" class="test-option" style={{ background: 'yellow' }}>
|
|
||||||
yiminghe
|
|
||||||
</Option>
|
|
||||||
,
|
|
||||||
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(i => (
|
|
||||||
<Option key={i} value={String(i)} text={String(i)}>
|
|
||||||
{i}-text
|
|
||||||
</Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<h2>native select</h2>
|
|
||||||
<select value={value} style={{ width: '500px' }} onChange={this.onChange}>
|
|
||||||
<option value="01">jack</option>
|
|
||||||
<option value="11">lucy</option>
|
|
||||||
<option value="21" disabled>
|
|
||||||
disabled
|
|
||||||
</option>
|
|
||||||
<option value="31">yiminghe</option>
|
|
||||||
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map(i => (
|
|
||||||
<option value={i} key={i}>
|
|
||||||
{i}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<h2>RTL Select</h2>
|
|
||||||
|
|
||||||
<div style={{ width: '300px' }}>
|
|
||||||
<Select
|
|
||||||
id="my-select-rtl"
|
|
||||||
placeholder="rtl"
|
|
||||||
direction="rtl"
|
|
||||||
dropdownMatchSelectWidth={300}
|
|
||||||
dropdownStyle={{ minWidth: '300px' }}
|
|
||||||
style={{ width: '500px' }}
|
|
||||||
>
|
|
||||||
<Option value="1">1</Option>
|
|
||||||
<Option value="2">2</Option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<button type="button" onClick={this.onDestroy}>
|
|
||||||
destroy
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Test;
|
|
||||||
/* eslint-enable */
|
|
|
@ -1,3 +0,0 @@
|
||||||
.test-option {
|
|
||||||
font-weight: bolder;
|
|
||||||
}
|
|
Loading…
Reference in New Issue