refactor: tree-select

pull/4577/head
tangjinzhou 2021-08-23 10:54:55 +08:00
parent 8e38ed883c
commit 1581943eb0
15 changed files with 389 additions and 465 deletions

View File

@ -2,13 +2,12 @@ import type { App, PropType, Plugin, ExtractPropTypes } from 'vue';
import { computed, defineComponent, ref } from 'vue'; import { computed, defineComponent, ref } from 'vue';
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import { selectProps as vcSelectProps } from '../vc-select'; import { selectProps as vcSelectProps } from '../vc-select';
import RcSelect, { Option, OptGroup, selectBaseProps } from '../vc-select'; import RcSelect, { Option, OptGroup } from '../vc-select';
import type { OptionProps as OptionPropsType } from '../vc-select/Option'; import type { OptionProps as OptionPropsType } from '../vc-select/Option';
import getIcons from './utils/iconUtil'; import getIcons from './utils/iconUtil';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import { tuple } from '../_util/type'; import { tuple } from '../_util/type';
import useConfigInject from '../_util/hooks/useConfigInject'; import useConfigInject from '../_util/hooks/useConfigInject';
import type { SizeType } from '../config-provider';
import omit from '../_util/omit'; import omit from '../_util/omit';
type RawValue = string | number; type RawValue = string | number;
@ -62,7 +61,7 @@ const Select = defineComponent({
'option', 'option',
], ],
setup(props, { attrs, emit, slots, expose }) { setup(props, { attrs, emit, slots, expose }) {
const selectRef = ref(null); const selectRef = ref();
const focus = () => { const focus = () => {
if (selectRef.value) { if (selectRef.value) {

View File

@ -22,6 +22,7 @@ import getIcons from '../select/utils/iconUtil';
import renderSwitcherIcon from '../tree/utils/iconUtil'; import renderSwitcherIcon from '../tree/utils/iconUtil';
import type { AntTreeNodeProps } from '../tree/Tree'; import type { AntTreeNodeProps } from '../tree/Tree';
import { warning } from '../vc-util/warning'; import { warning } from '../vc-util/warning';
import { flattenChildren } from '../_util/props-util';
const getTransitionName = (rootPrefixCls: string, motion: string, transitionName?: string) => { const getTransitionName = (rootPrefixCls: string, motion: string, transitionName?: string) => {
if (transitionName !== undefined) { if (transitionName !== undefined) {
@ -80,7 +81,7 @@ const TreeSelect = defineComponent({
setup(props, { attrs, slots, expose, emit }) { setup(props, { attrs, slots, expose, emit }) {
warning( warning(
!(props.treeData === undefined && slots.default), !(props.treeData === undefined && slots.default),
'`children` of Tree is deprecated. Please use `treeData` instead.', '`children` of TreeSelect is deprecated. Please use `treeData` instead.',
); );
watchEffect(() => { watchEffect(() => {
devWarning( devWarning(
@ -194,6 +195,10 @@ const TreeSelect = defineComponent({
attrs.class, attrs.class,
); );
const rootPrefixCls = configProvider.getPrefixCls(); const rootPrefixCls = configProvider.getPrefixCls();
const otherProps: any = {};
if (props.treeData === undefined && slots.default) {
otherProps.children = flattenChildren(slots.default());
}
return ( return (
<VcTreeSelect <VcTreeSelect
{...attrs} {...attrs}
@ -227,7 +232,7 @@ const TreeSelect = defineComponent({
...slots, ...slots,
treeCheckable: () => <span class={`${prefixCls.value}-tree-checkbox-inner`} />, treeCheckable: () => <span class={`${prefixCls.value}-tree-checkbox-inner`} />,
}} }}
children={slots.default?.()} {...otherProps}
/> />
); );
}; };

View File

@ -42,7 +42,8 @@ import {
flattenOptions, flattenOptions,
fillOptionsWithMissingValue, fillOptionsWithMissingValue,
} from './utils/valueUtil'; } from './utils/valueUtil';
import { selectBaseProps, SelectProps } from './generate'; import type { SelectProps } from './generate';
import { selectBaseProps } from './generate';
import generateSelector from './generate'; import generateSelector from './generate';
import type { DefaultValueType } from './interface/generator'; import type { DefaultValueType } from './interface/generator';
import warningProps from './utils/warningPropsUtil'; import warningProps from './utils/warningPropsUtil';
@ -79,7 +80,7 @@ const Select = defineComponent({
OptGroup, OptGroup,
props: RefSelect.props, props: RefSelect.props,
setup(props, { attrs, expose, slots }) { setup(props, { attrs, expose, slots }) {
const selectRef = ref(null); const selectRef = ref();
expose({ expose({
focus: () => { focus: () => {
selectRef.value?.focus(); selectRef.value?.focus();

View File

@ -1,7 +1,8 @@
import pickAttrs from '../../_util/pickAttrs'; import pickAttrs from '../../_util/pickAttrs';
import Input from './Input'; import Input from './Input';
import type { InnerSelectorProps } from './interface'; import type { InnerSelectorProps } from './interface';
import { Fragment, Suspense, VNodeChild } from 'vue'; import type { VNodeChild } from 'vue';
import { Fragment } from 'vue';
import { computed, defineComponent, ref, watch } from 'vue'; import { computed, defineComponent, ref, watch } from 'vue';
import PropTypes from '../../_util/vue-types'; import PropTypes from '../../_util/vue-types';
import { useInjectTreeSelectContext } from 'ant-design-vue/es/vc-tree-select/Context'; import { useInjectTreeSelectContext } from 'ant-design-vue/es/vc-tree-select/Context';

View File

@ -52,7 +52,7 @@ export interface SelectorProps {
// Motion // Motion
choiceTransitionName?: string; choiceTransitionName?: string;
onToggleOpen: (open?: boolean) => void; onToggleOpen: (open?: boolean) => void | any;
/** `onSearch` returns go next step boolean to check if need do toggle open */ /** `onSearch` returns go next step boolean to check if need do toggle open */
onSearch: (searchText: string, fromTyping: boolean, isCompositing: boolean) => boolean; onSearch: (searchText: string, fromTyping: boolean, isCompositing: boolean) => boolean;
onSearchSubmit: (searchText: string) => void; onSearchSubmit: (searchText: string) => void;

View File

@ -37,6 +37,7 @@ import useSelectTriggerControl from './hooks/useSelectTriggerControl';
import useCacheDisplayValue from './hooks/useCacheDisplayValue'; import useCacheDisplayValue from './hooks/useCacheDisplayValue';
import useCacheOptions from './hooks/useCacheOptions'; import useCacheOptions from './hooks/useCacheOptions';
import type { CSSProperties, DefineComponent, PropType, VNode, VNodeChild } from 'vue'; import type { CSSProperties, DefineComponent, PropType, VNode, VNodeChild } from 'vue';
import { getCurrentInstance } from 'vue';
import { import {
computed, computed,
defineComponent, defineComponent,
@ -275,7 +276,7 @@ export default function generateSelector<
slots: ['option'], slots: ['option'],
inheritAttrs: false, inheritAttrs: false,
props: selectBaseProps<OptionType, DefaultValueType>(), props: selectBaseProps<OptionType, DefaultValueType>(),
setup(props, { expose }) { setup(props, { expose, attrs, slots }) {
const useInternalProps = computed( const useInternalProps = computed(
() => props.internalProps && props.internalProps.mark === INTERNAL_PROPS_MARK, () => props.internalProps && props.internalProps.mark === INTERNAL_PROPS_MARK,
); );
@ -284,10 +285,10 @@ export default function generateSelector<
'Select', 'Select',
'optionFilterProp not support children, please use label instead', 'optionFilterProp not support children, please use label instead',
); );
const containerRef = ref(null); const containerRef = ref();
const triggerRef = ref(null); const triggerRef = ref();
const selectorRef = ref(null); const selectorRef = ref();
const listRef = ref(null); const listRef = ref();
const tokenWithEnter = computed(() => const tokenWithEnter = computed(() =>
(props.tokenSeparators || []).some(tokenSeparator => (props.tokenSeparators || []).some(tokenSeparator =>
['\n', '\r\n'].includes(tokenSeparator), ['\n', '\r\n'].includes(tokenSeparator),
@ -353,7 +354,7 @@ export default function generateSelector<
// ============================= Option ============================= // ============================= Option =============================
// Set by option list active, it will merge into search input when mode is `combobox` // Set by option list active, it will merge into search input when mode is `combobox`
const activeValue = ref(null); const activeValue = ref();
const setActiveValue = (val: string) => { const setActiveValue = (val: string) => {
activeValue.value = val; activeValue.value = val;
}; };
@ -925,7 +926,7 @@ export default function generateSelector<
}; };
// ============================= Popup ============================== // ============================= Popup ==============================
const containerWidth = ref(null); const containerWidth = ref<number>(null);
onMounted(() => { onMounted(() => {
watch( watch(
triggerOpen, triggerOpen,
@ -952,342 +953,270 @@ export default function generateSelector<
blur, blur,
scrollTo: (...args: any[]) => listRef.value?.scrollTo(...args), scrollTo: (...args: any[]) => listRef.value?.scrollTo(...args),
}); });
return { const instance = getCurrentInstance();
tokenWithEnter, const onPopupMouseEnter = () => {
mockFocused, // We need force update here since popup dom is render async
mergedId, instance.update();
containerWidth,
onActiveValue,
accessibilityIndex,
mergedDefaultActiveFirstOption,
onInternalMouseDown,
onContainerFocus,
onContainerBlur,
onInternalKeyDown,
isMultiple,
mergedOpen,
displayOptions,
displayFlattenOptions,
rawValues,
onInternalOptionSelect,
onToggleOpen,
mergedSearchValue,
useInternalProps,
triggerChange,
triggerSearch,
mergedRawValue,
mergedShowSearch,
onInternalKeyUp,
triggerOpen,
mergedOptions,
onInternalSelectionSelect,
selectorDomRef,
displayValues,
activeValue,
onSearchSubmit,
containerRef,
listRef,
triggerRef,
selectorRef,
}; };
}, return () => {
methods: { const {
// We need force update here since popup dom is render async prefixCls = defaultPrefixCls,
onPopupMouseEnter() { id,
(this as any).$forceUpdate();
},
},
render() {
const {
tokenWithEnter,
mockFocused,
mergedId,
containerWidth,
onActiveValue,
accessibilityIndex,
mergedDefaultActiveFirstOption,
onInternalMouseDown,
onInternalKeyDown,
isMultiple,
mergedOpen,
displayOptions,
displayFlattenOptions,
rawValues,
onInternalOptionSelect,
onToggleOpen,
mergedSearchValue,
onPopupMouseEnter,
useInternalProps,
triggerChange,
triggerSearch,
mergedRawValue,
mergedShowSearch,
onInternalKeyUp,
triggerOpen,
mergedOptions,
onInternalSelectionSelect,
selectorDomRef,
displayValues,
activeValue,
onSearchSubmit,
$slots: slots,
} = this as any;
const {
prefixCls = defaultPrefixCls,
id,
open, open,
defaultOpen, defaultOpen,
options, options,
children, children,
mode, mode,
value, value,
defaultValue, defaultValue,
labelInValue, labelInValue,
// Search related // Search related
showSearch, showSearch,
inputValue, inputValue,
searchValue, searchValue,
filterOption, filterOption,
optionFilterProp, optionFilterProp,
autoClearSearchValue, autoClearSearchValue,
onSearch, onSearch,
// Icons // Icons
allowClear, allowClear,
clearIcon, clearIcon,
showArrow, showArrow,
inputIcon, inputIcon,
menuItemSelectedIcon, menuItemSelectedIcon,
// Others // Others
disabled, disabled,
loading, loading,
defaultActiveFirstOption, defaultActiveFirstOption,
notFoundContent = 'Not Found', notFoundContent = 'Not Found',
optionLabelProp, optionLabelProp,
backfill, backfill,
getInputElement, getInputElement,
getPopupContainer, getPopupContainer,
// Dropdown // Dropdown
listHeight = 200, listHeight = 200,
listItemHeight = 20, listItemHeight = 20,
animation, animation,
transitionName, transitionName,
virtual, virtual,
dropdownStyle, dropdownStyle,
dropdownClassName, dropdownClassName,
dropdownMatchSelectWidth, dropdownMatchSelectWidth,
dropdownRender, dropdownRender,
dropdownAlign, dropdownAlign,
showAction, showAction,
direction, direction,
// Tags // Tags
tokenSeparators, tokenSeparators,
tagRender, tagRender,
// Events // Events
onPopupScroll, onPopupScroll,
onDropdownVisibleChange, onDropdownVisibleChange,
onFocus, onFocus,
onBlur, onBlur,
onKeyup, onKeyup,
onKeydown, onKeydown,
onMousedown, onMousedown,
onChange, onChange,
onSelect, onSelect,
onDeselect, onDeselect,
onClear, onClear,
internalProps = {}, internalProps = {},
...restProps ...restProps
} = this.$props; //as SelectProps<OptionType[], ValueType>; } = props; //as SelectProps<OptionType[], ValueType>;
// ============================= Input ==============================
// Only works in `combobox`
const customizeInputElement: VNodeChild | JSX.Element =
(mode === 'combobox' && getInputElement && getInputElement()) || null;
// ============================= Input ============================== const domProps = omitDOMProps ? omitDOMProps(restProps) : restProps;
// Only works in `combobox` DEFAULT_OMIT_PROPS.forEach(prop => {
const customizeInputElement: VNodeChild | JSX.Element = delete domProps[prop];
(mode === 'combobox' && getInputElement && getInputElement()) || null; });
const popupNode = (
const domProps = omitDOMProps ? omitDOMProps(restProps) : restProps; <OptionList
DEFAULT_OMIT_PROPS.forEach(prop => { ref={listRef}
delete domProps[prop]; prefixCls={prefixCls}
}); id={mergedId.value}
const popupNode = ( open={mergedOpen.value}
<OptionList childrenAsData={!options}
ref="listRef" options={displayOptions.value}
prefixCls={prefixCls} flattenOptions={displayFlattenOptions.value}
id={mergedId} multiple={isMultiple.value}
open={mergedOpen} values={rawValues.value}
childrenAsData={!options} height={listHeight}
options={displayOptions} itemHeight={listItemHeight}
flattenOptions={displayFlattenOptions} onSelect={onInternalOptionSelect}
multiple={isMultiple} onToggleOpen={onToggleOpen}
values={rawValues} onActiveValue={onActiveValue}
height={listHeight} defaultActiveFirstOption={mergedDefaultActiveFirstOption.value}
itemHeight={listItemHeight} notFoundContent={notFoundContent}
onSelect={onInternalOptionSelect} onScroll={onPopupScroll}
onToggleOpen={onToggleOpen} searchValue={mergedSearchValue.value}
onActiveValue={onActiveValue} menuItemSelectedIcon={menuItemSelectedIcon}
defaultActiveFirstOption={mergedDefaultActiveFirstOption} virtual={virtual !== false && dropdownMatchSelectWidth !== false}
notFoundContent={notFoundContent} onMouseenter={onPopupMouseEnter}
onScroll={onPopupScroll} v-slots={{ ...slots, option: slots.option }}
searchValue={mergedSearchValue}
menuItemSelectedIcon={menuItemSelectedIcon}
virtual={virtual !== false && dropdownMatchSelectWidth !== false}
onMouseenter={onPopupMouseEnter}
v-slots={{ ...slots, option: slots.option }}
/>
);
// ============================= Clear ==============================
let clearNode: VNode | JSX.Element;
const onClearMouseDown = () => {
// Trigger internal `onClear` event
if (useInternalProps && internalProps.onClear) {
internalProps.onClear();
}
if (onClear) {
onClear();
}
triggerChange([]);
triggerSearch('', false, false);
};
if (!disabled && allowClear && (mergedRawValue.length || mergedSearchValue)) {
clearNode = (
<TransBtn
class={`${prefixCls}-clear`}
onMousedown={onClearMouseDown}
customizeIcon={clearIcon}
>
×
</TransBtn>
);
}
// ============================= Arrow ==============================
const mergedShowArrow =
showArrow !== undefined ? showArrow : loading || (!isMultiple && mode !== 'combobox');
let arrowNode: VNode | JSX.Element;
if (mergedShowArrow) {
arrowNode = (
<TransBtn
class={classNames(`${prefixCls}-arrow`, {
[`${prefixCls}-arrow-loading`]: loading,
})}
customizeIcon={inputIcon}
customizeIconProps={{
loading,
searchValue: mergedSearchValue,
open: mergedOpen,
focused: mockFocused,
showSearch: mergedShowSearch,
}}
/> />
); );
}
// ============================ Warning ============================= // ============================= Clear ==============================
if (process.env.NODE_ENV !== 'production' && warningProps) { let clearNode: VNode | JSX.Element;
warningProps(this.$props); const onClearMouseDown = () => {
} // Trigger internal `onClear` event
if (useInternalProps.value && internalProps.onClear) {
internalProps.onClear();
}
// ============================= Render ============================= if (onClear) {
const mergedClassName = classNames(prefixCls, this.$attrs.class, { onClear();
[`${prefixCls}-focused`]: mockFocused, }
[`${prefixCls}-multiple`]: isMultiple,
[`${prefixCls}-single`]: !isMultiple, triggerChange([]);
[`${prefixCls}-allow-clear`]: allowClear, triggerSearch('', false, false);
[`${prefixCls}-show-arrow`]: mergedShowArrow, };
[`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-loading`]: loading, if (!disabled && allowClear && (mergedRawValue.value.length || mergedSearchValue.value)) {
[`${prefixCls}-open`]: mergedOpen, clearNode = (
[`${prefixCls}-customize-input`]: customizeInputElement, <TransBtn
[`${prefixCls}-show-search`]: mergedShowSearch, class={`${prefixCls}-clear`}
}); onMousedown={onClearMouseDown}
return ( customizeIcon={clearIcon}
<div
{...this.$attrs}
class={mergedClassName}
{...domProps}
ref="containerRef"
onMousedown={onInternalMouseDown}
onKeydown={onInternalKeyDown}
onKeyup={onInternalKeyUp}
// onFocus={onContainerFocus} // trigger by input
// onBlur={onContainerBlur} // trigger by input
>
{mockFocused && !mergedOpen && (
<span
style={{
width: 0,
height: 0,
display: 'flex',
overflow: 'hidden',
opacity: 0,
}}
aria-live="polite"
> >
{/* Merge into one string to make screen reader work as expect */} ×
{`${mergedRawValue.join(', ')}`} </TransBtn>
</span> );
)} }
<SelectTrigger
ref="triggerRef"
disabled={disabled}
prefixCls={prefixCls}
visible={triggerOpen}
popupElement={popupNode}
containerWidth={containerWidth}
animation={animation}
transitionName={transitionName}
dropdownStyle={dropdownStyle}
dropdownClassName={dropdownClassName}
direction={direction}
dropdownMatchSelectWidth={dropdownMatchSelectWidth}
dropdownRender={dropdownRender as any}
dropdownAlign={dropdownAlign}
getPopupContainer={getPopupContainer}
empty={!mergedOptions.length}
getTriggerDOMNode={() => selectorDomRef.current}
>
<Selector
{...this.$props}
domRef={selectorDomRef}
prefixCls={prefixCls}
inputElement={customizeInputElement}
ref="selectorRef"
id={mergedId}
showSearch={mergedShowSearch}
mode={mode}
accessibilityIndex={accessibilityIndex}
multiple={isMultiple}
tagRender={tagRender}
values={displayValues}
open={mergedOpen}
onToggleOpen={onToggleOpen}
searchValue={mergedSearchValue}
activeValue={activeValue}
onSearch={triggerSearch}
onSearchSubmit={onSearchSubmit}
onSelect={onInternalSelectionSelect}
tokenWithEnter={tokenWithEnter}
/>
</SelectTrigger>
{arrowNode} // ============================= Arrow ==============================
{clearNode} const mergedShowArrow =
</div> showArrow !== undefined
); ? showArrow
: loading || (!isMultiple.value && mode !== 'combobox');
let arrowNode: VNode | JSX.Element;
if (mergedShowArrow) {
arrowNode = (
<TransBtn
class={classNames(`${prefixCls}-arrow`, {
[`${prefixCls}-arrow-loading`]: loading,
})}
customizeIcon={inputIcon}
customizeIconProps={{
loading,
searchValue: mergedSearchValue.value,
open: mergedOpen.value,
focused: mockFocused.value,
showSearch: mergedShowSearch.value,
}}
/>
);
}
// ============================ Warning =============================
if (process.env.NODE_ENV !== 'production' && warningProps) {
warningProps(props);
}
// ============================= Render =============================
const mergedClassName = classNames(prefixCls, attrs.class, {
[`${prefixCls}-focused`]: mockFocused.value,
[`${prefixCls}-multiple`]: isMultiple.value,
[`${prefixCls}-single`]: !isMultiple.value,
[`${prefixCls}-allow-clear`]: allowClear,
[`${prefixCls}-show-arrow`]: mergedShowArrow,
[`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-loading`]: loading,
[`${prefixCls}-open`]: mergedOpen.value,
[`${prefixCls}-customize-input`]: customizeInputElement,
[`${prefixCls}-show-search`]: mergedShowSearch.value,
});
return (
<div
{...attrs}
class={mergedClassName}
{...domProps}
ref={containerRef}
onMousedown={onInternalMouseDown}
onKeydown={onInternalKeyDown}
onKeyup={onInternalKeyUp}
// onFocus={onContainerFocus} // trigger by input
// onBlur={onContainerBlur} // trigger by input
>
{mockFocused.value && !mergedOpen.value && (
<span
style={{
width: 0,
height: 0,
display: 'flex',
overflow: 'hidden',
opacity: 0,
}}
aria-live="polite"
>
{/* Merge into one string to make screen reader work as expect */}
{`${mergedRawValue.value.join(', ')}`}
</span>
)}
<SelectTrigger
ref={triggerRef}
disabled={disabled}
prefixCls={prefixCls}
visible={triggerOpen.value}
popupElement={popupNode}
containerWidth={containerWidth.value}
animation={animation}
transitionName={transitionName}
dropdownStyle={dropdownStyle}
dropdownClassName={dropdownClassName}
direction={direction}
dropdownMatchSelectWidth={dropdownMatchSelectWidth}
dropdownRender={dropdownRender as any}
dropdownAlign={dropdownAlign}
getPopupContainer={getPopupContainer}
empty={!mergedOptions.value.length}
getTriggerDOMNode={() => selectorDomRef.current}
>
<Selector
{...props}
domRef={selectorDomRef}
prefixCls={prefixCls}
inputElement={customizeInputElement}
ref={selectorRef}
id={mergedId.value}
showSearch={mergedShowSearch.value}
mode={mode}
accessibilityIndex={accessibilityIndex.value}
multiple={isMultiple.value}
tagRender={tagRender}
values={displayValues.value}
open={mergedOpen.value}
onToggleOpen={onToggleOpen}
searchValue={mergedSearchValue.value}
activeValue={activeValue.value}
onSearch={triggerSearch}
onSearchSubmit={onSearchSubmit}
onSelect={onInternalSelectionSelect}
tokenWithEnter={tokenWithEnter.value}
/>
</SelectTrigger>
{arrowNode}
{clearNode}
</div>
);
};
}, },
}); });
return Select; return Select;

View File

@ -34,7 +34,6 @@ export default defineComponent({
inheritAttrs: false, inheritAttrs: false,
props: optionListProps<DataNode>(), props: optionListProps<DataNode>(),
slots: ['notFoundContent', 'menuItemSelectedIcon'], slots: ['notFoundContent', 'menuItemSelectedIcon'],
expose: ['scrollTo', 'onKeydown', 'onKeyup'],
setup(props, { slots, expose }) { setup(props, { slots, expose }) {
const context = useInjectTreeSelectContext(); const context = useInjectTreeSelectContext();
@ -153,7 +152,7 @@ export default defineComponent({
case KeyCode.DOWN: case KeyCode.DOWN:
case KeyCode.LEFT: case KeyCode.LEFT:
case KeyCode.RIGHT: case KeyCode.RIGHT:
treeRef.value?.onKeyDown(event); treeRef.value?.onKeydown(event);
break; break;
// >>> Select item // >>> Select item

View File

@ -8,7 +8,8 @@ export interface TreeNodeProps extends Omit<DataNode, 'children'> {
} }
/** This is a placeholder, not real render in dom */ /** This is a placeholder, not real render in dom */
const TreeNode: FunctionalComponent<TreeNodeProps> = () => null; const TreeNode: FunctionalComponent<TreeNodeProps> & { isTreeSelectNode: boolean } = () => null;
TreeNode.inheritAttrs = false; TreeNode.inheritAttrs = false;
TreeNode.displayName = 'ATreeSelectNode'; TreeNode.displayName = 'ATreeSelectNode';
TreeNode.isTreeSelectNode = true;
export default TreeNode; export default TreeNode;

View File

@ -161,7 +161,7 @@ export default function generate(config: {
}); });
// ========================== Ref ========================== // ========================== Ref ==========================
const selectRef = ref(null); const selectRef = ref();
expose({ expose({
scrollTo: (...args: any[]) => selectRef.value.scrollTo?.(...args), scrollTo: (...args: any[]) => selectRef.value.scrollTo?.(...args),

View File

@ -1,5 +1,6 @@
import type { VNodeChild } from 'vue';
import { camelize } from 'vue';
import { warning } from '../../vc-util/warning'; import { warning } from '../../vc-util/warning';
import { isValidElement } from '../../_util/props-util';
import type { import type {
DataNode, DataNode,
LegacyDataNode, LegacyDataNode,
@ -10,32 +11,58 @@ import type {
} from '../interface'; } from '../interface';
import TreeNode from '../TreeNode'; import TreeNode from '../TreeNode';
export function convertChildrenToData(nodes): DataNode[] { function isTreeSelectNode(node: any) {
return nodes return node && node.type && (node.type as any).isTreeSelectNode;
.map(node => { }
if (!isValidElement(node) || !node.type) { export function convertChildrenToData(rootNodes: VNodeChild): DataNode[] {
function dig(treeNodes: any[] = []): DataNode[] {
return treeNodes.map(treeNode => {
// Filter invalidate node
if (!isTreeSelectNode(treeNode)) {
warning(!treeNode, 'TreeSelect/TreeSelectNode can only accept TreeSelectNode as children.');
return null; return null;
} }
const slots = (treeNode.children as any) || {};
const key = treeNode.key as string | number;
const props: any = {};
for (const [k, v] of Object.entries(treeNode.props)) {
props[camelize(k)] = v;
}
const { isLeaf, checkable, selectable, disabled, disableCheckbox } = props;
// undefined
const newProps = {
isLeaf: isLeaf || isLeaf === '' || undefined,
checkable: checkable || checkable === '' || undefined,
selectable: selectable || selectable === '' || undefined,
disabled: disabled || disabled === '' || undefined,
disableCheckbox: disableCheckbox || disableCheckbox === '' || undefined,
};
const slotsProps = { ...props, ...newProps };
const { const {
title = slots.title?.(slotsProps),
switcherIcon = slots.switcherIcon?.(slotsProps),
...rest
} = props;
const children = slots.default?.();
const dataNode: DataNode = {
...rest,
title,
switcherIcon,
key, key,
props: { children, value, ...restProps }, isLeaf,
} = node; ...newProps,
const data = {
key,
value,
...restProps,
}; };
const childData = convertChildrenToData(children); const parsedChildren = dig(children);
if (childData.length) { if (parsedChildren.length) {
data.children = childData; dataNode.children = parsedChildren;
} }
return data; return dataNode;
}) });
.filter(data => data); }
return dig(rootNodes as any[]);
} }
export function fillLegacyProps(dataNode: DataNode): LegacyDataNode { export function fillLegacyProps(dataNode: DataNode): LegacyDataNode {

View File

@ -96,8 +96,8 @@ export default defineComponent({
props: nodeListProps, props: nodeListProps,
setup(props, { expose, attrs }) { setup(props, { expose, attrs }) {
// =============================== Ref ================================ // =============================== Ref ================================
const listRef = ref(null); const listRef = ref();
const indentMeasurerRef = ref(null); const indentMeasurerRef = ref();
expose({ expose({
scrollTo: scroll => { scrollTo: scroll => {
listRef.value.scrollTo(scroll); listRef.value.scrollTo(scroll);

View File

@ -870,8 +870,8 @@ export default defineComponent({
active: true, active: true,
}); });
}); });
const onKeyDown = event => { const onKeydown = event => {
const { onKeyDown, checkable, selectable } = props; const { onKeydown, checkable, selectable } = props;
// >>>>>>>>>> Direction // >>>>>>>>>> Direction
switch (event.which) { switch (event.which) {
@ -943,13 +943,14 @@ export default defineComponent({
} }
} }
if (onKeyDown) { if (onKeydown) {
onKeyDown(event); onKeydown(event);
} }
}; };
expose({ expose({
onNodeExpand, onNodeExpand,
scrollTo, scrollTo,
onKeydown,
}); });
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('dragend', onWindowDragEnd); window.removeEventListener('dragend', onWindowDragEnd);
@ -1068,7 +1069,7 @@ export default defineComponent({
activeItem={activeItem.value} activeItem={activeItem.value}
onFocus={onFocus} onFocus={onFocus}
onBlur={onBlur} onBlur={onBlur}
onKeydown={onKeyDown} onKeydown={onKeydown}
onActiveChange={onActiveChange} onActiveChange={onActiveChange}
onListChangeStart={onListChangeStart} onListChangeStart={onListChangeStart}
onListChangeEnd={onListChangeEnd} onListChangeEnd={onListChangeEnd}

View File

@ -146,7 +146,7 @@ export const treeProps = () => ({
}, },
onFocus: { type: Function as PropType<(e: FocusEvent) => void> }, onFocus: { type: Function as PropType<(e: FocusEvent) => void> },
onBlur: { type: Function as PropType<(e: FocusEvent) => void> }, onBlur: { type: Function as PropType<(e: FocusEvent) => void> },
onKeyDown: { type: Function as PropType<EventHandlerNonNull> }, onKeydown: { type: Function as PropType<EventHandlerNonNull> },
onContextmenu: { type: Function as PropType<EventHandlerNonNull> }, onContextmenu: { type: Function as PropType<EventHandlerNonNull> },
onClick: { type: Function as PropType<NodeMouseEventHandler> }, onClick: { type: Function as PropType<NodeMouseEventHandler> },
onDblclick: { type: Function as PropType<NodeMouseEventHandler> }, onDblclick: { type: Function as PropType<NodeMouseEventHandler> },

View File

@ -12,6 +12,7 @@ import { getPosition, isTreeNode } from '../util';
import { warning } from '../../vc-util/warning'; import { warning } from '../../vc-util/warning';
import Omit from 'omit.js'; import Omit from 'omit.js';
import type { VNodeChild } from 'vue'; import type { VNodeChild } from 'vue';
import { camelize } from 'vue';
import type { TreeNodeProps } from '../props'; import type { TreeNodeProps } from '../props';
export function getKey(key: Key, pos: string) { export function getKey(key: Key, pos: string) {
@ -60,74 +61,58 @@ export function warningWithoutKey(treeData: DataNode[], fieldNames: FieldNames)
dig(treeData); dig(treeData);
} }
const cacheStringFunction = (fn: (s: string) => string) => {
const cache = Object.create(null);
return (str: string) => {
const hit = cache[str];
return hit || (cache[str] = fn(str));
};
};
const camelizeRE = /-(\w)/g;
const camelize = cacheStringFunction((str: string) => {
return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''));
});
/** /**
* Convert `children` of Tree into `treeData` structure. * Convert `children` of Tree into `treeData` structure.
*/ */
export function convertTreeToData(rootNodes: VNodeChild): DataNode[] { export function convertTreeToData(rootNodes: VNodeChild): DataNode[] {
function dig(node: VNodeChild = []): DataNode[] { function dig(node: VNodeChild = []): DataNode[] {
const treeNodes = node as NodeElement[]; const treeNodes = node as NodeElement[];
return treeNodes return treeNodes.map(treeNode => {
.map(treeNode => { // Filter invalidate node
// Filter invalidate node if (!isTreeNode(treeNode)) {
if (!isTreeNode(treeNode)) { warning(!treeNode, 'Tree/TreeNode can only accept TreeNode as children.');
warning(!treeNode, 'Tree/TreeNode can only accept TreeNode as children.'); return null;
return null; }
} const slots = (treeNode.children as any) || {};
const slots = (treeNode.children as any) || {}; const key = treeNode.key as string | number;
const key = treeNode.key as string | number; const props: any = {};
const props: any = {}; for (const [k, v] of Object.entries(treeNode.props)) {
for (const [k, v] of Object.entries(treeNode.props)) { props[camelize(k)] = v;
props[camelize(k)] = v; }
} const { isLeaf, checkable, selectable, disabled, disableCheckbox } = props;
const { isLeaf, checkable, selectable, disabled, disableCheckbox } = props; // 默认值为 undefined
// 默认值为 undefined const newProps = {
const newProps = { isLeaf: isLeaf || isLeaf === '' || undefined,
isLeaf: isLeaf || isLeaf === '' || undefined, checkable: checkable || checkable === '' || undefined,
checkable: checkable || checkable === '' || undefined, selectable: selectable || selectable === '' || undefined,
selectable: selectable || selectable === '' || undefined, disabled: disabled || disabled === '' || undefined,
disabled: disabled || disabled === '' || undefined, disableCheckbox: disableCheckbox || disableCheckbox === '' || undefined,
disableCheckbox: disableCheckbox || disableCheckbox === '' || undefined, };
}; const slotsProps = { ...props, ...newProps };
const slotsProps = { ...props, ...newProps }; const {
const { title = slots.title?.(slotsProps),
title = slots.title?.(slotsProps), icon = slots.icon?.(slotsProps),
icon = slots.icon?.(slotsProps), switcherIcon = slots.switcherIcon?.(slotsProps),
switcherIcon = slots.switcherIcon?.(slotsProps), ...rest
...rest } = props;
} = props; const children = slots.default?.();
const children = slots.default?.(); const dataNode: DataNode = {
const dataNode: DataNode = { ...rest,
...rest, title,
title, icon,
icon, switcherIcon,
switcherIcon, key,
key, isLeaf,
isLeaf, ...newProps,
...newProps, };
};
const parsedChildren = dig(children); const parsedChildren = dig(children);
if (parsedChildren.length) { if (parsedChildren.length) {
dataNode.children = parsedChildren; dataNode.children = parsedChildren;
} }
return dataNode; return dataNode;
}) });
.filter((dataNode: DataNode) => dataNode);
} }
return dig(rootNodes); return dig(rootNodes);

View File

@ -1,64 +1,40 @@
<template> <template>
<a-tree-select <a-tree-select
v-model:value="value" v-model:value="value"
show-search
style="width: 100%" style="width: 100%"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }" :dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
:tree-data="treeData"
placeholder="Please select" placeholder="Please select"
allow-clear
multiple
tree-default-expand-all tree-default-expand-all
> >
<template #title="{ key, value }"> <a-tree-select-node value="parent 1" title="parent 1">
<span v-if="key === '0-0-1'" style="color: #08c">Child Node1 {{ value }}</span> <a-tree-select-node value="parent 1-0" title="parent 1-0">
</template> <a-tree-select-node value="leaf1" title="my leaf" />
<a-tree-select-node value="leaf2" title="your leaf" />
</a-tree-select-node>
<a-tree-select-node value="parent 1-1" title="parent 1-1">
<a-tree-select-node value="sss">
<template #title><b style="color: #08c">sss</b></template>
</a-tree-select-node>
</a-tree-select-node>
</a-tree-select-node>
</a-tree-select> </a-tree-select>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, watch } from 'vue'; import { defineComponent, ref, watch } from 'vue';
interface TreeDataItem {
value: string;
key: string;
title?: string;
slots?: Record<string, string>;
children?: TreeDataItem[];
}
const treeData: TreeDataItem[] = [
{
title: 'Node1',
value: '0-0',
key: '0-0',
children: [
{
value: '0-0-1',
key: '0-0-1',
slots: {
title: 'title1',
},
},
{
title: 'Child Node2',
value: '0-0-2',
key: '0-0-2',
},
],
},
{
title: 'Node2',
value: '0-1',
key: '0-1',
},
];
export default defineComponent({ export default defineComponent({
setup() { setup() {
const value = ref<string>(); const value = ref<string[]>([]);
watch(value, () => { watch(value, () => {
console.log(value.value); console.log('select', value.value);
}); });
return { return {
value, value,
treeData,
}; };
}, },
}); });