2020-10-07 14:49:01 +00:00
|
|
|
|
import TransBtn from '../TransBtn';
|
2021-06-26 01:35:40 +00:00
|
|
|
|
import type {
|
2021-06-22 02:47:33 +00:00
|
|
|
|
LabelValueType,
|
|
|
|
|
RawValueType,
|
|
|
|
|
CustomTagProps,
|
|
|
|
|
DefaultValueType,
|
|
|
|
|
DisplayLabelValueType,
|
|
|
|
|
} from '../interface/generator';
|
2021-06-26 01:35:40 +00:00
|
|
|
|
import type { RenderNode } from '../interface';
|
2021-08-10 06:36:28 +00:00
|
|
|
|
import type { InnerSelectorProps } from './interface';
|
2020-10-07 14:49:01 +00:00
|
|
|
|
import Input from './Input';
|
2021-08-10 06:36:28 +00:00
|
|
|
|
import type { VNodeChild, Ref, PropType } from 'vue';
|
2021-06-26 01:35:40 +00:00
|
|
|
|
import { computed, defineComponent, onMounted, ref, watch } from 'vue';
|
2020-10-07 14:49:01 +00:00
|
|
|
|
import classNames from '../../_util/classNames';
|
|
|
|
|
import pickAttrs from '../../_util/pickAttrs';
|
|
|
|
|
import PropTypes from '../../_util/vue-types';
|
2021-06-26 06:27:26 +00:00
|
|
|
|
import type { VueNode } from '../../_util/type';
|
2021-06-22 02:47:33 +00:00
|
|
|
|
import Overflow from '../../vc-overflow';
|
2020-10-07 14:49:01 +00:00
|
|
|
|
|
2021-08-10 06:36:28 +00:00
|
|
|
|
type SelectorProps = InnerSelectorProps & {
|
2020-10-07 14:49:01 +00:00
|
|
|
|
// Icon
|
|
|
|
|
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-07 14:49:01 +00:00
|
|
|
|
tokenSeparators?: string[];
|
|
|
|
|
tagRender?: (props: CustomTagProps) => VNodeChild;
|
2021-08-10 06:36:28 +00:00
|
|
|
|
onToggleOpen: any;
|
2020-10-07 14:49:01 +00:00
|
|
|
|
|
|
|
|
|
// Motion
|
|
|
|
|
choiceTransitionName?: string;
|
|
|
|
|
|
|
|
|
|
// Event
|
|
|
|
|
onSelect: (value: RawValueType, option: { selected: boolean }) => void;
|
2021-08-10 06:36:28 +00:00
|
|
|
|
};
|
2020-10-07 14:49:01 +00:00
|
|
|
|
|
|
|
|
|
const props = {
|
|
|
|
|
id: PropTypes.string,
|
|
|
|
|
prefixCls: PropTypes.string,
|
|
|
|
|
values: PropTypes.array,
|
2020-10-11 13:31:57 +00:00
|
|
|
|
open: PropTypes.looseBool,
|
2020-10-07 14:49:01 +00:00
|
|
|
|
searchValue: PropTypes.string,
|
|
|
|
|
inputRef: PropTypes.any,
|
|
|
|
|
placeholder: PropTypes.any,
|
2020-10-11 13:31:57 +00:00
|
|
|
|
disabled: PropTypes.looseBool,
|
2020-10-07 14:49:01 +00:00
|
|
|
|
mode: PropTypes.string,
|
2020-10-11 13:31:57 +00:00
|
|
|
|
showSearch: PropTypes.looseBool,
|
|
|
|
|
autofocus: PropTypes.looseBool,
|
2020-10-07 14:49:01 +00:00
|
|
|
|
autocomplete: PropTypes.string,
|
|
|
|
|
accessibilityIndex: PropTypes.number,
|
2021-06-07 09:35:03 +00:00
|
|
|
|
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
2020-10-07 14:49:01 +00:00
|
|
|
|
|
2020-10-17 14:19:52 +00:00
|
|
|
|
removeIcon: PropTypes.VNodeChild,
|
2020-10-07 14:49:01 +00:00
|
|
|
|
choiceTransitionName: PropTypes.string,
|
|
|
|
|
|
2021-06-22 02:47:33 +00:00
|
|
|
|
maxTagCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
2020-10-07 14:49:01 +00:00
|
|
|
|
maxTagTextLength: PropTypes.number,
|
2021-06-23 15:08:16 +00:00
|
|
|
|
maxTagPlaceholder: PropTypes.any.def(
|
|
|
|
|
() => (omittedValues: LabelValueType[]) => `+ ${omittedValues.length} ...`,
|
2020-10-07 14:49:01 +00:00
|
|
|
|
),
|
|
|
|
|
tagRender: PropTypes.func,
|
|
|
|
|
|
2021-08-10 06:36:28 +00:00
|
|
|
|
onToggleOpen: { type: Function as PropType<(open?: boolean) => void> },
|
2020-10-07 14:49:01 +00:00
|
|
|
|
onSelect: PropTypes.func,
|
|
|
|
|
onInputChange: PropTypes.func,
|
|
|
|
|
onInputPaste: PropTypes.func,
|
|
|
|
|
onInputKeyDown: PropTypes.func,
|
|
|
|
|
onInputMouseDown: PropTypes.func,
|
|
|
|
|
onInputCompositionStart: PropTypes.func,
|
|
|
|
|
onInputCompositionEnd: PropTypes.func,
|
|
|
|
|
};
|
|
|
|
|
|
2021-06-22 02:47:33 +00:00
|
|
|
|
const onPreventMouseDown = (event: MouseEvent) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
};
|
|
|
|
|
|
2020-10-07 14:49:01 +00:00
|
|
|
|
const SelectSelector = defineComponent<SelectorProps>({
|
2020-10-26 10:31:50 +00:00
|
|
|
|
name: 'MultipleSelectSelector',
|
2021-08-10 06:36:28 +00:00
|
|
|
|
inheritAttrs: false,
|
|
|
|
|
props: props as any,
|
2020-10-07 14:49:01 +00:00
|
|
|
|
setup(props) {
|
|
|
|
|
const measureRef = ref();
|
|
|
|
|
const inputWidth = ref(0);
|
2021-06-22 02:47:33 +00:00
|
|
|
|
const focused = ref(false);
|
2020-10-07 14:49:01 +00:00
|
|
|
|
|
2021-06-22 02:47:33 +00:00
|
|
|
|
const selectionPrefixCls = computed(() => `${props.prefixCls}-selection`);
|
2020-10-07 14:49:01 +00:00
|
|
|
|
|
|
|
|
|
// ===================== Search ======================
|
|
|
|
|
const inputValue = computed(() =>
|
|
|
|
|
props.open || props.mode === 'tags' ? props.searchValue : '',
|
|
|
|
|
);
|
|
|
|
|
const inputEditable: Ref<boolean> = computed(
|
2021-06-22 02:47:33 +00:00
|
|
|
|
() =>
|
|
|
|
|
props.mode === 'tags' || ((props.showSearch && (props.open || focused.value)) as boolean),
|
2020-10-07 14:49:01 +00:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// We measure width and set to the input immediately
|
2020-10-08 14:51:09 +00:00
|
|
|
|
onMounted(() => {
|
|
|
|
|
watch(
|
|
|
|
|
inputValue,
|
|
|
|
|
() => {
|
|
|
|
|
inputWidth.value = measureRef.value.scrollWidth;
|
|
|
|
|
},
|
2021-06-22 02:47:33 +00:00
|
|
|
|
{ flush: 'post', immediate: true },
|
2020-10-08 14:51:09 +00:00
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2021-06-22 02:47:33 +00:00
|
|
|
|
// ===================== Render ======================
|
|
|
|
|
// >>> Render Selector Node. Includes Item & Rest
|
|
|
|
|
function defaultRenderSelector(
|
|
|
|
|
content: VueNode,
|
|
|
|
|
itemDisabled: boolean,
|
|
|
|
|
closable?: boolean,
|
|
|
|
|
onClose?: (e: MouseEvent) => void,
|
|
|
|
|
) {
|
|
|
|
|
return (
|
|
|
|
|
<span
|
|
|
|
|
class={classNames(`${selectionPrefixCls.value}-item`, {
|
|
|
|
|
[`${selectionPrefixCls.value}-item-disabled`]: itemDisabled,
|
|
|
|
|
})}
|
|
|
|
|
>
|
|
|
|
|
<span class={`${selectionPrefixCls.value}-item-content`}>{content}</span>
|
|
|
|
|
{closable && (
|
|
|
|
|
<TransBtn
|
|
|
|
|
class={`${selectionPrefixCls.value}-item-remove`}
|
|
|
|
|
onMousedown={onPreventMouseDown}
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
customizeIcon={props.removeIcon}
|
|
|
|
|
>
|
|
|
|
|
×
|
|
|
|
|
</TransBtn>
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
}
|
2020-10-07 14:49:01 +00:00
|
|
|
|
|
2021-06-22 02:47:33 +00:00
|
|
|
|
function customizeRenderSelector(
|
|
|
|
|
value: DefaultValueType,
|
|
|
|
|
content: VueNode,
|
|
|
|
|
itemDisabled: boolean,
|
|
|
|
|
closable: boolean,
|
|
|
|
|
onClose: (e: MouseEvent) => void,
|
|
|
|
|
) {
|
|
|
|
|
const onMouseDown = (e: MouseEvent) => {
|
|
|
|
|
onPreventMouseDown(e);
|
|
|
|
|
props.onToggleOpen(!open);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<span onMousedown={onMouseDown}>
|
|
|
|
|
{props.tagRender({
|
|
|
|
|
label: content,
|
|
|
|
|
value,
|
|
|
|
|
disabled: itemDisabled,
|
|
|
|
|
closable,
|
|
|
|
|
onClose,
|
|
|
|
|
})}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
}
|
2020-10-07 14:49:01 +00:00
|
|
|
|
|
2021-06-22 02:47:33 +00:00
|
|
|
|
function renderItem({ disabled: itemDisabled, label, value }: DisplayLabelValueType) {
|
|
|
|
|
const closable = !props.disabled && !itemDisabled;
|
2020-10-07 14:49:01 +00:00
|
|
|
|
|
2021-06-22 02:47:33 +00:00
|
|
|
|
let displayLabel = label;
|
2020-10-07 14:49:01 +00:00
|
|
|
|
|
2021-06-22 02:47:33 +00:00
|
|
|
|
if (typeof props.maxTagTextLength === 'number') {
|
|
|
|
|
if (typeof label === 'string' || typeof label === 'number') {
|
|
|
|
|
const strLabel = String(displayLabel);
|
2020-10-07 14:49:01 +00:00
|
|
|
|
|
2021-06-22 02:47:33 +00:00
|
|
|
|
if (strLabel.length > props.maxTagTextLength) {
|
|
|
|
|
displayLabel = `${strLabel.slice(0, props.maxTagTextLength)}...`;
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-10-07 14:49:01 +00:00
|
|
|
|
}
|
2021-06-22 02:47:33 +00:00
|
|
|
|
const onClose = (event?: MouseEvent) => {
|
|
|
|
|
if (event) event.stopPropagation();
|
|
|
|
|
props.onSelect(value, { selected: false });
|
|
|
|
|
};
|
2020-10-07 14:49:01 +00:00
|
|
|
|
|
2021-06-22 02:47:33 +00:00
|
|
|
|
return typeof props.tagRender === 'function'
|
|
|
|
|
? customizeRenderSelector(value, displayLabel, itemDisabled, closable, onClose)
|
|
|
|
|
: defaultRenderSelector(displayLabel, itemDisabled, closable, onClose);
|
|
|
|
|
}
|
2020-10-07 14:49:01 +00:00
|
|
|
|
|
2021-06-22 02:47:33 +00:00
|
|
|
|
function renderRest(omittedValues: DisplayLabelValueType[]) {
|
|
|
|
|
const {
|
|
|
|
|
maxTagPlaceholder = (omittedValues: LabelValueType[]) => `+ ${omittedValues.length} ...`,
|
|
|
|
|
} = props;
|
|
|
|
|
const content =
|
|
|
|
|
typeof maxTagPlaceholder === 'function'
|
|
|
|
|
? maxTagPlaceholder(omittedValues)
|
|
|
|
|
: maxTagPlaceholder;
|
|
|
|
|
|
|
|
|
|
return defaultRenderSelector(content, false);
|
|
|
|
|
}
|
2020-10-07 14:49:01 +00:00
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
const {
|
|
|
|
|
id,
|
|
|
|
|
prefixCls,
|
|
|
|
|
values,
|
|
|
|
|
open,
|
|
|
|
|
inputRef,
|
|
|
|
|
placeholder,
|
|
|
|
|
disabled,
|
|
|
|
|
autofocus,
|
|
|
|
|
autocomplete,
|
|
|
|
|
accessibilityIndex,
|
|
|
|
|
tabindex,
|
|
|
|
|
onInputChange,
|
|
|
|
|
onInputPaste,
|
|
|
|
|
onInputKeyDown,
|
|
|
|
|
onInputMouseDown,
|
|
|
|
|
onInputCompositionStart,
|
|
|
|
|
onInputCompositionEnd,
|
|
|
|
|
} = props;
|
|
|
|
|
|
2021-06-22 02:47:33 +00:00
|
|
|
|
// >>> Input Node
|
|
|
|
|
const inputNode = (
|
|
|
|
|
<div
|
|
|
|
|
class={`${selectionPrefixCls.value}-search`}
|
|
|
|
|
style={{ width: inputWidth.value + 'px' }}
|
|
|
|
|
key="input"
|
|
|
|
|
>
|
|
|
|
|
<Input
|
|
|
|
|
inputRef={inputRef}
|
|
|
|
|
open={open}
|
|
|
|
|
prefixCls={prefixCls}
|
|
|
|
|
id={id}
|
|
|
|
|
inputElement={null}
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
autofocus={autofocus}
|
|
|
|
|
autocomplete={autocomplete}
|
|
|
|
|
editable={inputEditable.value}
|
|
|
|
|
accessibilityIndex={accessibilityIndex}
|
|
|
|
|
value={inputValue.value}
|
|
|
|
|
onKeydown={onInputKeyDown}
|
|
|
|
|
onMousedown={onInputMouseDown}
|
|
|
|
|
onChange={onInputChange}
|
|
|
|
|
onPaste={onInputPaste}
|
|
|
|
|
onCompositionstart={onInputCompositionStart}
|
|
|
|
|
onCompositionend={onInputCompositionEnd}
|
|
|
|
|
tabindex={tabindex}
|
|
|
|
|
attrs={pickAttrs(props, true)}
|
|
|
|
|
onFocus={() => (focused.value = true)}
|
|
|
|
|
onBlur={() => (focused.value = false)}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* Measure Node */}
|
|
|
|
|
<span ref={measureRef} class={`${selectionPrefixCls.value}-search-mirror`} aria-hidden>
|
|
|
|
|
{inputValue.value}
|
2020-10-07 14:49:01 +00:00
|
|
|
|
</span>
|
2021-06-22 02:47:33 +00:00
|
|
|
|
</div>
|
|
|
|
|
);
|
2020-10-07 14:49:01 +00:00
|
|
|
|
|
2021-06-22 02:47:33 +00:00
|
|
|
|
// >>> Selections
|
|
|
|
|
const selectionNode = (
|
|
|
|
|
<Overflow
|
|
|
|
|
prefixCls={`${selectionPrefixCls.value}-overflow`}
|
|
|
|
|
data={values}
|
|
|
|
|
renderItem={renderItem}
|
|
|
|
|
renderRest={renderRest}
|
|
|
|
|
suffix={inputNode}
|
|
|
|
|
itemKey="key"
|
|
|
|
|
maxCount={props.maxTagCount}
|
|
|
|
|
key="overflow"
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{selectionNode}
|
2020-10-07 14:49:01 +00:00
|
|
|
|
{!values.length && !inputValue.value && (
|
2021-06-22 02:47:33 +00:00
|
|
|
|
<span class={`${selectionPrefixCls.value}-placeholder`}>{placeholder}</span>
|
2020-10-07 14:49:01 +00:00
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
});
|
2021-08-10 06:36:28 +00:00
|
|
|
|
|
2020-10-07 14:49:01 +00:00
|
|
|
|
export default SelectSelector;
|