283 lines
8.1 KiB
Vue
283 lines
8.1 KiB
Vue
import TransBtn from '../TransBtn';
|
||
import {
|
||
LabelValueType,
|
||
RawValueType,
|
||
CustomTagProps,
|
||
DefaultValueType,
|
||
DisplayLabelValueType,
|
||
} from '../interface/generator';
|
||
import { RenderNode } from '../interface';
|
||
import { InnerSelectorProps } from '.';
|
||
import Input from './Input';
|
||
import { computed, defineComponent, onMounted, ref, VNodeChild, watch, Ref } from 'vue';
|
||
import classNames from '../../_util/classNames';
|
||
import pickAttrs from '../../_util/pickAttrs';
|
||
import PropTypes from '../../_util/vue-types';
|
||
import { VueNode } from 'ant-design-vue/es/_util/type';
|
||
import Overflow from '../../vc-overflow';
|
||
|
||
interface SelectorProps extends InnerSelectorProps {
|
||
// Icon
|
||
removeIcon?: RenderNode;
|
||
|
||
// Tags
|
||
maxTagCount?: number | 'responsive';
|
||
maxTagTextLength?: number;
|
||
maxTagPlaceholder?: VNodeChild;
|
||
tokenSeparators?: string[];
|
||
tagRender?: (props: CustomTagProps) => VNodeChild;
|
||
onToggleOpen: (open?: boolean) => void;
|
||
|
||
// Motion
|
||
choiceTransitionName?: string;
|
||
|
||
// Event
|
||
onSelect: (value: RawValueType, option: { selected: boolean }) => void;
|
||
}
|
||
|
||
const props = {
|
||
id: PropTypes.string,
|
||
prefixCls: PropTypes.string,
|
||
values: PropTypes.array,
|
||
open: PropTypes.looseBool,
|
||
searchValue: PropTypes.string,
|
||
inputRef: PropTypes.any,
|
||
placeholder: PropTypes.any,
|
||
disabled: PropTypes.looseBool,
|
||
mode: PropTypes.string,
|
||
showSearch: PropTypes.looseBool,
|
||
autofocus: PropTypes.looseBool,
|
||
autocomplete: PropTypes.string,
|
||
accessibilityIndex: PropTypes.number,
|
||
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||
|
||
removeIcon: PropTypes.VNodeChild,
|
||
choiceTransitionName: PropTypes.string,
|
||
|
||
maxTagCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||
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 onPreventMouseDown = (event: MouseEvent) => {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
};
|
||
|
||
const SelectSelector = defineComponent<SelectorProps>({
|
||
name: 'MultipleSelectSelector',
|
||
setup(props) {
|
||
const measureRef = ref();
|
||
const inputWidth = ref(0);
|
||
const focused = ref(false);
|
||
|
||
const selectionPrefixCls = computed(() => `${props.prefixCls}-selection`);
|
||
|
||
// ===================== Search ======================
|
||
const inputValue = computed(() =>
|
||
props.open || props.mode === 'tags' ? props.searchValue : '',
|
||
);
|
||
const inputEditable: Ref<boolean> = computed(
|
||
() =>
|
||
props.mode === 'tags' || ((props.showSearch && (props.open || focused.value)) as boolean),
|
||
);
|
||
|
||
// We measure width and set to the input immediately
|
||
onMounted(() => {
|
||
watch(
|
||
inputValue,
|
||
() => {
|
||
inputWidth.value = measureRef.value.scrollWidth;
|
||
},
|
||
{ flush: 'post', immediate: true },
|
||
);
|
||
});
|
||
|
||
// ===================== Render ======================
|
||
// >>> Render Selector Node. Includes Item & Rest
|
||
function defaultRenderSelector(
|
||
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>
|
||
);
|
||
}
|
||
|
||
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>
|
||
);
|
||
}
|
||
|
||
function renderItem({ disabled: itemDisabled, label, value }: DisplayLabelValueType) {
|
||
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.onSelect(value, { selected: false });
|
||
};
|
||
|
||
return typeof props.tagRender === 'function'
|
||
? customizeRenderSelector(value, displayLabel, itemDisabled, closable, onClose)
|
||
: defaultRenderSelector(displayLabel, itemDisabled, closable, onClose);
|
||
}
|
||
|
||
function renderRest(omittedValues: DisplayLabelValueType[]) {
|
||
const {
|
||
maxTagPlaceholder = (omittedValues: LabelValueType[]) => `+ ${omittedValues.length} ...`,
|
||
} = props;
|
||
const content =
|
||
typeof maxTagPlaceholder === 'function'
|
||
? maxTagPlaceholder(omittedValues)
|
||
: maxTagPlaceholder;
|
||
|
||
return defaultRenderSelector(content, false);
|
||
}
|
||
|
||
return () => {
|
||
const {
|
||
id,
|
||
prefixCls,
|
||
values,
|
||
open,
|
||
inputRef,
|
||
placeholder,
|
||
disabled,
|
||
autofocus,
|
||
autocomplete,
|
||
accessibilityIndex,
|
||
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}
|
||
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}
|
||
</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>
|
||
)}
|
||
</>
|
||
);
|
||
};
|
||
},
|
||
});
|
||
SelectSelector.inheritAttrs = false;
|
||
SelectSelector.props = props;
|
||
export default SelectSelector;
|