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 classNames from '../_util/classNames';
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 getIcons from './utils/iconUtil';
import PropTypes from '../_util/vue-types';
import { tuple } from '../_util/type';
import useConfigInject from '../_util/hooks/useConfigInject';
import type { SizeType } from '../config-provider';
import omit from '../_util/omit';
type RawValue = string | number;
@ -62,7 +61,7 @@ const Select = defineComponent({
'option',
],
setup(props, { attrs, emit, slots, expose }) {
const selectRef = ref(null);
const selectRef = ref();
const focus = () => {
if (selectRef.value) {

View File

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

View File

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

View File

@ -1,7 +1,8 @@
import pickAttrs from '../../_util/pickAttrs';
import Input from './Input';
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 PropTypes from '../../_util/vue-types';
import { useInjectTreeSelectContext } from 'ant-design-vue/es/vc-tree-select/Context';

View File

@ -52,7 +52,7 @@ export interface SelectorProps {
// Motion
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: (searchText: string, fromTyping: boolean, isCompositing: boolean) => boolean;
onSearchSubmit: (searchText: string) => void;

View File

@ -37,6 +37,7 @@ import useSelectTriggerControl from './hooks/useSelectTriggerControl';
import useCacheDisplayValue from './hooks/useCacheDisplayValue';
import useCacheOptions from './hooks/useCacheOptions';
import type { CSSProperties, DefineComponent, PropType, VNode, VNodeChild } from 'vue';
import { getCurrentInstance } from 'vue';
import {
computed,
defineComponent,
@ -275,7 +276,7 @@ export default function generateSelector<
slots: ['option'],
inheritAttrs: false,
props: selectBaseProps<OptionType, DefaultValueType>(),
setup(props, { expose }) {
setup(props, { expose, attrs, slots }) {
const useInternalProps = computed(
() => props.internalProps && props.internalProps.mark === INTERNAL_PROPS_MARK,
);
@ -284,10 +285,10 @@ export default function generateSelector<
'Select',
'optionFilterProp not support children, please use label instead',
);
const containerRef = ref(null);
const triggerRef = ref(null);
const selectorRef = ref(null);
const listRef = ref(null);
const containerRef = ref();
const triggerRef = ref();
const selectorRef = ref();
const listRef = ref();
const tokenWithEnter = computed(() =>
(props.tokenSeparators || []).some(tokenSeparator =>
['\n', '\r\n'].includes(tokenSeparator),
@ -353,7 +354,7 @@ export default function generateSelector<
// ============================= Option =============================
// 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) => {
activeValue.value = val;
};
@ -925,7 +926,7 @@ export default function generateSelector<
};
// ============================= Popup ==============================
const containerWidth = ref(null);
const containerWidth = ref<number>(null);
onMounted(() => {
watch(
triggerOpen,
@ -952,86 +953,12 @@ export default function generateSelector<
blur,
scrollTo: (...args: any[]) => listRef.value?.scrollTo(...args),
});
return {
tokenWithEnter,
mockFocused,
mergedId,
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,
};
},
methods: {
const instance = getCurrentInstance();
const onPopupMouseEnter = () => {
// We need force update here since popup dom is render async
onPopupMouseEnter() {
(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;
instance.update();
};
return () => {
const {
prefixCls = defaultPrefixCls,
id,
@ -1107,8 +1034,7 @@ export default function generateSelector<
internalProps = {},
...restProps
} = this.$props; //as SelectProps<OptionType[], ValueType>;
} = props; //as SelectProps<OptionType[], ValueType>;
// ============================= Input ==============================
// Only works in `combobox`
const customizeInputElement: VNodeChild | JSX.Element =
@ -1120,24 +1046,24 @@ export default function generateSelector<
});
const popupNode = (
<OptionList
ref="listRef"
ref={listRef}
prefixCls={prefixCls}
id={mergedId}
open={mergedOpen}
id={mergedId.value}
open={mergedOpen.value}
childrenAsData={!options}
options={displayOptions}
flattenOptions={displayFlattenOptions}
multiple={isMultiple}
values={rawValues}
options={displayOptions.value}
flattenOptions={displayFlattenOptions.value}
multiple={isMultiple.value}
values={rawValues.value}
height={listHeight}
itemHeight={listItemHeight}
onSelect={onInternalOptionSelect}
onToggleOpen={onToggleOpen}
onActiveValue={onActiveValue}
defaultActiveFirstOption={mergedDefaultActiveFirstOption}
defaultActiveFirstOption={mergedDefaultActiveFirstOption.value}
notFoundContent={notFoundContent}
onScroll={onPopupScroll}
searchValue={mergedSearchValue}
searchValue={mergedSearchValue.value}
menuItemSelectedIcon={menuItemSelectedIcon}
virtual={virtual !== false && dropdownMatchSelectWidth !== false}
onMouseenter={onPopupMouseEnter}
@ -1149,7 +1075,7 @@ export default function generateSelector<
let clearNode: VNode | JSX.Element;
const onClearMouseDown = () => {
// Trigger internal `onClear` event
if (useInternalProps && internalProps.onClear) {
if (useInternalProps.value && internalProps.onClear) {
internalProps.onClear();
}
@ -1161,7 +1087,7 @@ export default function generateSelector<
triggerSearch('', false, false);
};
if (!disabled && allowClear && (mergedRawValue.length || mergedSearchValue)) {
if (!disabled && allowClear && (mergedRawValue.value.length || mergedSearchValue.value)) {
clearNode = (
<TransBtn
class={`${prefixCls}-clear`}
@ -1175,7 +1101,9 @@ export default function generateSelector<
// ============================= Arrow ==============================
const mergedShowArrow =
showArrow !== undefined ? showArrow : loading || (!isMultiple && mode !== 'combobox');
showArrow !== undefined
? showArrow
: loading || (!isMultiple.value && mode !== 'combobox');
let arrowNode: VNode | JSX.Element;
if (mergedShowArrow) {
@ -1187,10 +1115,10 @@ export default function generateSelector<
customizeIcon={inputIcon}
customizeIconProps={{
loading,
searchValue: mergedSearchValue,
open: mergedOpen,
focused: mockFocused,
showSearch: mergedShowSearch,
searchValue: mergedSearchValue.value,
open: mergedOpen.value,
focused: mockFocused.value,
showSearch: mergedShowSearch.value,
}}
/>
);
@ -1198,35 +1126,35 @@ export default function generateSelector<
// ============================ Warning =============================
if (process.env.NODE_ENV !== 'production' && warningProps) {
warningProps(this.$props);
warningProps(props);
}
// ============================= Render =============================
const mergedClassName = classNames(prefixCls, this.$attrs.class, {
[`${prefixCls}-focused`]: mockFocused,
[`${prefixCls}-multiple`]: isMultiple,
[`${prefixCls}-single`]: !isMultiple,
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,
[`${prefixCls}-open`]: mergedOpen.value,
[`${prefixCls}-customize-input`]: customizeInputElement,
[`${prefixCls}-show-search`]: mergedShowSearch,
[`${prefixCls}-show-search`]: mergedShowSearch.value,
});
return (
<div
{...this.$attrs}
{...attrs}
class={mergedClassName}
{...domProps}
ref="containerRef"
ref={containerRef}
onMousedown={onInternalMouseDown}
onKeydown={onInternalKeyDown}
onKeyup={onInternalKeyUp}
// onFocus={onContainerFocus} // trigger by input
// onBlur={onContainerBlur} // trigger by input
>
{mockFocused && !mergedOpen && (
{mockFocused.value && !mergedOpen.value && (
<span
style={{
width: 0,
@ -1238,16 +1166,16 @@ export default function generateSelector<
aria-live="polite"
>
{/* Merge into one string to make screen reader work as expect */}
{`${mergedRawValue.join(', ')}`}
{`${mergedRawValue.value.join(', ')}`}
</span>
)}
<SelectTrigger
ref="triggerRef"
ref={triggerRef}
disabled={disabled}
prefixCls={prefixCls}
visible={triggerOpen}
visible={triggerOpen.value}
popupElement={popupNode}
containerWidth={containerWidth}
containerWidth={containerWidth.value}
animation={animation}
transitionName={transitionName}
dropdownStyle={dropdownStyle}
@ -1257,30 +1185,30 @@ export default function generateSelector<
dropdownRender={dropdownRender as any}
dropdownAlign={dropdownAlign}
getPopupContainer={getPopupContainer}
empty={!mergedOptions.length}
empty={!mergedOptions.value.length}
getTriggerDOMNode={() => selectorDomRef.current}
>
<Selector
{...this.$props}
{...props}
domRef={selectorDomRef}
prefixCls={prefixCls}
inputElement={customizeInputElement}
ref="selectorRef"
id={mergedId}
showSearch={mergedShowSearch}
ref={selectorRef}
id={mergedId.value}
showSearch={mergedShowSearch.value}
mode={mode}
accessibilityIndex={accessibilityIndex}
multiple={isMultiple}
accessibilityIndex={accessibilityIndex.value}
multiple={isMultiple.value}
tagRender={tagRender}
values={displayValues}
open={mergedOpen}
values={displayValues.value}
open={mergedOpen.value}
onToggleOpen={onToggleOpen}
searchValue={mergedSearchValue}
activeValue={activeValue}
searchValue={mergedSearchValue.value}
activeValue={activeValue.value}
onSearch={triggerSearch}
onSearchSubmit={onSearchSubmit}
onSelect={onInternalSelectionSelect}
tokenWithEnter={tokenWithEnter}
tokenWithEnter={tokenWithEnter.value}
/>
</SelectTrigger>
@ -1288,6 +1216,7 @@ export default function generateSelector<
{clearNode}
</div>
);
};
},
});
return Select;

View File

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

View File

@ -8,7 +8,8 @@ export interface TreeNodeProps extends Omit<DataNode, 'children'> {
}
/** 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.displayName = 'ATreeSelectNode';
TreeNode.isTreeSelectNode = true;
export default TreeNode;

View File

@ -161,7 +161,7 @@ export default function generate(config: {
});
// ========================== Ref ==========================
const selectRef = ref(null);
const selectRef = ref();
expose({
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 { isValidElement } from '../../_util/props-util';
import type {
DataNode,
LegacyDataNode,
@ -10,32 +11,58 @@ import type {
} from '../interface';
import TreeNode from '../TreeNode';
export function convertChildrenToData(nodes): DataNode[] {
return nodes
.map(node => {
if (!isValidElement(node) || !node.type) {
function isTreeSelectNode(node: any) {
return node && node.type && (node.type as any).isTreeSelectNode;
}
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;
}
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 {
title = slots.title?.(slotsProps),
switcherIcon = slots.switcherIcon?.(slotsProps),
...rest
} = props;
const children = slots.default?.();
const dataNode: DataNode = {
...rest,
title,
switcherIcon,
key,
props: { children, value, ...restProps },
} = node;
const data = {
key,
value,
...restProps,
isLeaf,
...newProps,
};
const childData = convertChildrenToData(children);
if (childData.length) {
data.children = childData;
const parsedChildren = dig(children);
if (parsedChildren.length) {
dataNode.children = parsedChildren;
}
return data;
})
.filter(data => data);
return dataNode;
});
}
return dig(rootNodes as any[]);
}
export function fillLegacyProps(dataNode: DataNode): LegacyDataNode {

View File

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

View File

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

View File

@ -146,7 +146,7 @@ export const treeProps = () => ({
},
onFocus: { 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> },
onClick: { 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 Omit from 'omit.js';
import type { VNodeChild } from 'vue';
import { camelize } from 'vue';
import type { TreeNodeProps } from '../props';
export function getKey(key: Key, pos: string) {
@ -60,28 +61,13 @@ export function warningWithoutKey(treeData: DataNode[], fieldNames: FieldNames)
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.
*/
export function convertTreeToData(rootNodes: VNodeChild): DataNode[] {
function dig(node: VNodeChild = []): DataNode[] {
const treeNodes = node as NodeElement[];
return treeNodes
.map(treeNode => {
return treeNodes.map(treeNode => {
// Filter invalidate node
if (!isTreeNode(treeNode)) {
warning(!treeNode, 'Tree/TreeNode can only accept TreeNode as children.');
@ -126,8 +112,7 @@ export function convertTreeToData(rootNodes: VNodeChild): DataNode[] {
}
return dataNode;
})
.filter((dataNode: DataNode) => dataNode);
});
}
return dig(rootNodes);

View File

@ -1,64 +1,40 @@
<template>
<a-tree-select
v-model:value="value"
show-search
style="width: 100%"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
:tree-data="treeData"
placeholder="Please select"
allow-clear
multiple
tree-default-expand-all
>
<template #title="{ key, value }">
<span v-if="key === '0-0-1'" style="color: #08c">Child Node1 {{ value }}</span>
</template>
<a-tree-select-node value="parent 1" title="parent 1">
<a-tree-select-node value="parent 1-0" title="parent 1-0">
<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>
</template>
<script lang="ts">
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({
setup() {
const value = ref<string>();
const value = ref<string[]>([]);
watch(value, () => {
console.log(value.value);
console.log('select', value.value);
});
return {
value,
treeData,
};
},
});