feat: refactor vc-select
parent
72b1c0b539
commit
b786f6bba6
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module.exports = function(modules) {
|
||||
const plugins = [
|
||||
require.resolve('@vue/babel-plugin-jsx'),
|
||||
[require.resolve('@vue/babel-plugin-jsx'), { mergeProps: false }],
|
||||
require.resolve('@babel/plugin-proposal-optional-chaining'),
|
||||
require.resolve('@babel/plugin-transform-object-assign'),
|
||||
require.resolve('@babel/plugin-proposal-object-rest-spread'),
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ function getWebpackConfig(modules) {
|
|||
options: {
|
||||
presets: [require.resolve('@babel/preset-env')],
|
||||
plugins: [
|
||||
require.resolve('@vue/babel-plugin-jsx'),
|
||||
[require.resolve('@vue/babel-plugin-jsx'), { mergeProps: false }],
|
||||
require.resolve('@babel/plugin-proposal-object-rest-spread'),
|
||||
],
|
||||
},
|
||||
|
|
@ -231,7 +231,6 @@ All rights reserved.
|
|||
return config;
|
||||
}
|
||||
|
||||
getWebpackConfig.webpack = webpack;
|
||||
getWebpackConfig.svgRegex = svgRegex;
|
||||
getWebpackConfig.svgOptions = svgOptions;
|
||||
getWebpackConfig.imageOptions = imageOptions;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ module.exports = {
|
|||
test: {
|
||||
presets: [['@babel/preset-env', { targets: { node: true } }]],
|
||||
plugins: [
|
||||
'@vue/babel-plugin-jsx',
|
||||
['@vue/babel-plugin-jsx', { mergeProps: false }],
|
||||
'@babel/plugin-proposal-optional-chaining',
|
||||
'@babel/plugin-transform-object-assign',
|
||||
'@babel/plugin-proposal-object-rest-spread',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
interface RefObject extends Function {
|
||||
export interface RefObject extends Function {
|
||||
current?: any;
|
||||
}
|
||||
|
||||
|
|
@ -9,4 +9,23 @@ function createRef(): RefObject {
|
|||
return func;
|
||||
}
|
||||
|
||||
export function fillRef<T>(ref, node: T) {
|
||||
if (typeof ref === 'function') {
|
||||
ref(node);
|
||||
} else if (typeof ref === 'object' && ref && 'current' in ref) {
|
||||
(ref as any).current = node;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge refs into one ref function to support ref passing.
|
||||
*/
|
||||
export function composeRef<T>(...refs: any[]) {
|
||||
return (node: T) => {
|
||||
refs.forEach(ref => {
|
||||
fillRef(ref, node);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default createRef;
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
const attributes = `accept acceptCharset accessKey action allowFullScreen allowTransparency
|
||||
alt async autoComplete autoFocus autoPlay capture cellPadding cellSpacing challenge
|
||||
charSet checked classID className colSpan cols content contentEditable contextMenu
|
||||
controls coords crossOrigin data dateTime default defer dir disabled download draggable
|
||||
encType form formAction formEncType formMethod formNoValidate formTarget frameBorder
|
||||
headers height hidden high href hrefLang htmlFor httpEquiv icon id inputMode integrity
|
||||
is keyParams keyType kind label lang list loop low manifest marginHeight marginWidth max maxLength media
|
||||
mediaGroup method min minLength multiple muted name noValidate nonce open
|
||||
optimum pattern placeholder poster preload radioGroup readOnly rel required
|
||||
reversed role rowSpan rows sandbox scope scoped scrolling seamless selected
|
||||
shape size sizes span spellCheck src srcDoc srcLang srcSet start step style
|
||||
summary tabIndex target title type useMap value width wmode wrap`;
|
||||
const attributes = `accept acceptcharset accesskey action allowfullscreen allowtransparency
|
||||
alt async autocomplete autofocus autoplay capture cellpadding cellspacing challenge
|
||||
charset checked classid classname colspan cols content contenteditable contextmenu
|
||||
controls coords crossorigin data datetime default defer dir disabled download draggable
|
||||
enctype form formaction formenctype formmethod formnovalidate formtarget frameborder
|
||||
headers height hidden high href hreflang htmlfor httpequiv icon id inputmode integrity
|
||||
is keyparams keytype kind label lang list loop low manifest marginheight marginwidth max maxlength media
|
||||
mediagroup method min minlength multiple muted name novalidate nonce open
|
||||
optimum pattern placeholder poster preload radiogroup readonly rel required
|
||||
reversed role rowspan rows sandbox scope scoped scrolling seamless selected
|
||||
shape size sizes span spellcheck src srcdoc srclang srcset start step style
|
||||
summary tabindex target title type usemap value width wmode wrap`;
|
||||
|
||||
const eventsName = `onCopy onCut onPaste onCompositionEnd onCompositionStart onCompositionUpdate onKeyDown
|
||||
onKeyPress onKeyUp onFocus onBlur onChange onInput onSubmit onClick onContextMenu onDoubleClick
|
||||
onDrag onDragEnd onDragEnter onDragExit onDragLeave onDragOver onDragStart onDrop onMouseDown
|
||||
onMouseEnter onMouseLeave onMouseMove onMouseOut onMouseOver onMouseUp onSelect onTouchCancel
|
||||
onTouchEnd onTouchMove onTouchStart onScroll onWheel onAbort onCanPlay onCanPlayThrough
|
||||
onDurationChange onEmptied onEncrypted onEnded onError onLoadedData onLoadedMetadata
|
||||
onLoadStart onPause onPlay onPlaying onProgress onRateChange onSeeked onSeeking onStalled onSuspend onTimeUpdate onVolumeChange onWaiting onLoad onError`;
|
||||
const eventsName = `onCopy onCut onPaste onCompositionend onCompositionstart onCompositionupdate onKeydown
|
||||
onKeypress onKeyup onFocus onBlur onChange onInput onSubmit onClick onContextmenu onDoubleclick onDblclick
|
||||
onDrag onDragend onDragenter onDragexit onDragleave onDragover onDragstart onDrop onMousedown
|
||||
onMouseenter onMouseleave onMousemove onMouseout onMouseover onMouseup onSelect onTouchcancel
|
||||
onTouchend onTouchmove onTouchstart onScroll onWheel onAbort onCanplay onCanplaythrough
|
||||
onDurationchange onEmptied onEncrypted onEnded onError onLoadeddata onLoadedmetadata
|
||||
onLoadstart onPause onPlay onPlaying onProgress onRatechange onSeeked onSeeking onStalled onSuspend onTimeupdate onVolumechange onWaiting onLoad onError`;
|
||||
|
||||
const propList = `${attributes} ${eventsName}`.split(/[\s\n]+/);
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ export default function pickAttrs(props, ariaOnly = false) {
|
|||
// Data
|
||||
(mergedConfig.data && match(key, dataPrefix)) ||
|
||||
// Attr
|
||||
(mergedConfig.attr && propList.includes(key))
|
||||
(mergedConfig.attr && (propList.includes(key) || propList.includes(key.toLowerCase())))
|
||||
) {
|
||||
attrs[key] = props[key];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ export type LiteralUnion<T extends U, U> = T | (U & {});
|
|||
|
||||
export type Data = Record<string, unknown>;
|
||||
|
||||
export type Key = string | number;
|
||||
|
||||
type DefaultFactory<T> = (props: Data) => T | null | undefined;
|
||||
|
||||
export interface PropOptions<T = any, D = T> {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { filterEmpty } from './props-util';
|
|||
import { cloneVNode } from 'vue';
|
||||
import warning from './warning';
|
||||
|
||||
export function cloneElement(vnode, nodeProps = {}, override = true) {
|
||||
export function cloneElement(vnode, nodeProps = {}, override = true, mergeRef = false) {
|
||||
let ele = vnode;
|
||||
if (Array.isArray(vnode)) {
|
||||
ele = filterEmpty(vnode)[0];
|
||||
|
|
@ -10,7 +10,7 @@ export function cloneElement(vnode, nodeProps = {}, override = true) {
|
|||
if (!ele) {
|
||||
return null;
|
||||
}
|
||||
const node = cloneVNode(ele, nodeProps);
|
||||
const node = cloneVNode(ele, nodeProps, mergeRef);
|
||||
|
||||
// cloneVNode内部是合并属性,这里改成覆盖属性
|
||||
node.props = override ? { ...node.props, ...nodeProps } : node.props;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
import PropTypes from '../_util/vue-types';
|
||||
export default {
|
||||
props: {
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
},
|
||||
isSelectOptGroup: true,
|
||||
render() {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
import { FunctionalComponent } from 'vue';
|
||||
|
||||
import { OptionGroupData } from './interface';
|
||||
|
||||
export interface OptGroupProps extends Omit<OptionGroupData, 'options'> {}
|
||||
|
||||
export interface OptionGroupFC extends FunctionalComponent<OptGroupProps> {
|
||||
/** Legacy for check if is a Option Group */
|
||||
isSelectOptGroup: boolean;
|
||||
}
|
||||
|
||||
const OptGroup: OptionGroupFC = () => null;
|
||||
OptGroup.isSelectOptGroup = true;
|
||||
export default OptGroup;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
import PropTypes from '../_util/vue-types';
|
||||
import { FunctionalComponent } from 'vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
disabled: PropTypes.bool,
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
},
|
||||
isSelectOption: true,
|
||||
render() {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
import { OptionCoreData } from './interface';
|
||||
|
||||
export interface OptionProps extends Omit<OptionCoreData, '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;
|
||||
export default Option;
|
||||
|
|
|
|||
|
|
@ -5,8 +5,41 @@ import classNames from '../_util/classNames';
|
|||
import pickAttrs from '../_util/pickAttrs';
|
||||
import { isValidElement } from '../_util/props-util';
|
||||
import createRef from '../_util/createRef';
|
||||
import { computed, defineComponent, reactive, watch } from 'vue';
|
||||
import { computed, defineComponent, reactive, VNodeChild, watch } from 'vue';
|
||||
import List from '../vc-virtual-list/List';
|
||||
import {
|
||||
OptionsType as SelectOptionsType,
|
||||
OptionData,
|
||||
RenderNode,
|
||||
OnActiveValue,
|
||||
} from './interface';
|
||||
import { RawValueType, FlattenOptionsType } from './interface/generator';
|
||||
export interface OptionListProps {
|
||||
prefixCls: string;
|
||||
id: string;
|
||||
options: SelectOptionsType;
|
||||
flattenOptions: FlattenOptionsType<SelectOptionsType>;
|
||||
height: number;
|
||||
itemHeight: number;
|
||||
values: Set<RawValueType>;
|
||||
multiple: boolean;
|
||||
open: boolean;
|
||||
defaultActiveFirstOption?: boolean;
|
||||
notFoundContent?: VNodeChild;
|
||||
menuItemSelectedIcon?: RenderNode;
|
||||
childrenAsData: boolean;
|
||||
searchValue: string;
|
||||
virtual: boolean;
|
||||
|
||||
onSelect: (value: RawValueType, option: { selected: boolean }) => void;
|
||||
onToggleOpen: (open?: boolean) => void;
|
||||
/** Tell Select that some value is now active to make accessibility work */
|
||||
onActiveValue: OnActiveValue;
|
||||
onScroll: EventHandlerNonNull;
|
||||
|
||||
/** Tell Select that mouse enter the popup to force re-render */
|
||||
onMouseenter?: EventHandlerNonNull;
|
||||
}
|
||||
|
||||
const OptionListProps = {
|
||||
prefixCls: PropTypes.string,
|
||||
|
|
@ -39,8 +72,7 @@ const OptionListProps = {
|
|||
* Using virtual list of option display.
|
||||
* Will fallback to dom if use customize render.
|
||||
*/
|
||||
const OptionList = defineComponent({
|
||||
props: OptionListProps,
|
||||
const OptionList = defineComponent<OptionListProps>({
|
||||
name: 'OptionList',
|
||||
inheritAttrs: false,
|
||||
setup(props) {
|
||||
|
|
@ -49,25 +81,25 @@ const OptionList = defineComponent({
|
|||
// =========================== List ===========================
|
||||
const listRef = createRef();
|
||||
|
||||
const onListMouseDown = event => {
|
||||
const onListMouseDown: EventHandlerNonNull = event => {
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const scrollIntoView = index => {
|
||||
const scrollIntoView = (index: number) => {
|
||||
if (listRef.current) {
|
||||
listRef.current.scrollTo({ index });
|
||||
}
|
||||
};
|
||||
|
||||
// ========================== Active ==========================
|
||||
const getEnabledActiveIndex = (index, offset = 1) => {
|
||||
const getEnabledActiveIndex = (index: number, offset = 1) => {
|
||||
const len = props.flattenOptions.length;
|
||||
|
||||
for (let i = 0; i < len; i += 1) {
|
||||
const current = (index + i * offset + len) % len;
|
||||
|
||||
const { group, data } = props.flattenOptions[current];
|
||||
if (!group && !data.disabled) {
|
||||
if (!group && !(data as OptionData).disabled) {
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
|
@ -78,9 +110,9 @@ const OptionList = defineComponent({
|
|||
activeIndex: getEnabledActiveIndex(0),
|
||||
});
|
||||
|
||||
const setActive = (index, fromKeyboard = false) => {
|
||||
const setActive = (index: number, fromKeyboard = false) => {
|
||||
state.activeIndex = index;
|
||||
const info = { source: fromKeyboard ? 'keyboard' : 'mouse' };
|
||||
const info = { source: fromKeyboard ? ('keyboard' as const) : ('mouse' as const) };
|
||||
|
||||
// Trigger active event
|
||||
const flattenItem = props.flattenOptions[index];
|
||||
|
|
@ -94,30 +126,38 @@ const OptionList = defineComponent({
|
|||
|
||||
// Auto active first item when list length or searchValue changed
|
||||
|
||||
watch([props.flattenOptions.length, props.searchValue], () => {
|
||||
setActive(props.defaultActiveFirstOption !== false ? getEnabledActiveIndex(0) : -1);
|
||||
});
|
||||
watch(
|
||||
computed(() => [props.flattenOptions.length, props.searchValue]),
|
||||
() => {
|
||||
setActive(props.defaultActiveFirstOption !== false ? getEnabledActiveIndex(0) : -1);
|
||||
},
|
||||
);
|
||||
// Auto scroll to item position in single mode
|
||||
watch(props.open, () => {
|
||||
/**
|
||||
* React will skip `onChange` when component update.
|
||||
* `setActive` function will call root accessibility state update which makes re-render.
|
||||
* So we need to delay to let Input component trigger onChange first.
|
||||
*/
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!props.multiple && props.open && props.values.size === 1) {
|
||||
const value = Array.from(props.values)[0];
|
||||
const index = props.flattenOptions.findIndex(({ data }) => data.value === value);
|
||||
setActive(index);
|
||||
scrollIntoView(index);
|
||||
}
|
||||
});
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
});
|
||||
let timeoutId: number;
|
||||
watch(
|
||||
computed(() => props.open),
|
||||
() => {
|
||||
/**
|
||||
* React will skip `onChange` when component update.
|
||||
* `setActive` function will call root accessibility state update which makes re-render.
|
||||
* So we need to delay to let Input component trigger onChange first.
|
||||
*/
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
if (!props.multiple && props.open && props.values.size === 1) {
|
||||
const value = Array.from(props.values)[0];
|
||||
const index = props.flattenOptions.findIndex(({ data }) => data.value === value);
|
||||
setActive(index);
|
||||
scrollIntoView(index);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ immediate: true, flush: 'post' },
|
||||
);
|
||||
|
||||
// ========================== Values ==========================
|
||||
const onSelectValue = value => {
|
||||
const onSelectValue = (value?: RawValueType) => {
|
||||
if (value !== undefined) {
|
||||
props.onSelect(value, { selected: !props.values.has(value) });
|
||||
}
|
||||
|
|
@ -128,11 +168,11 @@ const OptionList = defineComponent({
|
|||
}
|
||||
};
|
||||
|
||||
function renderItem(index) {
|
||||
function renderItem(index: number) {
|
||||
const item = props.flattenOptions[index];
|
||||
if (!item) return null;
|
||||
|
||||
const itemData = item.data || {};
|
||||
const itemData = (item.data || {}) as OptionData;
|
||||
const { value, label, children } = itemData;
|
||||
const attrs = pickAttrs(itemData, true);
|
||||
const mergedLabel = props.childrenAsData ? children : label;
|
||||
|
|
@ -157,7 +197,7 @@ const OptionList = defineComponent({
|
|||
itemPrefixCls,
|
||||
setActive,
|
||||
onSelectValue,
|
||||
onKeydown: event => {
|
||||
onKeydown: (event: KeyboardEvent) => {
|
||||
const { which } = event;
|
||||
switch (which) {
|
||||
// >>> Arrow keys
|
||||
|
|
@ -204,7 +244,7 @@ const OptionList = defineComponent({
|
|||
},
|
||||
onKeyup: () => {},
|
||||
|
||||
scrollTo: index => {
|
||||
scrollTo: (index: number) => {
|
||||
scrollIntoView(index);
|
||||
},
|
||||
};
|
||||
|
|
@ -256,8 +296,7 @@ const OptionList = defineComponent({
|
|||
onScroll={onScroll}
|
||||
virtual={virtual}
|
||||
onMouseenter={onMouseenter}
|
||||
>
|
||||
{({ group, groupOption, data }, itemIndex) => {
|
||||
children={({ group, groupOption, data }, itemIndex) => {
|
||||
const { label, key } = data;
|
||||
|
||||
// Group
|
||||
|
|
@ -339,10 +378,12 @@ const OptionList = defineComponent({
|
|||
</div>
|
||||
);
|
||||
}}
|
||||
</List>
|
||||
></List>
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
OptionList.props = OptionListProps;
|
||||
|
||||
export default OptionList;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* 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 { OptionsType as SelectOptionsType } from './interface';
|
||||
import SelectOptionList from './OptionList';
|
||||
import Option from './Option';
|
||||
import OptGroup from './OptGroup';
|
||||
import { convertChildrenToData as convertSelectChildrenToData } from './utils/legacyUtil';
|
||||
import {
|
||||
getLabeledValue as getSelectLabeledValue,
|
||||
filterOptions as selectDefaultFilterOptions,
|
||||
isValueDisabled as isSelectValueDisabled,
|
||||
findValueOption as findSelectValueOption,
|
||||
flattenOptions,
|
||||
fillOptionsWithMissingValue,
|
||||
} from './utils/valueUtil';
|
||||
import generateSelector, { SelectProps } from './generate';
|
||||
import { DefaultValueType } from './interface/generator';
|
||||
import warningProps from './utils/warningPropsUtil';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import { getSlot } from '../_util/props-util';
|
||||
import omit from 'lodash-es/omit';
|
||||
|
||||
const RefSelect = generateSelector<SelectOptionsType>({
|
||||
prefixCls: 'rc-select',
|
||||
components: {
|
||||
optionList: SelectOptionList,
|
||||
},
|
||||
convertChildrenToData: convertSelectChildrenToData,
|
||||
flattenOptions,
|
||||
getLabeledValue: getSelectLabeledValue,
|
||||
filterOptions: selectDefaultFilterOptions,
|
||||
isValueDisabled: isSelectValueDisabled,
|
||||
findValueOption: findSelectValueOption,
|
||||
warningProps,
|
||||
fillOptionsWithMissingValue,
|
||||
});
|
||||
|
||||
export type ExportedSelectProps<
|
||||
ValueType extends DefaultValueType = DefaultValueType
|
||||
> = SelectProps<SelectOptionsType, ValueType>;
|
||||
|
||||
const Select = defineComponent<Omit<ExportedSelectProps, 'children'>>({
|
||||
setup() {
|
||||
const selectRef = ref(null);
|
||||
return {
|
||||
selectRef,
|
||||
focus: () => {
|
||||
selectRef.value?.focus();
|
||||
},
|
||||
blur: () => {
|
||||
selectRef.value?.blur();
|
||||
},
|
||||
};
|
||||
},
|
||||
render() {
|
||||
return <RefSelect ref="selectRef" {...this.$props} {...this.$attrs} children={getSlot(this)} />;
|
||||
},
|
||||
});
|
||||
Select.inheritAttrs = false;
|
||||
Select.props = omit(RefSelect.props, ['children']);
|
||||
Select.Option = Option;
|
||||
Select.OptGroup = OptGroup;
|
||||
export default Select;
|
||||
|
|
@ -49,7 +49,7 @@ export interface SelectTriggerProps {
|
|||
prefixCls: string;
|
||||
disabled: boolean;
|
||||
visible: boolean;
|
||||
popupElement: VNodeChild;
|
||||
popupElement: VNodeChild | JSX.Element;
|
||||
animation?: string;
|
||||
transitionName?: string;
|
||||
containerWidth: number;
|
||||
|
|
@ -61,7 +61,7 @@ export interface SelectTriggerProps {
|
|||
getPopupContainer?: RenderDOMFunc;
|
||||
dropdownAlign: object;
|
||||
empty: boolean;
|
||||
getTriggerDOMNode: () => HTMLElement;
|
||||
getTriggerDOMNode: () => any;
|
||||
}
|
||||
const SelectTrigger = defineComponent<SelectTriggerProps>({
|
||||
name: 'SelectTrigger',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,158 @@
|
|||
import { cloneElement } from '../../_util/vnode';
|
||||
import { defineComponent, inject, VNode, VNodeChild, withDirectives } from 'vue';
|
||||
import PropTypes from '../../_util/vue-types';
|
||||
import { RefObject } from '../../_util/createRef';
|
||||
import antInput from '../../_util/antInputDirective';
|
||||
|
||||
interface InputProps {
|
||||
prefixCls: string;
|
||||
id: string;
|
||||
inputElement: VNodeChild;
|
||||
disabled: boolean;
|
||||
autofocus: boolean;
|
||||
autocomplete: string;
|
||||
editable: boolean;
|
||||
accessibilityIndex: number;
|
||||
value: string;
|
||||
open: boolean;
|
||||
tabindex: number;
|
||||
/** Pass accessibility props to input */
|
||||
attrs: object;
|
||||
inputRef: RefObject;
|
||||
onKeydown: EventHandlerNonNull;
|
||||
onMousedown: EventHandlerNonNull;
|
||||
onChange: EventHandlerNonNull;
|
||||
onPaste: EventHandlerNonNull;
|
||||
onCompositionstart: EventHandlerNonNull;
|
||||
onCompositionend: EventHandlerNonNull;
|
||||
}
|
||||
|
||||
const Input = defineComponent<InputProps>({
|
||||
name: 'Input',
|
||||
inheritAttrs: false,
|
||||
setup() {
|
||||
return {
|
||||
VCSelectContainerEvent: inject('VCSelectContainerEvent'),
|
||||
};
|
||||
},
|
||||
render() {
|
||||
const {
|
||||
prefixCls,
|
||||
id,
|
||||
inputElement,
|
||||
disabled,
|
||||
tabindex,
|
||||
autofocus,
|
||||
autocomplete,
|
||||
editable,
|
||||
accessibilityIndex,
|
||||
value,
|
||||
onKeydown,
|
||||
onMousedown,
|
||||
onChange,
|
||||
onPaste,
|
||||
onCompositionstart,
|
||||
onCompositionend,
|
||||
open,
|
||||
inputRef,
|
||||
attrs,
|
||||
} = this.$props as InputProps;
|
||||
let inputNode: any = withDirectives((inputElement || <input />) as VNode, [[antInput]]);
|
||||
|
||||
const inputProps = inputNode.props || {};
|
||||
const {
|
||||
onKeydown: onOriginKeyDown,
|
||||
onInput: onOriginInput,
|
||||
onMousedown: onOriginMouseDown,
|
||||
onCompositionstart: onOriginCompositionStart,
|
||||
onCompositionend: onOriginCompositionEnd,
|
||||
style,
|
||||
} = inputProps;
|
||||
|
||||
inputNode = cloneElement(inputNode, {
|
||||
id,
|
||||
ref: inputRef,
|
||||
disabled,
|
||||
tabindex,
|
||||
autocomplete: autocomplete || 'off',
|
||||
type: 'search',
|
||||
autofocus,
|
||||
class: `${prefixCls}-selection-search-input`,
|
||||
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': `${id}_list_${accessibilityIndex}`,
|
||||
...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[]) => {
|
||||
this.VCSelectContainerEvent?.focus(args[0]);
|
||||
},
|
||||
onBlur: (...args: any[]) => {
|
||||
this.VCSelectContainerEvent?.blur(args[0]);
|
||||
},
|
||||
}) as VNode;
|
||||
return inputNode;
|
||||
},
|
||||
});
|
||||
|
||||
Input.props = {
|
||||
inputRef: PropTypes.any,
|
||||
prefixCls: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
inputElement: PropTypes.any,
|
||||
disabled: PropTypes.bool,
|
||||
autofocus: PropTypes.bool,
|
||||
autocomplete: PropTypes.string,
|
||||
editable: PropTypes.bool,
|
||||
accessibilityIndex: PropTypes.number,
|
||||
value: PropTypes.string,
|
||||
open: PropTypes.bool,
|
||||
tabindex: PropTypes.number,
|
||||
/** 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,
|
||||
};
|
||||
|
||||
export default Input;
|
||||
|
|
@ -0,0 +1,281 @@
|
|||
import TransBtn from '../TransBtn';
|
||||
import { LabelValueType, RawValueType, CustomTagProps } from '../interface/generator';
|
||||
import { RenderNode } from '../interface';
|
||||
import { InnerSelectorProps } from '.';
|
||||
import Input from './Input';
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
onMounted,
|
||||
ref,
|
||||
TransitionGroup,
|
||||
VNodeChild,
|
||||
watch,
|
||||
watchEffect,
|
||||
Ref,
|
||||
} from 'vue';
|
||||
import classNames from '../../_util/classNames';
|
||||
import pickAttrs from '../../_util/pickAttrs';
|
||||
import PropTypes from '../../_util/vue-types';
|
||||
import getTransitionProps from '../../_util/getTransitionProps';
|
||||
|
||||
const REST_TAG_KEY = '__RC_SELECT_MAX_REST_COUNT__';
|
||||
|
||||
interface SelectorProps extends InnerSelectorProps {
|
||||
// Icon
|
||||
removeIcon?: RenderNode;
|
||||
|
||||
// Tags
|
||||
maxTagCount?: number;
|
||||
maxTagTextLength?: number;
|
||||
maxTagPlaceholder?: VNodeChild;
|
||||
tokenSeparators?: string[];
|
||||
tagRender?: (props: CustomTagProps) => VNodeChild;
|
||||
|
||||
// Motion
|
||||
choiceTransitionName?: string;
|
||||
|
||||
// Event
|
||||
onSelect: (value: RawValueType, option: { selected: boolean }) => void;
|
||||
}
|
||||
|
||||
const props = {
|
||||
id: PropTypes.string,
|
||||
prefixCls: PropTypes.string,
|
||||
values: PropTypes.array,
|
||||
open: PropTypes.bool,
|
||||
searchValue: PropTypes.string,
|
||||
inputRef: PropTypes.any,
|
||||
placeholder: PropTypes.any,
|
||||
disabled: PropTypes.bool,
|
||||
mode: PropTypes.string,
|
||||
showSearch: PropTypes.bool,
|
||||
autofocus: PropTypes.bool,
|
||||
autocomplete: PropTypes.string,
|
||||
accessibilityIndex: PropTypes.number,
|
||||
tabindex: PropTypes.number,
|
||||
|
||||
removeIcon: PropTypes.bool,
|
||||
choiceTransitionName: PropTypes.string,
|
||||
|
||||
maxTagCount: PropTypes.number,
|
||||
maxTagTextLength: PropTypes.number,
|
||||
maxTagPlaceholder: PropTypes.any.def(
|
||||
(omittedValues: LabelValueType[]) => `+ ${omittedValues.length} ...`,
|
||||
),
|
||||
tagRender: PropTypes.func,
|
||||
|
||||
onSelect: PropTypes.func,
|
||||
onInputChange: PropTypes.func,
|
||||
onInputPaste: PropTypes.func,
|
||||
onInputKeyDown: PropTypes.func,
|
||||
onInputMouseDown: PropTypes.func,
|
||||
onInputCompositionStart: PropTypes.func,
|
||||
onInputCompositionEnd: PropTypes.func,
|
||||
};
|
||||
|
||||
const SelectSelector = defineComponent<SelectorProps>({
|
||||
name: 'SelectSelector',
|
||||
setup(props) {
|
||||
let motionAppear = false; // not need use ref, because not need trigger watchEffect
|
||||
const measureRef = ref();
|
||||
const inputWidth = ref(0);
|
||||
|
||||
// ===================== Motion ======================
|
||||
onMounted(() => {
|
||||
motionAppear = true;
|
||||
});
|
||||
|
||||
// ===================== Search ======================
|
||||
const inputValue = computed(() =>
|
||||
props.open || props.mode === 'tags' ? props.searchValue : '',
|
||||
);
|
||||
const inputEditable: Ref<boolean> = computed(
|
||||
() => props.mode === 'tags' || ((props.open && props.showSearch) as boolean),
|
||||
);
|
||||
|
||||
// We measure width and set to the input immediately
|
||||
watch(
|
||||
inputValue,
|
||||
() => {
|
||||
inputWidth.value = measureRef.value.scrollWidth;
|
||||
},
|
||||
{ flush: 'pre' },
|
||||
);
|
||||
const selectionNode = ref();
|
||||
watchEffect(() => {
|
||||
const {
|
||||
values,
|
||||
prefixCls,
|
||||
removeIcon,
|
||||
choiceTransitionName,
|
||||
maxTagCount,
|
||||
maxTagTextLength,
|
||||
maxTagPlaceholder = (omittedValues: LabelValueType[]) => `+ ${omittedValues.length} ...`,
|
||||
tagRender,
|
||||
onSelect,
|
||||
} = props;
|
||||
// ==================== Selection ====================
|
||||
let displayValues: LabelValueType[] = values;
|
||||
|
||||
// Cut by `maxTagCount`
|
||||
let restCount: number;
|
||||
if (typeof maxTagCount === 'number') {
|
||||
restCount = values.length - maxTagCount;
|
||||
displayValues = values.slice(0, maxTagCount);
|
||||
}
|
||||
|
||||
// Update by `maxTagTextLength`
|
||||
if (typeof maxTagTextLength === 'number') {
|
||||
displayValues = displayValues.map(({ label, ...rest }) => {
|
||||
let displayLabel = label;
|
||||
|
||||
if (typeof label === 'string' || typeof label === 'number') {
|
||||
const strLabel = String(displayLabel);
|
||||
|
||||
if (strLabel.length > maxTagTextLength) {
|
||||
displayLabel = `${strLabel.slice(0, maxTagTextLength)}...`;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...rest,
|
||||
label: displayLabel,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Fill rest
|
||||
if (restCount > 0) {
|
||||
displayValues.push({
|
||||
key: REST_TAG_KEY,
|
||||
label:
|
||||
typeof maxTagPlaceholder === 'function'
|
||||
? maxTagPlaceholder(values.slice(maxTagCount))
|
||||
: maxTagPlaceholder,
|
||||
});
|
||||
}
|
||||
const transitionProps = getTransitionProps(choiceTransitionName, {
|
||||
appear: motionAppear,
|
||||
});
|
||||
selectionNode.value = (
|
||||
<TransitionGroup {...transitionProps}>
|
||||
{displayValues.map(
|
||||
({ key, label, value, disabled: itemDisabled, class: className, style }) => {
|
||||
const mergedKey = key || value;
|
||||
const closable = key !== REST_TAG_KEY && !itemDisabled;
|
||||
const onMousedown = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
const onClose = (event?: MouseEvent) => {
|
||||
if (event) event.stopPropagation();
|
||||
onSelect(value as RawValueType, { selected: false });
|
||||
};
|
||||
|
||||
return typeof tagRender === 'function' ? (
|
||||
<span
|
||||
key={mergedKey as string}
|
||||
onMousedown={onMousedown}
|
||||
class={className}
|
||||
style={style}
|
||||
>
|
||||
{tagRender({
|
||||
label,
|
||||
value,
|
||||
disabled: itemDisabled,
|
||||
closable,
|
||||
onClose,
|
||||
} as CustomTagProps)}
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
key={mergedKey as string}
|
||||
class={classNames(className, `${prefixCls}-selection-item`, {
|
||||
[`${prefixCls}-selection-item-disabled`]: itemDisabled,
|
||||
})}
|
||||
style={style}
|
||||
>
|
||||
<span class={`${prefixCls}-selection-item-content`}>{label}</span>
|
||||
{closable && (
|
||||
<TransBtn
|
||||
class={`${prefixCls}-selection-item-remove`}
|
||||
onMousedown={onMousedown}
|
||||
onClick={onClose}
|
||||
customizeIcon={removeIcon}
|
||||
>
|
||||
×
|
||||
</TransBtn>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
)}
|
||||
{}
|
||||
</TransitionGroup>
|
||||
);
|
||||
});
|
||||
|
||||
return () => {
|
||||
const {
|
||||
id,
|
||||
prefixCls,
|
||||
values,
|
||||
open,
|
||||
inputRef,
|
||||
placeholder,
|
||||
disabled,
|
||||
autofocus,
|
||||
autocomplete,
|
||||
accessibilityIndex,
|
||||
tabindex,
|
||||
onInputChange,
|
||||
onInputPaste,
|
||||
onInputKeyDown,
|
||||
onInputMouseDown,
|
||||
onInputCompositionStart,
|
||||
onInputCompositionEnd,
|
||||
} = props;
|
||||
return (
|
||||
<>
|
||||
{selectionNode.value}
|
||||
<span class={`${prefixCls}-selection-search`} style={{ width: inputWidth.value + 'px' }}>
|
||||
<Input
|
||||
inputRef={inputRef}
|
||||
open={open}
|
||||
prefixCls={prefixCls}
|
||||
id={id}
|
||||
inputElement={null}
|
||||
disabled={disabled}
|
||||
autofocus={autofocus}
|
||||
autocomplete={autocomplete}
|
||||
editable={inputEditable.value as boolean}
|
||||
accessibilityIndex={accessibilityIndex}
|
||||
value={inputValue.value}
|
||||
onKeydown={onInputKeyDown}
|
||||
onMousedown={onInputMouseDown}
|
||||
onChange={onInputChange}
|
||||
onPaste={onInputPaste}
|
||||
onCompositionstart={onInputCompositionStart}
|
||||
onCompositionend={onInputCompositionEnd}
|
||||
tabindex={tabindex}
|
||||
attrs={pickAttrs(props, true)}
|
||||
/>
|
||||
|
||||
{/* Measure Node */}
|
||||
<span ref={measureRef} class={`${prefixCls}-selection-search-mirror`} aria-hidden>
|
||||
{inputValue.value}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{!values.length && !inputValue.value && (
|
||||
<span class={`${prefixCls}-selection-placeholder`}>{placeholder}</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
SelectSelector.inheritAttrs = false;
|
||||
SelectSelector.props = props;
|
||||
export default SelectSelector;
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
import pickAttrs from '../../_util/pickAttrs';
|
||||
import Input from './Input';
|
||||
import { InnerSelectorProps } from '.';
|
||||
import { computed, defineComponent, ref, VNodeChild, watch } from 'vue';
|
||||
import PropTypes from '../../_util/vue-types';
|
||||
|
||||
interface SelectorProps extends InnerSelectorProps {
|
||||
inputElement: VNodeChild;
|
||||
activeValue: string;
|
||||
backfill?: boolean;
|
||||
}
|
||||
const props = {
|
||||
inputElement: PropTypes.any,
|
||||
id: PropTypes.string,
|
||||
prefixCls: PropTypes.string,
|
||||
values: PropTypes.array,
|
||||
open: PropTypes.bool,
|
||||
searchValue: PropTypes.string,
|
||||
inputRef: PropTypes.any,
|
||||
placeholder: PropTypes.any,
|
||||
disabled: PropTypes.bool,
|
||||
mode: PropTypes.string,
|
||||
showSearch: PropTypes.bool,
|
||||
autofocus: PropTypes.bool,
|
||||
autocomplete: PropTypes.string,
|
||||
accessibilityIndex: PropTypes.number,
|
||||
tabindex: PropTypes.number,
|
||||
activeValue: PropTypes.string,
|
||||
backfill: PropTypes.bool,
|
||||
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;
|
||||
});
|
||||
watch(
|
||||
computed(() => [combobox.value, props.activeValue]),
|
||||
() => {
|
||||
if (combobox.value) {
|
||||
inputChanged.value = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Not show text when closed expect combobox mode
|
||||
const hasTextInput = computed(() =>
|
||||
props.mode !== 'combobox' && !props.open ? 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;
|
||||
});
|
||||
|
||||
return () => {
|
||||
const {
|
||||
inputElement,
|
||||
prefixCls,
|
||||
id,
|
||||
values,
|
||||
inputRef,
|
||||
disabled,
|
||||
autofocus,
|
||||
autocomplete,
|
||||
accessibilityIndex,
|
||||
open,
|
||||
placeholder,
|
||||
tabindex,
|
||||
onInputKeyDown,
|
||||
onInputMouseDown,
|
||||
onInputChange,
|
||||
onInputPaste,
|
||||
onInputCompositionStart,
|
||||
onInputCompositionEnd,
|
||||
} = props;
|
||||
const item = values[0];
|
||||
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}
|
||||
accessibilityIndex={accessibilityIndex}
|
||||
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}>
|
||||
{Array.isArray(item.label) ? item.label.map(la => la) : item.label}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Display placeholder */}
|
||||
{!item && !hasTextInput.value && (
|
||||
<span class={`${prefixCls}-selection-placeholder`}>{placeholder}</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
SingleSelector.props = props;
|
||||
SingleSelector.inheritAttrs = false;
|
||||
|
||||
export default SingleSelector;
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
/**
|
||||
* 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 { LabelValueType, RawValueType, CustomTagProps } from '../interface/generator';
|
||||
import { RenderNode, Mode } from '../interface';
|
||||
import useLock from '../hooks/useLock';
|
||||
import { defineComponent, VNode, VNodeChild } from 'vue';
|
||||
import createRef, { RefObject } from '../../_util/createRef';
|
||||
import PropTypes from '../../_util/vue-types copy';
|
||||
|
||||
export interface InnerSelectorProps {
|
||||
prefixCls: string;
|
||||
id: string;
|
||||
mode: Mode;
|
||||
inputRef: RefObject;
|
||||
placeholder?: VNodeChild;
|
||||
disabled?: boolean;
|
||||
autofocus?: boolean;
|
||||
autocomplete?: string;
|
||||
values: LabelValueType[];
|
||||
showSearch?: boolean;
|
||||
searchValue: string;
|
||||
accessibilityIndex: number;
|
||||
open: boolean;
|
||||
tabindex?: number;
|
||||
onInputKeyDown: EventHandlerNonNull;
|
||||
onInputMouseDown: EventHandlerNonNull;
|
||||
onInputChange: EventHandlerNonNull;
|
||||
onInputPaste: EventHandlerNonNull;
|
||||
onInputCompositionStart: EventHandlerNonNull;
|
||||
onInputCompositionEnd: EventHandlerNonNull;
|
||||
}
|
||||
|
||||
export interface SelectorProps {
|
||||
id: string;
|
||||
prefixCls: string;
|
||||
showSearch?: boolean;
|
||||
open: boolean;
|
||||
/** Display in the Selector value, it's not same as `value` prop */
|
||||
values: LabelValueType[];
|
||||
multiple: boolean;
|
||||
mode: Mode;
|
||||
searchValue: string;
|
||||
activeValue: string;
|
||||
inputElement: JSX.Element;
|
||||
|
||||
autofocus?: boolean;
|
||||
accessibilityIndex: number;
|
||||
tabindex?: number;
|
||||
disabled?: boolean;
|
||||
placeholder?: VNodeChild;
|
||||
removeIcon?: RenderNode;
|
||||
|
||||
// Tags
|
||||
maxTagCount?: number;
|
||||
maxTagTextLength?: number;
|
||||
maxTagPlaceholder?: VNodeChild;
|
||||
tagRender?: (props: CustomTagProps) => VNode;
|
||||
|
||||
/** Check if `tokenSeparators` contains `\n` or `\r\n` */
|
||||
tokenWithEnter?: boolean;
|
||||
|
||||
// Motion
|
||||
choiceTransitionName?: string;
|
||||
|
||||
onToggleOpen: (open?: boolean) => void;
|
||||
/** `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;
|
||||
onSelect: (value: RawValueType, option: { selected: boolean }) => void;
|
||||
onInputKeyDown?: EventHandlerNonNull;
|
||||
|
||||
/**
|
||||
* @private get real dom for trigger align.
|
||||
* This may be removed after React provides replacement of `findDOMNode`
|
||||
*/
|
||||
domRef: () => HTMLDivElement;
|
||||
}
|
||||
|
||||
const Selector = defineComponent<SelectorProps>({
|
||||
name: 'Selector',
|
||||
setup(props) {
|
||||
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 (![KeyCode.SHIFT, KeyCode.TAB, KeyCode.BACKSPACE, KeyCode.ESC].includes(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 = () => {
|
||||
compositionStatus = false;
|
||||
};
|
||||
|
||||
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/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();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
focus: () => {
|
||||
inputRef.current.focus();
|
||||
},
|
||||
blur: () => {
|
||||
inputRef.current.blur();
|
||||
},
|
||||
onMousedown,
|
||||
onClick,
|
||||
onInputPaste,
|
||||
inputRef,
|
||||
onInternalInputKeyDown,
|
||||
onInternalInputMouseDown,
|
||||
onInputChange,
|
||||
onInputCompositionEnd,
|
||||
onInputCompositionStart,
|
||||
};
|
||||
},
|
||||
render() {
|
||||
const { prefixCls, domRef, multiple } = this.$props as SelectorProps;
|
||||
const {
|
||||
onMousedown,
|
||||
onClick,
|
||||
inputRef,
|
||||
onInputPaste,
|
||||
onInternalInputKeyDown,
|
||||
onInternalInputMouseDown,
|
||||
onInputChange,
|
||||
onInputCompositionStart,
|
||||
onInputCompositionEnd,
|
||||
} = this;
|
||||
const sharedProps = {
|
||||
inputRef,
|
||||
onInputKeyDown: onInternalInputKeyDown,
|
||||
onInputMouseDown: onInternalInputMouseDown,
|
||||
onInputChange,
|
||||
onInputPaste,
|
||||
onInputCompositionStart,
|
||||
onInputCompositionEnd,
|
||||
};
|
||||
const selectNode = multiple ? (
|
||||
<MultipleSelector {...this.$props} {...sharedProps} />
|
||||
) : (
|
||||
<SingleSelector {...this.$props} {...sharedProps} />
|
||||
);
|
||||
return (
|
||||
<div ref={domRef} class={`${prefixCls}-selector`} onClick={onClick} onMousedown={onMousedown}>
|
||||
{selectNode}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
Selector.inheritAttrs = false;
|
||||
Selector.props = {
|
||||
id: PropTypes.string,
|
||||
prefixCls: PropTypes.string,
|
||||
showSearch: PropTypes.bool,
|
||||
open: PropTypes.bool,
|
||||
/** Display in the Selector value, it's not same as `value` prop */
|
||||
values: PropTypes.array,
|
||||
multiple: PropTypes.bool,
|
||||
mode: PropTypes.string,
|
||||
searchValue: PropTypes.string,
|
||||
activeValue: PropTypes.string,
|
||||
inputElement: PropTypes.any,
|
||||
|
||||
autofocus: PropTypes.bool,
|
||||
accessibilityIndex: PropTypes.number,
|
||||
tabindex: PropTypes.number,
|
||||
disabled: PropTypes.bool,
|
||||
placeholder: PropTypes.any,
|
||||
removeIcon: PropTypes.any,
|
||||
|
||||
// Tags
|
||||
maxTagCount: PropTypes.number,
|
||||
maxTagTextLength: PropTypes.number,
|
||||
maxTagPlaceholder: PropTypes.any,
|
||||
tagRender: PropTypes.func,
|
||||
|
||||
/** Check if `tokenSeparators` contains `\n` or `\r\n` */
|
||||
tokenWithEnter: PropTypes.bool,
|
||||
|
||||
// Motion
|
||||
choiceTransitionName: PropTypes.string,
|
||||
|
||||
onToggleOpen: PropTypes.func,
|
||||
/** `onSearch` returns go next step boolean to check if need do toggle open */
|
||||
onSearch: PropTypes.func,
|
||||
onSearchSubmit: PropTypes.func,
|
||||
onSelect: PropTypes.func,
|
||||
onInputKeyDown: PropTypes.func,
|
||||
|
||||
/**
|
||||
* @private get real dom for trigger align.
|
||||
* This may be removed after React provides replacement of `findDOMNode`
|
||||
*/
|
||||
domRef: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Selector;
|
||||
|
|
@ -0,0 +1,345 @@
|
|||
@select-prefix: ~'rc-select';
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-input-without-border() {
|
||||
.@{select-prefix}-selection-search-input {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: rgba(255, 0, 0, 0.2);
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.@{select-prefix} {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
width: 100px;
|
||||
position: relative;
|
||||
|
||||
&-disabled {
|
||||
&,
|
||||
& input {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.@{select-prefix}-selector {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
&-show-arrow&-loading {
|
||||
.@{select-prefix}-arrow {
|
||||
&-icon::after {
|
||||
box-sizing: border-box;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 100%;
|
||||
border: 2px solid #999;
|
||||
border-top-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
transform: none;
|
||||
margin-top: 4px;
|
||||
|
||||
animation: rcSelectLoadingIcon 0.5s infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Selector ===============
|
||||
.@{select-prefix}-selection-placeholder {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
// ============== Search ===============
|
||||
.@{select-prefix}-selection-search-input {
|
||||
appearance: none;
|
||||
|
||||
&::-webkit-search-cancel-button {
|
||||
display: none;
|
||||
appearance: none;
|
||||
}
|
||||
}
|
||||
|
||||
// --------------- Single ----------------
|
||||
&-single {
|
||||
.@{select-prefix}-selector {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
.@{select-prefix}-selection-search {
|
||||
width: 100%;
|
||||
|
||||
&-input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.@{select-prefix}-selection-item,
|
||||
.@{select-prefix}-selection-placeholder {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 3px;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Not customize
|
||||
&:not(.@{select-prefix}-customize-input) {
|
||||
.@{select-prefix}-selector {
|
||||
padding: 1px;
|
||||
border: 1px solid #000;
|
||||
|
||||
.search-input-without-border();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------- Multiple ---------------
|
||||
&-multiple .@{select-prefix}-selector {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 1px;
|
||||
border: 1px solid #000;
|
||||
|
||||
.@{select-prefix}-selection-item {
|
||||
flex: none;
|
||||
background: #bbb;
|
||||
border-radius: 4px;
|
||||
margin-right: 2px;
|
||||
padding: 0 8px;
|
||||
|
||||
&-disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.@{select-prefix}-selection-search {
|
||||
position: relative;
|
||||
|
||||
&-input,
|
||||
&-mirror {
|
||||
padding: 1px;
|
||||
font-family: system-ui;
|
||||
}
|
||||
|
||||
&-mirror {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
white-space: nowrap;
|
||||
position: none;
|
||||
left: 0;
|
||||
top: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.search-input-without-border();
|
||||
}
|
||||
|
||||
// ================ Icons ================
|
||||
&-allow-clear {
|
||||
&.@{select-prefix}-multiple .@{select-prefix}-selector {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.@{select-prefix}-clear {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-show-arrow {
|
||||
&.@{select-prefix}-multiple .@{select-prefix}-selector {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.@{select-prefix}-arrow {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 0;
|
||||
|
||||
&-icon::after {
|
||||
content: '';
|
||||
border: 5px solid transparent;
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: inline-block;
|
||||
border-top-color: #999;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============== Focused ===============
|
||||
&-focused {
|
||||
.@{select-prefix}-selector {
|
||||
border-color: blue !important;
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Dropdown ===============
|
||||
&-dropdown {
|
||||
border: 1px solid green;
|
||||
min-height: 100px;
|
||||
position: absolute;
|
||||
background: #fff;
|
||||
|
||||
&-hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// =============== Option ================
|
||||
&-item {
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
padding: 4px 16px;
|
||||
|
||||
// >>> Group
|
||||
&-group {
|
||||
color: #999;
|
||||
font-weight: bold;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
// >>> Option
|
||||
&-option {
|
||||
position: relative;
|
||||
|
||||
&-grouped {
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.@{select-prefix}-item-option-state {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 4px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// ------- Active -------
|
||||
&-active {
|
||||
background: green;
|
||||
}
|
||||
|
||||
// ------ Disabled ------
|
||||
&-disabled {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
// >>> Empty
|
||||
&-empty {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{select-prefix}-selection__choice-zoom {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.@{select-prefix}-selection__choice-zoom-appear {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
|
||||
&&-active {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.@{select-prefix}-selection__choice-zoom-leave {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
|
||||
&&-active {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.effect() {
|
||||
animation-duration: 0.3s;
|
||||
animation-fill-mode: both;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.@{select-prefix}-dropdown {
|
||||
&-slide-up-enter,
|
||||
&-slide-up-appear {
|
||||
.effect();
|
||||
opacity: 0;
|
||||
animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1);
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
&-slide-up-leave {
|
||||
.effect();
|
||||
opacity: 1;
|
||||
animation-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.34);
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
&-slide-up-enter&-slide-up-enter-active&-placement-bottomLeft,
|
||||
&-slide-up-appear&-slide-up-appear-active&-placement-bottomLeft {
|
||||
animation-name: rcSelectDropdownSlideUpIn;
|
||||
animation-play-state: running;
|
||||
}
|
||||
|
||||
&-slide-up-leave&-slide-up-leave-active&-placement-bottomLeft {
|
||||
animation-name: rcSelectDropdownSlideUpOut;
|
||||
animation-play-state: running;
|
||||
}
|
||||
|
||||
&-slide-up-enter&-slide-up-enter-active&-placement-topLeft,
|
||||
&-slide-up-appear&-slide-up-appear-active&-placement-topLeft {
|
||||
animation-name: rcSelectDropdownSlideDownIn;
|
||||
animation-play-state: running;
|
||||
}
|
||||
|
||||
&-slide-up-leave&-slide-up-leave-active&-placement-topLeft {
|
||||
animation-name: rcSelectDropdownSlideDownOut;
|
||||
animation-play-state: running;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rcSelectDropdownSlideUpIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform-origin: 0% 0%;
|
||||
transform: scaleY(0);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform-origin: 0% 0%;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
@keyframes rcSelectDropdownSlideUpOut {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform-origin: 0% 0%;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform-origin: 0% 0%;
|
||||
transform: scaleY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rcSelectLoadingIcon {
|
||||
0% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
// input {
|
||||
// // height: 24px;
|
||||
// // line-height: 24px;
|
||||
// border: 1px solid #333;
|
||||
// border-radius: 4px;
|
||||
// }
|
||||
|
||||
// button {
|
||||
// border: 1px solid #333;
|
||||
// }
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
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);
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.test-option {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
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 */
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,35 @@
|
|||
import { computed, ComputedRef, Ref } from 'vue';
|
||||
import { DisplayLabelValueType } from '../interface/generator';
|
||||
|
||||
export default function useCacheDisplayValue(
|
||||
values: Ref<DisplayLabelValueType[]>,
|
||||
): ComputedRef<DisplayLabelValueType[]> {
|
||||
let prevValues = [...values.value];
|
||||
|
||||
const mergedValues = computed(() => {
|
||||
// Create value - label map
|
||||
const valueLabels = new Map();
|
||||
prevValues.forEach(({ value, label }) => {
|
||||
if (value !== label) {
|
||||
valueLabels.set(value, label);
|
||||
}
|
||||
});
|
||||
|
||||
const resultValues = values.value.map(item => {
|
||||
const cacheLabel = valueLabels.get(item.value);
|
||||
if (item.value === item.label && cacheLabel) {
|
||||
return {
|
||||
...item,
|
||||
label: cacheLabel,
|
||||
};
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
prevValues = resultValues;
|
||||
return resultValues;
|
||||
});
|
||||
|
||||
return mergedValues;
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { computed, Ref, VNodeChild } from 'vue';
|
||||
import { RawValueType, FlattenOptionsType, Key } from '../interface/generator';
|
||||
|
||||
export default function useCacheOptions<
|
||||
OptionsType extends {
|
||||
value?: RawValueType;
|
||||
label?: VNodeChild;
|
||||
key?: Key;
|
||||
disabled?: boolean;
|
||||
}[]
|
||||
>(_values: RawValueType[], options: Ref) {
|
||||
const optionMap = computed(() => {
|
||||
const map: Map<RawValueType, FlattenOptionsType<OptionsType>[number]> = new Map();
|
||||
options.value.forEach(item => {
|
||||
const {
|
||||
data: { value },
|
||||
} = item;
|
||||
map.set(value, item);
|
||||
});
|
||||
return map;
|
||||
});
|
||||
|
||||
const getValueOption = (vals: RawValueType[]): FlattenOptionsType<OptionsType> =>
|
||||
vals.map(value => optionMap.value.get(value)).filter(Boolean);
|
||||
|
||||
return getValueOption;
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { onBeforeUpdate, Ref, 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: number;
|
||||
|
||||
const cancelLatest = () => {
|
||||
window.clearTimeout(delay);
|
||||
};
|
||||
|
||||
onBeforeUpdate(() => {
|
||||
cancelLatest();
|
||||
});
|
||||
const delaySetBool = (value: boolean, callback: () => void) => {
|
||||
cancelLatest();
|
||||
|
||||
delay = window.setTimeout(() => {
|
||||
bool.value = value;
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}, timeout);
|
||||
};
|
||||
|
||||
return [bool, delaySetBool, cancelLatest];
|
||||
}
|
||||
|
|
@ -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: number;
|
||||
|
||||
onBeforeUpdate(() => {
|
||||
window.clearTimeout(timeout);
|
||||
});
|
||||
|
||||
function doLock(locked: boolean) {
|
||||
if (locked || lock === null) {
|
||||
lock = locked;
|
||||
}
|
||||
|
||||
window.clearTimeout(timeout);
|
||||
timeout = window.setTimeout(() => {
|
||||
lock = null;
|
||||
}, duration);
|
||||
}
|
||||
|
||||
return [() => lock, doLock];
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { onBeforeUnmount, onMounted, Ref } from 'vue';
|
||||
|
||||
export default function useSelectTriggerControl(
|
||||
elements: (HTMLElement | undefined)[],
|
||||
open: Ref<boolean>,
|
||||
triggerOpen: (open: boolean) => void,
|
||||
) {
|
||||
function onGlobalMouseDown(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
// based on vc-select 9.2.2
|
||||
import Select from './Select';
|
||||
import Option from './Option';
|
||||
import { SelectPropTypes } from './PropTypes';
|
||||
import OptGroup from './OptGroup';
|
||||
Select.Option = Option;
|
||||
Select.OptGroup = OptGroup;
|
||||
export { Select, Option, OptGroup, SelectPropTypes };
|
||||
export default Select;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import Select, { ExportedSelectProps } from './Select';
|
||||
import Option from './Option';
|
||||
import OptGroup from './OptGroup';
|
||||
type SelectProps = ExportedSelectProps;
|
||||
export { Option, OptGroup, SelectProps };
|
||||
|
||||
export default Select;
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import * as Vue from 'vue';
|
||||
import { SelectProps, RefSelectProps } from '../generate';
|
||||
|
||||
export type SelectSource = 'option' | 'selection' | 'input';
|
||||
|
||||
|
|
@ -8,9 +7,9 @@ export const INTERNAL_PROPS_MARK = 'RC_SELECT_INTERNAL_PROPS_MARK';
|
|||
// =================================== Shared Type ===================================
|
||||
export type Key = string | number;
|
||||
|
||||
export type RawValueType = string | number;
|
||||
export type RawValueType = string | number | null;
|
||||
|
||||
export interface LabelValueType {
|
||||
export interface LabelValueType extends Record<string, any> {
|
||||
key?: Key;
|
||||
value?: RawValueType;
|
||||
label?: Vue.VNodeChild;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import * as Vue from 'vue';
|
||||
import { VNode } from 'vue';
|
||||
import { Key, RawValueType } from './generator';
|
||||
|
||||
export type RenderDOMFunc = (props: any) => HTMLElement;
|
||||
|
|
@ -19,12 +20,11 @@ export interface OptionCoreData {
|
|||
disabled?: boolean;
|
||||
value: Key;
|
||||
title?: string;
|
||||
className?: string;
|
||||
class?: string;
|
||||
style?: Vue.CSSProperties;
|
||||
label?: Vue.VNodeChild;
|
||||
/** @deprecated Only works when use `children` as option data */
|
||||
children?: Vue.VNodeChild;
|
||||
children?: VNode[] | JSX.Element[];
|
||||
}
|
||||
|
||||
export interface OptionData extends OptionCoreData {
|
||||
|
|
@ -36,7 +36,6 @@ export interface OptionGroupData {
|
|||
key?: Key;
|
||||
label?: Vue.VNodeChild;
|
||||
options: OptionData[];
|
||||
className?: string;
|
||||
class?: string;
|
||||
style?: Vue.CSSProperties;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
import {
|
||||
RawValueType,
|
||||
GetLabeledValue,
|
||||
LabelValueType,
|
||||
DefaultValueType,
|
||||
FlattenOptionsType,
|
||||
} from '../interface/generator';
|
||||
|
||||
export function toArray<T>(value: T | T[]): T[] {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
return value !== undefined ? [value] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert outer props value into internal value
|
||||
*/
|
||||
export function toInnerValue(
|
||||
value: DefaultValueType,
|
||||
{ labelInValue, combobox }: { labelInValue: boolean; combobox: boolean },
|
||||
): RawValueType[] {
|
||||
if (value === undefined || (value === '' && combobox)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const values = Array.isArray(value) ? value : [value];
|
||||
|
||||
if (labelInValue) {
|
||||
return (values as LabelValueType[]).map(({ key, value: val }: LabelValueType) =>
|
||||
val !== undefined ? val : key,
|
||||
);
|
||||
}
|
||||
|
||||
return values as RawValueType[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert internal value into out event value
|
||||
*/
|
||||
export function toOuterValues<FOT extends FlattenOptionsType>(
|
||||
valueList: RawValueType[],
|
||||
{
|
||||
optionLabelProp,
|
||||
labelInValue,
|
||||
prevValue,
|
||||
options,
|
||||
getLabeledValue,
|
||||
}: {
|
||||
optionLabelProp: string;
|
||||
labelInValue: boolean;
|
||||
getLabeledValue: GetLabeledValue<FOT>;
|
||||
options: FOT;
|
||||
prevValue: DefaultValueType;
|
||||
},
|
||||
): RawValueType[] | LabelValueType[] | DefaultValueType {
|
||||
let values: DefaultValueType = valueList;
|
||||
|
||||
if (labelInValue) {
|
||||
values = values.map(val =>
|
||||
getLabeledValue(val, {
|
||||
options,
|
||||
prevValue,
|
||||
labelInValue,
|
||||
optionLabelProp,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
export function removeLastEnabledValue<
|
||||
T extends { disabled?: boolean },
|
||||
P extends RawValueType | object
|
||||
>(measureValues: T[], values: P[]): { values: P[]; removedValue: P } {
|
||||
const newValues = [...values];
|
||||
|
||||
let removeIndex: number;
|
||||
for (removeIndex = measureValues.length - 1; removeIndex >= 0; removeIndex -= 1) {
|
||||
if (!measureValues[removeIndex].disabled) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let removedValue = null;
|
||||
|
||||
if (removeIndex !== -1) {
|
||||
removedValue = newValues[removeIndex];
|
||||
newValues.splice(removeIndex, 1);
|
||||
}
|
||||
|
||||
return {
|
||||
values: newValues,
|
||||
removedValue,
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
let uuid = 0;
|
||||
/** 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;
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import { flattenChildren, isValidElement } from '../../_util/props-util';
|
||||
import { VNode } from 'vue';
|
||||
import { OptionData, OptionGroupData, OptionsType } from '../interface';
|
||||
|
||||
function convertNodeToOption(node: VNode): OptionData {
|
||||
const {
|
||||
key,
|
||||
children,
|
||||
props: { value, ...restProps },
|
||||
} = node as VNode & {
|
||||
children: { default?: () => any };
|
||||
};
|
||||
const child = children.default ? children.default() : undefined;
|
||||
return { key, value: value !== undefined ? value : key, children: child, ...restProps };
|
||||
}
|
||||
|
||||
export function convertChildrenToData(nodes: any[], optionOnly = false): OptionsType {
|
||||
const dd = flattenChildren(nodes)
|
||||
.map((node: VNode, index: number): OptionData | OptionGroupData | null => {
|
||||
if (!isValidElement(node) || !node.type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
type: { isSelectOptGroup },
|
||||
key,
|
||||
children,
|
||||
props,
|
||||
} = node as VNode & {
|
||||
type: { isSelectOptGroup?: boolean };
|
||||
children: { default?: () => any };
|
||||
};
|
||||
|
||||
if (optionOnly || !isSelectOptGroup) {
|
||||
return convertNodeToOption(node);
|
||||
}
|
||||
const child = children.default ? children.default() : undefined;
|
||||
return {
|
||||
key: `__RC_SELECT_GRP__${key === null ? index : key}__`,
|
||||
label: key,
|
||||
...props,
|
||||
options: convertChildrenToData(child || []),
|
||||
} as any;
|
||||
})
|
||||
.filter(data => data);
|
||||
return dd;
|
||||
}
|
||||
|
|
@ -0,0 +1,312 @@
|
|||
import { warning } from '../../vc-util/warning';
|
||||
import { VNodeChild } from 'vue';
|
||||
import {
|
||||
OptionsType as SelectOptionsType,
|
||||
OptionData,
|
||||
OptionGroupData,
|
||||
FlattenOptionData,
|
||||
} from '../interface';
|
||||
import {
|
||||
LabelValueType,
|
||||
FilterFunc,
|
||||
RawValueType,
|
||||
GetLabeledValue,
|
||||
DefaultValueType,
|
||||
} from '../interface/generator';
|
||||
|
||||
import { toArray } from './commonUtil';
|
||||
|
||||
function getKey(data: OptionData | OptionGroupData, 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}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(options: SelectOptionsType): FlattenOptionData[] {
|
||||
const flattenList: FlattenOptionData[] = [];
|
||||
|
||||
function dig(list: SelectOptionsType, isGroupOption: boolean) {
|
||||
list.forEach(data => {
|
||||
if (isGroupOption || !('options' in data)) {
|
||||
// Option
|
||||
flattenList.push({
|
||||
key: getKey(data, flattenList.length),
|
||||
groupOption: isGroupOption,
|
||||
data,
|
||||
});
|
||||
} else {
|
||||
// Option Group
|
||||
flattenList.push({
|
||||
key: getKey(data, flattenList.length),
|
||||
group: true,
|
||||
data,
|
||||
});
|
||||
|
||||
dig(data.options, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dig(options, false);
|
||||
|
||||
return flattenList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject `props` into `option` for legacy usage
|
||||
*/
|
||||
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 findValueOption(
|
||||
values: RawValueType[],
|
||||
options: FlattenOptionData[],
|
||||
{ prevValueOptions = [] }: { prevValueOptions?: OptionData[] } = {},
|
||||
): OptionData[] {
|
||||
const optionMap: Map<RawValueType, OptionData> = new Map();
|
||||
|
||||
options.forEach(flattenItem => {
|
||||
if (!flattenItem.group) {
|
||||
const data = flattenItem.data as OptionData;
|
||||
// Check if match
|
||||
optionMap.set(data.value, data);
|
||||
}
|
||||
});
|
||||
|
||||
return values.map(val => {
|
||||
let option = optionMap.get(val);
|
||||
|
||||
// Fallback to try to find prev options
|
||||
if (!option) {
|
||||
option = {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
...prevValueOptions.find(opt => opt._INTERNAL_OPTION_VALUE_ === val),
|
||||
};
|
||||
}
|
||||
|
||||
return injectPropsWithOption(option);
|
||||
});
|
||||
}
|
||||
|
||||
export const getLabeledValue: GetLabeledValue<FlattenOptionData[]> = (
|
||||
value,
|
||||
{ options, prevValue, labelInValue, optionLabelProp },
|
||||
) => {
|
||||
const item = findValueOption([value], options)[0];
|
||||
const result: LabelValueType = {
|
||||
value,
|
||||
};
|
||||
|
||||
let prevValItem: LabelValueType;
|
||||
const prevValues = toArray<RawValueType | LabelValueType>(prevValue);
|
||||
if (labelInValue) {
|
||||
prevValItem = prevValues.find((prevItem: LabelValueType) => {
|
||||
if (typeof prevItem === 'object' && 'value' in prevItem) {
|
||||
return prevItem.value === value;
|
||||
}
|
||||
// [Legacy] Support `key` as `value`
|
||||
return prevItem.key === value;
|
||||
}) as LabelValueType;
|
||||
}
|
||||
|
||||
if (prevValItem && typeof prevValItem === 'object' && 'label' in prevValItem) {
|
||||
result.label = prevValItem.label;
|
||||
|
||||
if (
|
||||
item &&
|
||||
typeof prevValItem.label === 'string' &&
|
||||
typeof item[optionLabelProp] === 'string' &&
|
||||
prevValItem.label.trim() !== item[optionLabelProp].trim()
|
||||
) {
|
||||
warning(false, '`label` of `value` is not same as `label` in Select options.');
|
||||
}
|
||||
} else if (item && optionLabelProp in item) {
|
||||
result.label = item[optionLabelProp];
|
||||
} else {
|
||||
result.label = value;
|
||||
}
|
||||
|
||||
// Used for motion control
|
||||
result.key = result.value;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
function toRawString(content: VNodeChild): string {
|
||||
return toArray(content).join('');
|
||||
}
|
||||
|
||||
/** Filter single option if match the search text */
|
||||
function getFilterFunction(optionFilterProp: string) {
|
||||
return (searchValue: string, option: OptionData | OptionGroupData) => {
|
||||
const lowerSearchText = searchValue.toLowerCase();
|
||||
|
||||
// Group label search
|
||||
if ('options' in option) {
|
||||
return toRawString(option.label)
|
||||
.toLowerCase()
|
||||
.includes(lowerSearchText);
|
||||
}
|
||||
|
||||
// Option value search
|
||||
const rawValue = option[optionFilterProp];
|
||||
const value = toRawString(rawValue).toLowerCase();
|
||||
return value.includes(lowerSearchText);
|
||||
};
|
||||
}
|
||||
|
||||
/** Filter options and return a new options by the search text */
|
||||
export function filterOptions(
|
||||
searchValue: string,
|
||||
options: SelectOptionsType,
|
||||
{
|
||||
optionFilterProp,
|
||||
filterOption,
|
||||
}: { optionFilterProp: string; filterOption: boolean | FilterFunc<SelectOptionsType[number]> },
|
||||
) {
|
||||
const filteredOptions: SelectOptionsType = [];
|
||||
let filterFunc: FilterFunc<SelectOptionsType[number]>;
|
||||
|
||||
if (filterOption === false) {
|
||||
return options;
|
||||
}
|
||||
if (typeof filterOption === 'function') {
|
||||
filterFunc = filterOption;
|
||||
} else {
|
||||
filterFunc = getFilterFunction(optionFilterProp);
|
||||
}
|
||||
|
||||
options.forEach(item => {
|
||||
// Group should check child options
|
||||
if ('options' in item) {
|
||||
// Check group first
|
||||
const matchGroup = filterFunc(searchValue, item);
|
||||
if (matchGroup) {
|
||||
filteredOptions.push(item);
|
||||
} else {
|
||||
// Check option
|
||||
const subOptions = item.options.filter(subItem => filterFunc(searchValue, subItem));
|
||||
if (subOptions.length) {
|
||||
filteredOptions.push({
|
||||
...item,
|
||||
options: subOptions,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (filterFunc(searchValue, injectPropsWithOption(item))) {
|
||||
filteredOptions.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return filteredOptions;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function isValueDisabled(value: RawValueType, options: FlattenOptionData[]): boolean {
|
||||
const option = findValueOption([value], options)[0];
|
||||
return option.disabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* `tags` mode should fill un-list item into the option list
|
||||
*/
|
||||
export function fillOptionsWithMissingValue(
|
||||
options: SelectOptionsType,
|
||||
value: DefaultValueType,
|
||||
optionLabelProp: string,
|
||||
labelInValue: boolean,
|
||||
): SelectOptionsType {
|
||||
const values = toArray<RawValueType | LabelValueType>(value)
|
||||
.slice()
|
||||
.sort();
|
||||
const cloneOptions = [...options];
|
||||
|
||||
// Convert options value to set
|
||||
const optionValues = new Set<RawValueType>();
|
||||
options.forEach(opt => {
|
||||
if (opt.options) {
|
||||
opt.options.forEach((subOpt: OptionData) => {
|
||||
optionValues.add(subOpt.value);
|
||||
});
|
||||
} else {
|
||||
optionValues.add((opt as OptionData).value);
|
||||
}
|
||||
});
|
||||
|
||||
// Fill missing value
|
||||
values.forEach(item => {
|
||||
const val: RawValueType = labelInValue
|
||||
? (item as LabelValueType).value
|
||||
: (item as RawValueType);
|
||||
|
||||
if (!optionValues.has(val)) {
|
||||
cloneOptions.push(
|
||||
labelInValue
|
||||
? {
|
||||
[optionLabelProp]: (item as LabelValueType).label,
|
||||
value: val,
|
||||
}
|
||||
: { value: val },
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return cloneOptions;
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
import warning, { noteOnce } from '../../vc-util/warning';
|
||||
import { SelectProps } from '..';
|
||||
import { convertChildrenToData } from './legacyUtil';
|
||||
import { OptionData, OptionGroupData } from '../interface';
|
||||
import { toArray } from './commonUtil';
|
||||
import { RawValueType, LabelValueType } from '../interface/generator';
|
||||
import { isValidElement } from '../../_util/props-util';
|
||||
import { VNode } from 'vue';
|
||||
|
||||
function warningProps(props: SelectProps) {
|
||||
const {
|
||||
mode,
|
||||
options,
|
||||
children,
|
||||
backfill,
|
||||
allowClear,
|
||||
placeholder,
|
||||
getInputElement,
|
||||
showSearch,
|
||||
onSearch,
|
||||
defaultOpen,
|
||||
autofocus,
|
||||
labelInValue,
|
||||
value,
|
||||
inputValue,
|
||||
optionLabelProp,
|
||||
} = props;
|
||||
|
||||
const multiple = mode === 'multiple' || mode === 'tags';
|
||||
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 } & OptionGroupData) => !opt.disabled),
|
||||
'Please avoid setting option to disabled in tags mode since user can always type text as tag.',
|
||||
);
|
||||
|
||||
// `combobox` & `tags` should option be `string` type
|
||||
if (mode === 'tags' || mode === 'combobox') {
|
||||
const hasNumberValue = mergedOptions.some(item => {
|
||||
if (item.options) {
|
||||
return item.options.some(
|
||||
(opt: OptionData) => typeof ('value' in opt ? opt.value : opt.key) === 'number',
|
||||
);
|
||||
}
|
||||
return typeof ('value' in item ? item.value : item.key) === 'number';
|
||||
});
|
||||
|
||||
warning(
|
||||
!hasNumberValue,
|
||||
'`value` of Option should not use number type when `mode` is `tags` or `combobox`.',
|
||||
);
|
||||
}
|
||||
|
||||
// `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 | LabelValueType>(value);
|
||||
warning(
|
||||
!labelInValue ||
|
||||
values.every(val => typeof val === 'object' && ('key' in val || 'value' in val)),
|
||||
'`value` should in shape of `{ value: string | number, label?: ReactNode }` 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: VNode & {
|
||||
children: { default?: () => 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;
|
||||
|
|
@ -401,7 +401,7 @@ export default defineComponent({
|
|||
prefixCls,
|
||||
destroyPopupOnHide,
|
||||
visible: sPopupVisible,
|
||||
point: alignPoint && point,
|
||||
point: alignPoint ? point : null,
|
||||
action,
|
||||
align,
|
||||
animation: popupAnimation,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
import addDOMEventListener from 'add-dom-event-listener';
|
||||
|
||||
export default function addEventListenerWrap(target, eventType, cb, option) {
|
||||
return addDOMEventListener(target, eventType, cb, option);
|
||||
if (target.addEventListener) {
|
||||
target.addEventListener(eventType, cb, option);
|
||||
}
|
||||
return {
|
||||
remove: () => {
|
||||
if (target.removeEventListener) {
|
||||
target.removeEventListener(eventType, cb);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
export default function contains(root, n) {
|
||||
let node = n;
|
||||
while (node) {
|
||||
if (node === root) {
|
||||
return true;
|
||||
}
|
||||
node = node.parentNode;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
export default function contains(root: Node | null | undefined, n?: Node) {
|
||||
if (!root) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return root.contains(n);
|
||||
}
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
/* eslint-disable no-console */
|
||||
let warned = {};
|
||||
let warned: Record<string, boolean> = {};
|
||||
|
||||
export function warning(valid, message) {
|
||||
export function warning(valid: boolean, message: string) {
|
||||
// Support uglify
|
||||
if (process.env.NODE_ENV !== 'production' && !valid && console !== undefined) {
|
||||
console.error(`Warning: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function note(valid, message) {
|
||||
export function note(valid: boolean, message: string) {
|
||||
// Support uglify
|
||||
if (process.env.NODE_ENV !== 'production' && !valid && console !== undefined) {
|
||||
console.warn(`Note: ${message}`);
|
||||
|
|
@ -19,18 +19,22 @@ export function resetWarned() {
|
|||
warned = {};
|
||||
}
|
||||
|
||||
export function call(method, valid, message) {
|
||||
export function call(
|
||||
method: (valid: boolean, message: string) => void,
|
||||
valid: boolean,
|
||||
message: string,
|
||||
) {
|
||||
if (!valid && !warned[message]) {
|
||||
method(false, message);
|
||||
warned[message] = true;
|
||||
}
|
||||
}
|
||||
|
||||
export function warningOnce(valid, message) {
|
||||
export function warningOnce(valid: boolean, message: string) {
|
||||
call(warning, valid, message);
|
||||
}
|
||||
|
||||
export function noteOnce(valid, message) {
|
||||
export function noteOnce(valid: boolean, message: string) {
|
||||
call(note, valid, message);
|
||||
}
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import classNames from '../_util/classNames';
|
||||
import ResizeObserver from '../vc-resize-observer';
|
||||
import { CSSProperties, FunctionalComponent } from 'vue';
|
||||
import { CSSProperties, FunctionalComponent, PropType } from 'vue';
|
||||
|
||||
interface FillerProps {
|
||||
prefixCls?: string;
|
||||
|
|
@ -59,5 +59,13 @@ const Filter: FunctionalComponent<FillerProps> = (
|
|||
|
||||
Filter.displayName = 'Filter';
|
||||
Filter.inheritAttrs = false;
|
||||
Filter.props = {
|
||||
prefixCls: String,
|
||||
/** Virtual filler height. Should be `count * itemMinHeight` */
|
||||
height: Number,
|
||||
/** Set offset of visible items. Should be the top of start item position */
|
||||
offset: Number,
|
||||
onInnerResize: Function as PropType<() => void>,
|
||||
};
|
||||
|
||||
export default Filter;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { cloneVNode, FunctionalComponent } from 'vue';
|
||||
import { cloneVNode, FunctionalComponent, PropType } from 'vue';
|
||||
|
||||
export interface ItemProps {
|
||||
setRef: (element: HTMLElement) => void;
|
||||
|
|
@ -13,5 +13,10 @@ const Item: FunctionalComponent<ItemProps> = ({ setRef }, { slots }) => {
|
|||
})
|
||||
: children;
|
||||
};
|
||||
|
||||
Item.props = {
|
||||
setRef: {
|
||||
type: Function as PropType<(element: HTMLElement) => void>,
|
||||
default: () => {},
|
||||
},
|
||||
};
|
||||
export default Item;
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ const List = defineComponent({
|
|||
virtual: PropTypes.bool,
|
||||
children: PropTypes.func,
|
||||
onScroll: PropTypes.func,
|
||||
onMousedown: PropTypes.func,
|
||||
onMouseenter: PropTypes.func,
|
||||
},
|
||||
setup(props) {
|
||||
// ================================= MISC =================================
|
||||
|
|
@ -255,7 +257,7 @@ const List = defineComponent({
|
|||
const removeEventListener = () => {
|
||||
if (componentRef.value) {
|
||||
componentRef.value.removeEventListener('wheel', onRawWheel);
|
||||
componentRef.value.removeEventListener('DOMMouseScroll', onFireFoxScroll);
|
||||
componentRef.value.removeEventListener('DOMMouseScroll' as any, onFireFoxScroll);
|
||||
componentRef.value.removeEventListener('MozMousePixelScroll', onMozMousePixelScroll);
|
||||
}
|
||||
};
|
||||
|
|
@ -264,7 +266,7 @@ const List = defineComponent({
|
|||
if (componentRef.value) {
|
||||
removeEventListener();
|
||||
componentRef.value.addEventListener('wheel', onRawWheel);
|
||||
componentRef.value.addEventListener('DOMMouseScroll', onFireFoxScroll);
|
||||
componentRef.value.addEventListener('DOMMouseScroll' as any, onFireFoxScroll);
|
||||
componentRef.value.addEventListener('MozMousePixelScroll', onMozMousePixelScroll);
|
||||
}
|
||||
});
|
||||
|
|
@ -331,7 +333,7 @@ const List = defineComponent({
|
|||
style,
|
||||
class: className,
|
||||
...restProps
|
||||
} = { ...this.$props, ...this.$attrs };
|
||||
} = { ...this.$props, ...this.$attrs } as any;
|
||||
const mergedClassName = classNames(prefixCls, className);
|
||||
const { scrollTop, mergedData } = this.state;
|
||||
const { scrollHeight, offset, start, end } = this.calRes;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export default function useScrollTo(
|
|||
) {
|
||||
let scroll: number | null = null;
|
||||
|
||||
return arg => {
|
||||
return (arg: any) => {
|
||||
raf.cancel(scroll!);
|
||||
const data = state.mergedData;
|
||||
const itemHeight = props.itemHeight;
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@
|
|||
"@types/raf": "^3.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.1.0",
|
||||
"@typescript-eslint/parser": "^4.1.0",
|
||||
"@vue/babel-plugin-jsx": "^1.0.0-rc.2",
|
||||
"@vue/babel-plugin-jsx": "^1.0.0-rc.3",
|
||||
"@vue/cli-plugin-eslint": "^4.0.0",
|
||||
"@vue/compiler-sfc": "^3.0.0",
|
||||
"@vue/eslint-config-prettier": "^6.0.0",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"ant-design-vue": ["components/index.tsx"],
|
||||
"ant-design-vue/es/*": ["components/*"]
|
||||
},
|
||||
"strictNullChecks": true,
|
||||
"strictNullChecks": false,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const babelConfig = {
|
|||
style: true,
|
||||
},
|
||||
],
|
||||
['@vue/babel-plugin-jsx'],
|
||||
['@vue/babel-plugin-jsx', { mergeProps: false }],
|
||||
'@babel/plugin-proposal-optional-chaining',
|
||||
'@babel/plugin-transform-object-assign',
|
||||
'@babel/plugin-proposal-object-rest-spread',
|
||||
|
|
|
|||
Loading…
Reference in New Issue