ant-design-vue/components/vc-select/Selector/index.tsx

284 lines
8.3 KiB
Vue
Raw Normal View History

2020-10-07 14:49:01 +00:00
/**
* 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';
2021-06-26 01:35:40 +00:00
import type { LabelValueType, RawValueType, CustomTagProps } from '../interface/generator';
import type { RenderNode, Mode } from '../interface';
2020-10-07 14:49:01 +00:00
import useLock from '../hooks/useLock';
2021-08-10 06:36:28 +00:00
import type { VNodeChild, PropType } from 'vue';
2021-06-26 01:35:40 +00:00
import { defineComponent } from 'vue';
import createRef from '../../_util/createRef';
2020-10-12 10:46:05 +00:00
import PropTypes from '../../_util/vue-types';
2021-06-26 01:35:40 +00:00
import type { VueNode } from '../../_util/type';
2020-10-07 14:49:01 +00:00
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;
2021-04-09 03:00:41 +00:00
inputElement: VueNode;
2020-10-07 14:49:01 +00:00
autofocus?: boolean;
accessibilityIndex: number;
refactor: Anchor、Alert、Avatar、Badge、BackTop、Col、Form、Layout、Menu、Space、Spin、Switch、Row、Result、Rate (#4171) * chore: remove resize-observer-polyfill * refactor: align * refactor(v3/avatar): refactor using composition api (#4052) * refactor(avatar): refactor using composition api * refactor: update props define * fix: avatar src scale not update * refactor: resizeObserver * refactor: divider * refactor: localeProvider * refactor(v3/back-top): use composition api (#4060) * refactor: backtop * refactor: empty * refactor: transButton * feat(v3/avatar): add avatar group (#4062) * feat(avatar): add avatar group * refactor: update * refactor: update Co-authored-by: tangjinzhou <415800467@qq.com> * refactor: avatar * refactor: avatar * style: rename useProvide * refactor: menu (#4110) * fix: menu * refactor: menu * refactor: remove rc-menu * fix: menu rtl error * style: lint * refactor(Anchor): use composition api (#4054) * refactor: anchor * refactor: anchor * refactor: anchor * feat: update * fix: icon class lose * refactor(v3/badge): use composition api (#4076) * refactor: badge * fix: badge inheritAttrs * refactor: grid * refactor: layout * fix: menu not close * refactor: space * refactor: result * refactor: affix * refactor: comment * refactor: form * feat: spin add rtl * feat: export spin type * refactor: pageHeader * refactor: page-header * refactor: skeleton * refactor: typography * refactor(v3/rate): use composition api * fix: add useRef hook * refactor: form * fix: menu not update * refactor: form * refactor: form * fix: slide animate not work * fix: menu mode error * fix: menu icon * refactor: rate * perf: remove rate * feat: add vc-overflow * refactor: menu * fix: remove flex check (#4165) * fix: dist locale file lose #3684 * release 2.2.0-beta.1 * dcos: update changelog * chore: update type * docs: update changelog Co-authored-by: John <John60676@qq.com> Co-authored-by: 言肆 <18x@loacg.com> Co-authored-by: zkwolf <chenhao5866@gmail.com>
2021-06-07 09:35:03 +00:00
tabindex?: number | string;
2020-10-07 14:49:01 +00:00
disabled?: boolean;
placeholder?: VNodeChild;
removeIcon?: RenderNode;
// Tags
2021-06-22 02:47:33 +00:00
maxTagCount?: number | 'responsive';
2020-10-07 14:49:01 +00:00
maxTagTextLength?: number;
2021-08-10 06:36:28 +00:00
maxTagPlaceholder?: VNodeChild | ((omittedValues: LabelValueType[]) => VNodeChild);
2020-10-10 05:57:37 +00:00
tagRender?: (props: CustomTagProps) => VNodeChild;
2020-10-07 14:49:01 +00:00
/** 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;
2021-08-21 08:25:55 +00:00
onInputKeyDown?: (e: KeyboardEvent) => void;
2020-10-07 14:49:01 +00:00
/**
* @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',
2021-08-10 06:36:28 +00:00
inheritAttrs: false,
props: {
id: PropTypes.string,
prefixCls: PropTypes.string,
showSearch: PropTypes.looseBool,
open: PropTypes.looseBool,
/** Display in the Selector value, it's not same as `value` prop */
values: PropTypes.array,
multiple: PropTypes.looseBool,
mode: PropTypes.string,
searchValue: PropTypes.string,
activeValue: PropTypes.string,
inputElement: PropTypes.any,
autofocus: PropTypes.looseBool,
accessibilityIndex: PropTypes.number,
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
disabled: PropTypes.looseBool,
placeholder: PropTypes.any,
removeIcon: PropTypes.any,
// Tags
maxTagCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
maxTagTextLength: PropTypes.number,
maxTagPlaceholder: PropTypes.any,
tagRender: PropTypes.func,
/** Check if `tokenSeparators` contains `\n` or `\r\n` */
tokenWithEnter: PropTypes.looseBool,
// Motion
choiceTransitionName: PropTypes.string,
onToggleOpen: { type: Function as PropType<(open?: boolean) => void> },
/** `onSearch` returns go next step boolean to check if need do toggle open */
onSearch: PropTypes.func,
onSearchSubmit: PropTypes.func,
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,
} as any,
2020-10-07 14:49:01 +00:00
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;
};
2021-06-22 02:47:33 +00:00
const onInputCompositionEnd = (e: InputEvent) => {
2020-10-07 14:49:01 +00:00
compositionStatus = false;
2021-06-22 02:47:33 +00:00
// Trigger search again to support `tokenSeparators` with typewriting
if (props.mode !== 'combobox') {
triggerOnSearch((e.target as HTMLInputElement).value);
}
2020-10-07 14:49:01 +00:00
};
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
2021-06-22 02:47:33 +00:00
const replacedText = pastedText
.replace(/[\r\n]+$/, '')
.replace(/\r\n/g, ' ')
.replace(/[\r\n]/g, ' ');
2020-10-07 14:49:01 +00:00
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,
2020-10-10 05:57:37 +00:00
} = this as any;
2020-10-07 14:49:01 +00:00
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>
);
},
});
export default Selector;