|
|
import TransBtn from '../TransBtn';
|
|
|
import type { InnerSelectorProps } from './interface';
|
|
|
import Input from './Input';
|
|
|
import type { Ref, PropType } from 'vue';
|
|
|
import { computed, defineComponent, onMounted, ref, watch } from 'vue';
|
|
|
import classNames from '../../_util/classNames';
|
|
|
import pickAttrs from '../../_util/pickAttrs';
|
|
|
import PropTypes from '../../_util/vue-types';
|
|
|
import type { VueNode } from '../../_util/type';
|
|
|
import Overflow from '../../vc-overflow';
|
|
|
import type { DisplayValueType, RenderNode, CustomTagProps, RawValueType } from '../BaseSelect';
|
|
|
import type { BaseOptionType } from '../Select';
|
|
|
import useInjectLegacySelectContext from '../../vc-tree-select/LegacyContext';
|
|
|
|
|
|
type SelectorProps = InnerSelectorProps & {
|
|
|
// Icon
|
|
|
removeIcon?: RenderNode;
|
|
|
|
|
|
// Tags
|
|
|
maxTagCount?: number | 'responsive';
|
|
|
maxTagTextLength?: number;
|
|
|
maxTagPlaceholder?: VueNode | ((omittedValues: DisplayValueType[]) => VueNode);
|
|
|
tokenSeparators?: string[];
|
|
|
tagRender?: (props: CustomTagProps) => VueNode;
|
|
|
onToggleOpen: any;
|
|
|
|
|
|
// Motion
|
|
|
choiceTransitionName?: string;
|
|
|
|
|
|
// Event
|
|
|
onRemove: (value: DisplayValueType) => void;
|
|
|
};
|
|
|
|
|
|
const props = {
|
|
|
id: String,
|
|
|
prefixCls: String,
|
|
|
values: PropTypes.array,
|
|
|
open: { type: Boolean, default: undefined },
|
|
|
searchValue: String,
|
|
|
inputRef: PropTypes.any,
|
|
|
placeholder: PropTypes.any,
|
|
|
disabled: { type: Boolean, default: undefined },
|
|
|
mode: String,
|
|
|
showSearch: { type: Boolean, default: undefined },
|
|
|
autofocus: { type: Boolean, default: undefined },
|
|
|
autocomplete: String,
|
|
|
activeDescendantId: String,
|
|
|
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
|
|
|
|
removeIcon: PropTypes.any,
|
|
|
choiceTransitionName: String,
|
|
|
|
|
|
maxTagCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
|
maxTagTextLength: Number,
|
|
|
maxTagPlaceholder: PropTypes.any.def(
|
|
|
() => (omittedValues: DisplayValueType[]) => `+ ${omittedValues.length} ...`,
|
|
|
),
|
|
|
tagRender: Function,
|
|
|
|
|
|
onToggleOpen: { type: Function as PropType<(open?: boolean) => void> },
|
|
|
onRemove: Function,
|
|
|
onInputChange: Function,
|
|
|
onInputPaste: Function,
|
|
|
onInputKeyDown: Function,
|
|
|
onInputMouseDown: Function,
|
|
|
onInputCompositionStart: Function,
|
|
|
onInputCompositionEnd: Function,
|
|
|
};
|
|
|
|
|
|
const onPreventMouseDown = (event: MouseEvent) => {
|
|
|
event.preventDefault();
|
|
|
event.stopPropagation();
|
|
|
};
|
|
|
|
|
|
const SelectSelector = defineComponent<SelectorProps>({
|
|
|
name: 'MultipleSelectSelector',
|
|
|
inheritAttrs: false,
|
|
|
props: props as any,
|
|
|
setup(props) {
|
|
|
const measureRef = ref();
|
|
|
const inputWidth = ref(0);
|
|
|
const focused = ref(false);
|
|
|
const legacyTreeSelectContext = useInjectLegacySelectContext();
|
|
|
const selectionPrefixCls = computed(() => `${props.prefixCls}-selection`);
|
|
|
|
|
|
// ===================== Search ======================
|
|
|
const inputValue = computed(() =>
|
|
|
props.open || props.mode === 'tags' ? props.searchValue : '',
|
|
|
);
|
|
|
const inputEditable: Ref<boolean> = computed(
|
|
|
() =>
|
|
|
props.mode === 'tags' || ((props.showSearch && (props.open || focused.value)) as boolean),
|
|
|
);
|
|
|
|
|
|
// We measure width and set to the input immediately
|
|
|
onMounted(() => {
|
|
|
watch(
|
|
|
inputValue,
|
|
|
() => {
|
|
|
inputWidth.value = measureRef.value.scrollWidth;
|
|
|
},
|
|
|
{ flush: 'post', immediate: true },
|
|
|
);
|
|
|
});
|
|
|
|
|
|
// ===================== Render ======================
|
|
|
// >>> Render Selector Node. Includes Item & Rest
|
|
|
function defaultRenderSelector(
|
|
|
title: VueNode,
|
|
|
content: VueNode,
|
|
|
itemDisabled: boolean,
|
|
|
closable?: boolean,
|
|
|
onClose?: (e: MouseEvent) => void,
|
|
|
) {
|
|
|
return (
|
|
|
<span
|
|
|
class={classNames(`${selectionPrefixCls.value}-item`, {
|
|
|
[`${selectionPrefixCls.value}-item-disabled`]: itemDisabled,
|
|
|
})}
|
|
|
title={
|
|
|
typeof title === 'string' || typeof title === 'number' ? title.toString() : undefined
|
|
|
}
|
|
|
>
|
|
|
<span class={`${selectionPrefixCls.value}-item-content`}>{content}</span>
|
|
|
{closable && (
|
|
|
<TransBtn
|
|
|
class={`${selectionPrefixCls.value}-item-remove`}
|
|
|
onMousedown={onPreventMouseDown}
|
|
|
onClick={onClose}
|
|
|
customizeIcon={props.removeIcon}
|
|
|
>
|
|
|
×
|
|
|
</TransBtn>
|
|
|
)}
|
|
|
</span>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
function customizeRenderSelector(
|
|
|
value: RawValueType,
|
|
|
content: VueNode,
|
|
|
itemDisabled: boolean,
|
|
|
closable: boolean,
|
|
|
onClose: (e: MouseEvent) => void,
|
|
|
option: BaseOptionType,
|
|
|
) {
|
|
|
const onMouseDown = (e: MouseEvent) => {
|
|
|
onPreventMouseDown(e);
|
|
|
props.onToggleOpen(!open);
|
|
|
};
|
|
|
let originData = option;
|
|
|
// For TreeSelect
|
|
|
if (legacyTreeSelectContext.keyEntities) {
|
|
|
originData = legacyTreeSelectContext.keyEntities[value]?.node || {};
|
|
|
}
|
|
|
return (
|
|
|
<span key={value} onMousedown={onMouseDown}>
|
|
|
{props.tagRender({
|
|
|
label: content,
|
|
|
value,
|
|
|
disabled: itemDisabled,
|
|
|
closable,
|
|
|
onClose,
|
|
|
option: originData,
|
|
|
})}
|
|
|
</span>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
function renderItem(valueItem: DisplayValueType) {
|
|
|
const { disabled: itemDisabled, label, value, option } = valueItem;
|
|
|
const closable = !props.disabled && !itemDisabled;
|
|
|
|
|
|
let displayLabel = label;
|
|
|
|
|
|
if (typeof props.maxTagTextLength === 'number') {
|
|
|
if (typeof label === 'string' || typeof label === 'number') {
|
|
|
const strLabel = String(displayLabel);
|
|
|
|
|
|
if (strLabel.length > props.maxTagTextLength) {
|
|
|
displayLabel = `${strLabel.slice(0, props.maxTagTextLength)}...`;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
const onClose = (event?: MouseEvent) => {
|
|
|
if (event) event.stopPropagation();
|
|
|
props.onRemove?.(valueItem);
|
|
|
};
|
|
|
|
|
|
return typeof props.tagRender === 'function'
|
|
|
? customizeRenderSelector(value, displayLabel, itemDisabled, closable, onClose, option)
|
|
|
: defaultRenderSelector(label, displayLabel, itemDisabled, closable, onClose);
|
|
|
}
|
|
|
|
|
|
function renderRest(omittedValues: DisplayValueType[]) {
|
|
|
const { maxTagPlaceholder = omittedValues => `+ ${omittedValues.length} ...` } = props;
|
|
|
const content =
|
|
|
typeof maxTagPlaceholder === 'function'
|
|
|
? maxTagPlaceholder(omittedValues)
|
|
|
: maxTagPlaceholder;
|
|
|
|
|
|
return defaultRenderSelector(content, content, false);
|
|
|
}
|
|
|
|
|
|
return () => {
|
|
|
const {
|
|
|
id,
|
|
|
prefixCls,
|
|
|
values,
|
|
|
open,
|
|
|
inputRef,
|
|
|
placeholder,
|
|
|
disabled,
|
|
|
autofocus,
|
|
|
autocomplete,
|
|
|
activeDescendantId,
|
|
|
tabindex,
|
|
|
onInputChange,
|
|
|
onInputPaste,
|
|
|
onInputKeyDown,
|
|
|
onInputMouseDown,
|
|
|
onInputCompositionStart,
|
|
|
onInputCompositionEnd,
|
|
|
} = props;
|
|
|
|
|
|
// >>> Input Node
|
|
|
const inputNode = (
|
|
|
<div
|
|
|
class={`${selectionPrefixCls.value}-search`}
|
|
|
style={{ width: inputWidth.value + 'px' }}
|
|
|
key="input"
|
|
|
>
|
|
|
<Input
|
|
|
inputRef={inputRef}
|
|
|
open={open}
|
|
|
prefixCls={prefixCls}
|
|
|
id={id}
|
|
|
inputElement={null}
|
|
|
disabled={disabled}
|
|
|
autofocus={autofocus}
|
|
|
autocomplete={autocomplete}
|
|
|
editable={inputEditable.value}
|
|
|
activeDescendantId={activeDescendantId}
|
|
|
value={inputValue.value}
|
|
|
onKeydown={onInputKeyDown}
|
|
|
onMousedown={onInputMouseDown}
|
|
|
onChange={onInputChange}
|
|
|
onPaste={onInputPaste}
|
|
|
onCompositionstart={onInputCompositionStart}
|
|
|
onCompositionend={onInputCompositionEnd}
|
|
|
tabindex={tabindex}
|
|
|
attrs={pickAttrs(props, true)}
|
|
|
onFocus={() => (focused.value = true)}
|
|
|
onBlur={() => (focused.value = false)}
|
|
|
/>
|
|
|
|
|
|
{/* Measure Node */}
|
|
|
<span ref={measureRef} class={`${selectionPrefixCls.value}-search-mirror`} aria-hidden>
|
|
|
{inputValue.value}
|
|
|
</span>
|
|
|
</div>
|
|
|
);
|
|
|
|
|
|
// >>> Selections
|
|
|
const selectionNode = (
|
|
|
<Overflow
|
|
|
prefixCls={`${selectionPrefixCls.value}-overflow`}
|
|
|
data={values}
|
|
|
renderItem={renderItem}
|
|
|
renderRest={renderRest}
|
|
|
suffix={inputNode}
|
|
|
itemKey="key"
|
|
|
maxCount={props.maxTagCount}
|
|
|
key="overflow"
|
|
|
/>
|
|
|
);
|
|
|
return (
|
|
|
<>
|
|
|
{selectionNode}
|
|
|
{!values.length && !inputValue.value && (
|
|
|
<span class={`${selectionPrefixCls.value}-placeholder`}>{placeholder}</span>
|
|
|
)}
|
|
|
</>
|
|
|
);
|
|
|
};
|
|
|
},
|
|
|
});
|
|
|
|
|
|
export default SelectSelector;
|