Merge branch 'refactor-tree' into v2.3

pull/4606/head
tangjinzhou 2021-08-26 14:22:36 +08:00
commit d72798bc08
112 changed files with 7736 additions and 7544 deletions

View File

@ -0,0 +1,89 @@
// copy from https://github.dev/vueuse/vueuse
import type { Ref, WatchOptions, WatchStopHandle } from 'vue';
import { unref, watch } from 'vue';
type MaybeRef<T> = T | Ref<T>;
type Fn = () => void;
export type FunctionArgs<Args extends any[] = any[], Return = void> = (...args: Args) => Return;
export interface FunctionWrapperOptions<Args extends any[] = any[], This = any> {
fn: FunctionArgs<Args, This>;
args: Args;
thisArg: This;
}
export type EventFilter<Args extends any[] = any[], This = any> = (
invoke: Fn,
options: FunctionWrapperOptions<Args, This>,
) => void;
const bypassFilter: EventFilter = invoke => {
return invoke();
};
/**
* Create an EventFilter that debounce the events
*
* @param ms
*/
export function debounceFilter(ms: MaybeRef<number>) {
let timer: ReturnType<typeof setTimeout> | undefined;
const filter: EventFilter = invoke => {
const duration = unref(ms);
if (timer) clearTimeout(timer);
if (duration <= 0) return invoke();
timer = setTimeout(invoke, duration);
};
return filter;
}
export interface DebouncedWatchOptions<Immediate> extends WatchOptions<Immediate> {
debounce?: MaybeRef<number>;
}
interface ConfigurableEventFilter {
eventFilter?: EventFilter;
}
/**
* @internal
*/
function createFilterWrapper<T extends FunctionArgs>(filter: EventFilter, fn: T) {
function wrapper(this: any, ...args: any[]) {
filter(() => fn.apply(this, args), { fn, thisArg: this, args });
}
return wrapper as any as T;
}
export interface WatchWithFilterOptions<Immediate>
extends WatchOptions<Immediate>,
ConfigurableEventFilter {}
// implementation
export function watchWithFilter<Immediate extends Readonly<boolean> = false>(
source: any,
cb: any,
options: WatchWithFilterOptions<Immediate> = {},
): WatchStopHandle {
const { eventFilter = bypassFilter, ...watchOptions } = options;
return watch(source, createFilterWrapper(eventFilter, cb), watchOptions);
}
// implementation
export default function debouncedWatch<Immediate extends Readonly<boolean> = false>(
source: any,
cb: any,
options: DebouncedWatchOptions<Immediate> = {},
): WatchStopHandle {
const { debounce = 0, ...watchOptions } = options;
return watchWithFilter(source, cb, {
...watchOptions,
eventFilter: debounceFilter(debounce),
});
}

View File

@ -14,30 +14,40 @@ export default (
direction: ComputedRef<Direction>; direction: ComputedRef<Direction>;
size: ComputedRef<SizeType>; size: ComputedRef<SizeType>;
getTargetContainer: ComputedRef<() => HTMLElement>; getTargetContainer: ComputedRef<() => HTMLElement>;
getPopupContainer: ComputedRef<() => HTMLElement>;
space: ComputedRef<{ size: SizeType | number }>; space: ComputedRef<{ size: SizeType | number }>;
pageHeader: ComputedRef<{ ghost: boolean }>; pageHeader: ComputedRef<{ ghost: boolean }>;
form?: ComputedRef<{ form?: ComputedRef<{
requiredMark?: RequiredMark; requiredMark?: RequiredMark;
}>; }>;
autoInsertSpaceInButton: ComputedRef<Boolean>; autoInsertSpaceInButton: ComputedRef<boolean>;
renderEmpty?: ComputedRef<(componentName?: string) => VNodeChild | JSX.Element>; renderEmpty?: ComputedRef<(componentName?: string) => VNodeChild | JSX.Element>;
virtual: ComputedRef<boolean>;
dropdownMatchSelectWidth: ComputedRef<boolean>;
getPopupContainer: ComputedRef<ConfigProviderProps['getPopupContainer']>;
} => { } => {
const configProvider = inject<UnwrapRef<ConfigProviderProps>>( const configProvider = inject<UnwrapRef<ConfigProviderProps>>(
'configProvider', 'configProvider',
defaultConfigProvider, defaultConfigProvider,
); );
const prefixCls = computed(() => configProvider.getPrefixCls(name, props.prefixCls)); const prefixCls = computed(() => configProvider.getPrefixCls(name, props.prefixCls));
const direction = computed(() => props.direction ?? configProvider.direction);
const rootPrefixCls = computed(() => configProvider.getPrefixCls()); const rootPrefixCls = computed(() => configProvider.getPrefixCls());
const direction = computed(() => configProvider.direction);
const autoInsertSpaceInButton = computed(() => configProvider.autoInsertSpaceInButton); const autoInsertSpaceInButton = computed(() => configProvider.autoInsertSpaceInButton);
const renderEmpty = computed(() => configProvider.renderEmpty); const renderEmpty = computed(() => configProvider.renderEmpty);
const space = computed(() => configProvider.space); const space = computed(() => configProvider.space);
const pageHeader = computed(() => configProvider.pageHeader); const pageHeader = computed(() => configProvider.pageHeader);
const form = computed(() => configProvider.form); const form = computed(() => configProvider.form);
const getTargetContainer = computed(
() => props.getTargetContainer || configProvider.getTargetContainer,
);
const getPopupContainer = computed(
() => props.getPopupContainer || configProvider.getPopupContainer,
);
const virtual = computed(() => props.virtual ?? configProvider.virtual);
const dropdownMatchSelectWidth = computed<boolean>(
() => props.dropdownMatchSelectWidth ?? configProvider.dropdownMatchSelectWidth,
);
const size = computed(() => props.size || configProvider.componentSize); const size = computed(() => props.size || configProvider.componentSize);
const getTargetContainer = computed(() => props.getTargetContainer);
const getPopupContainer = computed(() => props.getPopupContainer);
return { return {
configProvider, configProvider,
prefixCls, prefixCls,
@ -50,6 +60,8 @@ export default (
form, form,
autoInsertSpaceInButton, autoInsertSpaceInButton,
renderEmpty, renderEmpty,
virtual,
dropdownMatchSelectWidth,
rootPrefixCls, rootPrefixCls,
}; };
}; };

10
components/_util/omit.ts Normal file
View File

@ -0,0 +1,10 @@
function omit<T extends object, K extends keyof T>(obj: T, fields: K[]): Omit<T, K> {
// eslint-disable-next-line prefer-object-spread
const shallowCopy = Object.assign({}, obj);
for (let i = 0; i < fields.length; i += 1) {
const key = fields[i];
delete shallowCopy[key];
}
return shallowCopy;
}
export default omit;

View File

@ -17,7 +17,13 @@ const initDefaultProps = <T>(
Object.keys(defaultProps).forEach(k => { Object.keys(defaultProps).forEach(k => {
const prop = propTypes[k] as VueTypeValidableDef; const prop = propTypes[k] as VueTypeValidableDef;
if (prop) { if (prop) {
prop.default = defaultProps[k]; if (prop.type || prop.default) {
prop.default = defaultProps[k];
} else if (prop.def) {
prop.def(defaultProps[k]);
} else {
propTypes[k] = { type: prop, default: defaultProps[k] };
}
} else { } else {
throw new Error(`not have ${k} prop`); throw new Error(`not have ${k} prop`);
} }

View File

@ -1,6 +1,6 @@
import type { App, Plugin, VNode, ExtractPropTypes } from 'vue'; import type { App, Plugin, VNode, ExtractPropTypes } from 'vue';
import { defineComponent, inject, provide } from 'vue'; import { defineComponent, inject, provide } from 'vue';
import Select, { SelectProps } from '../select'; import Select, { selectProps } from '../select';
import Input from '../input'; import Input from '../input';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import { defaultConfigProvider } from '../config-provider'; import { defaultConfigProvider } from '../config-provider';
@ -15,7 +15,7 @@ function isSelectOptionOrSelectOptGroup(child: any): boolean {
} }
const autoCompleteProps = { const autoCompleteProps = {
...SelectProps(), ...selectProps(),
dataSource: PropTypes.array, dataSource: PropTypes.array,
dropdownMenuStyle: PropTypes.style, dropdownMenuStyle: PropTypes.style,
optionLabelProp: PropTypes.string, optionLabelProp: PropTypes.string,
@ -33,7 +33,7 @@ const AutoComplete = defineComponent({
inheritAttrs: false, inheritAttrs: false,
props: { props: {
...autoCompleteProps, ...autoCompleteProps,
prefixCls: PropTypes.string.def('ant-select'), prefixCls: PropTypes.string,
showSearch: PropTypes.looseBool, showSearch: PropTypes.looseBool,
transitionName: PropTypes.string.def('slide-up'), transitionName: PropTypes.string.def('slide-up'),
choiceTransitionName: PropTypes.string.def('zoom'), choiceTransitionName: PropTypes.string.def('zoom'),

View File

@ -169,6 +169,7 @@ export { default as Table, TableColumn, TableColumnGroup } from './table';
export type { TransferProps } from './transfer'; export type { TransferProps } from './transfer';
export { default as Transfer } from './transfer'; export { default as Transfer } from './transfer';
export type { TreeProps, DirectoryTreeProps } from './tree';
export { default as Tree, TreeNode, DirectoryTree } from './tree'; export { default as Tree, TreeNode, DirectoryTree } from './tree';
export type { TreeSelectProps } from './tree-select'; export type { TreeSelectProps } from './tree-select';

View File

@ -318,14 +318,16 @@ const Form = defineComponent({
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
emit('submit', e); emit('submit', e);
const res = validateFields(); if (props.model) {
res const res = validateFields();
.then(values => { res
emit('finish', values); .then(values => {
}) emit('finish', values);
.catch(errors => { })
handleFinishFailed(errors); .catch(errors => {
}); handleFinishFailed(errors);
});
}
}; };
expose({ expose({

View File

@ -107,6 +107,8 @@ function useForm(
validateInfos: validateInfos; validateInfos: validateInfos;
resetFields: (newValues?: Props) => void; resetFields: (newValues?: Props) => void;
validate: <T = any>(names?: namesType, option?: validateOptions) => Promise<T>; validate: <T = any>(names?: namesType, option?: validateOptions) => Promise<T>;
/** This is an internal usage. Do not use in your prod */
validateField: ( validateField: (
name: string, name: string,
value: any, value: any,
@ -117,19 +119,33 @@ function useForm(
clearValidate: (names?: namesType) => void; clearValidate: (names?: namesType) => void;
} { } {
const initialModel = cloneDeep(unref(modelRef)); const initialModel = cloneDeep(unref(modelRef));
let validateInfos: validateInfos = {}; const validateInfos = reactive<validateInfos>({});
const rulesKeys = computed(() => { const rulesKeys = computed(() => {
return Object.keys(unref(rulesRef)); return Object.keys(unref(rulesRef));
}); });
rulesKeys.value.forEach(key => { watch(
validateInfos[key] = { rulesKeys,
autoLink: false, () => {
required: isRequired(unref(rulesRef)[key]), const newValidateInfos = {};
}; rulesKeys.value.forEach(key => {
}); newValidateInfos[key] = validateInfos[key] || {
validateInfos = reactive(validateInfos); autoLink: false,
required: isRequired(unref(rulesRef)[key]),
};
delete validateInfos[key];
});
for (const key in validateInfos) {
if (Object.prototype.hasOwnProperty.call(validateInfos, key)) {
delete validateInfos[key];
}
}
Object.assign(validateInfos, newValidateInfos);
},
{ immediate: true },
);
const resetFields = (newValues: Props) => { const resetFields = (newValues: Props) => {
Object.assign(unref(modelRef), { Object.assign(unref(modelRef), {
...cloneDeep(initialModel), ...cloneDeep(initialModel),
@ -249,6 +265,9 @@ function useForm(
}, },
!!option.validateFirst, !!option.validateFirst,
); );
if (!validateInfos[name]) {
return promise.catch((e: any) => e);
}
validateInfos[name].validateStatus = 'validating'; validateInfos[name].validateStatus = 'validating';
promise promise
.catch((e: any) => e) .catch((e: any) => e)
@ -325,7 +344,9 @@ function useForm(
validate(names, { trigger: 'change' }); validate(names, { trigger: 'change' });
oldModel = cloneDeep(model); oldModel = cloneDeep(model);
}; };
const debounceOptions = options?.debounce; const debounceOptions = options?.debounce;
watch( watch(
modelRef, modelRef,
debounceOptions && debounceOptions.wait debounceOptions && debounceOptions.wait

View File

@ -68,7 +68,7 @@ export default defineComponent({
'click', 'click',
'update:activeKey', 'update:activeKey',
], ],
slots: ['expandIcon'], slots: ['expandIcon', 'overflowedIndicator'],
setup(props, { slots, emit }) { setup(props, { slots, emit }) {
const { prefixCls, direction } = useConfigInject('menu', props); const { prefixCls, direction } = useConfigInject('menu', props);
const store = ref<Record<string, StoreMenuInfo>>({}); const store = ref<Record<string, StoreMenuInfo>>({});
@ -396,7 +396,7 @@ export default defineComponent({
{child} {child}
</MenuContextProvider> </MenuContextProvider>
)); ));
const overflowedIndicator = <EllipsisOutlined />; const overflowedIndicator = slots.overflowedIndicator?.() || <EllipsisOutlined />;
return ( return (
<Overflow <Overflow

View File

@ -1,10 +1,10 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import VcSelect, { SelectProps } from '../select'; import VcSelect, { selectProps } from '../select';
import { getOptionProps, getSlot } from '../_util/props-util'; import { getOptionProps, getSlot } from '../_util/props-util';
export default defineComponent({ export default defineComponent({
inheritAttrs: false, inheritAttrs: false,
props: SelectProps(), props: selectProps(),
Option: VcSelect.Option, Option: VcSelect.Option,
render() { render() {
const selectOptionsProps = getOptionProps(this); const selectOptionsProps = getOptionProps(this);

View File

@ -1,15 +1,14 @@
import type { VNodeChild, App, PropType, Plugin } from 'vue'; import type { App, PropType, Plugin, ExtractPropTypes } from 'vue';
import { computed, defineComponent, ref } from 'vue'; import { computed, defineComponent, ref } from 'vue';
import omit from 'omit.js';
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import type { SelectProps as RcSelectProps } from '../vc-select'; import { selectProps as vcSelectProps } from '../vc-select';
import RcSelect, { Option, OptGroup, BaseProps } 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';
type RawValue = string | number; type RawValue = string | number;
@ -20,47 +19,21 @@ export type OptionType = typeof Option;
export interface LabeledValue { export interface LabeledValue {
key?: string; key?: string;
value: RawValue; value: RawValue;
label: VNodeChild; label: any;
} }
export type SelectValue = RawValue | RawValue[] | LabeledValue | LabeledValue[] | undefined; export type SelectValue = RawValue | RawValue[] | LabeledValue | LabeledValue[] | undefined;
export interface InternalSelectProps<VT> extends Omit<RcSelectProps<VT>, 'mode'> { export const selectProps = () => ({
suffixIcon?: VNodeChild; ...omit(vcSelectProps<SelectValue>(), ['inputIcon', 'mode', 'getInputElement', 'backfill']),
itemIcon?: VNodeChild;
size?: SizeType;
mode?: 'multiple' | 'tags' | 'SECRET_COMBOBOX_MODE_DO_NOT_USE';
bordered?: boolean;
}
export interface SelectPropsTypes<VT>
extends Omit<
InternalSelectProps<VT>,
'inputIcon' | 'mode' | 'getInputElement' | 'backfill' | 'class' | 'style'
> {
mode?: 'multiple' | 'tags';
}
export type SelectTypes = SelectPropsTypes<SelectValue>;
export const SelectProps = () => ({
...(omit(BaseProps(), [
'inputIcon',
'mode',
'getInputElement',
'backfill',
'class',
'style',
]) as Omit<
ReturnType<typeof BaseProps>,
'inputIcon' | 'mode' | 'getInputElement' | 'backfill' | 'class' | 'style'
>),
value: { value: {
type: [Array, Object, String, Number] as PropType<SelectValue>, type: [Array, Object, String, Number] as PropType<SelectValue>,
}, },
defaultValue: { defaultValue: {
type: [Array, Object, String, Number] as PropType<SelectValue>, type: [Array, Object, String, Number] as PropType<SelectValue>,
}, },
notFoundContent: PropTypes.VNodeChild, notFoundContent: PropTypes.any,
suffixIcon: PropTypes.VNodeChild, suffixIcon: PropTypes.any,
itemIcon: PropTypes.VNodeChild, itemIcon: PropTypes.any,
size: PropTypes.oneOf(tuple('small', 'middle', 'large', 'default')), size: PropTypes.oneOf(tuple('small', 'middle', 'large', 'default')),
mode: PropTypes.oneOf(tuple('multiple', 'tags', 'SECRET_COMBOBOX_MODE_DO_NOT_USE')), mode: PropTypes.oneOf(tuple('multiple', 'tags', 'SECRET_COMBOBOX_MODE_DO_NOT_USE')),
bordered: PropTypes.looseBool.def(true), bordered: PropTypes.looseBool.def(true),
@ -68,12 +41,14 @@ export const SelectProps = () => ({
choiceTransitionName: PropTypes.string.def(''), choiceTransitionName: PropTypes.string.def(''),
}); });
export type SelectProps = Partial<ExtractPropTypes<ReturnType<typeof selectProps>>>;
const Select = defineComponent({ const Select = defineComponent({
name: 'ASelect', name: 'ASelect',
Option, Option,
OptGroup, OptGroup,
inheritAttrs: false, inheritAttrs: false,
props: SelectProps(), props: selectProps(),
SECRET_COMBOBOX_MODE_DO_NOT_USE: 'SECRET_COMBOBOX_MODE_DO_NOT_USE', SECRET_COMBOBOX_MODE_DO_NOT_USE: 'SECRET_COMBOBOX_MODE_DO_NOT_USE',
emits: ['change', 'update:value'], emits: ['change', 'update:value'],
slots: [ slots: [
@ -86,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) {
@ -146,7 +121,7 @@ const Select = defineComponent({
const isMultiple = mode.value === 'multiple' || mode.value === 'tags'; const isMultiple = mode.value === 'multiple' || mode.value === 'tags';
// ===================== Empty ===================== // ===================== Empty =====================
let mergedNotFound: VNodeChild; let mergedNotFound: any;
if (notFoundContent !== undefined) { if (notFoundContent !== undefined) {
mergedNotFound = notFoundContent; mergedNotFound = notFoundContent;
} else if (slots.notFoundContent) { } else if (slots.notFoundContent) {

View File

@ -1,7 +1,7 @@
// mixins for clearfix // mixins for clearfix
// ------------------------ // ------------------------
.clearfix() { .clearfix() {
zoom: 1;
&::before, &::before,
&::after { &::after {
display: table; display: table;

View File

@ -771,6 +771,7 @@
// Tree // Tree
// --- // ---
@tree-bg: @component-background;
@tree-title-height: 24px; @tree-title-height: 24px;
@tree-child-padding: 18px; @tree-child-padding: 18px;
@tree-directory-selected-color: #fff; @tree-directory-selected-color: #fff;

View File

@ -70,4 +70,35 @@ describe('Switch', () => {
}); });
expect(checked.value).toBe(1); expect(checked.value).toBe(1);
}); });
it('customize checked value and children should work', async () => {
resetWarned();
const checked = ref(1);
const onUpdate = val => (checked.value = val);
const wrapper = mount({
render() {
return (
<Switch
{...{ 'onUpdate:checked': onUpdate }}
checked={checked.value}
unCheckedValue={1}
checkedValue={2}
checkedChildren="on"
unCheckedChildren="off"
/>
);
},
});
await asyncExpect(() => {
wrapper.find('button').trigger('click');
});
expect(checked.value).toBe(2);
expect(wrapper.find('.ant-switch-inner').text()).toBe('on');
await asyncExpect(() => {
wrapper.find('button').trigger('click');
});
expect(checked.value).toBe(1);
expect(wrapper.find('.ant-switch-inner').text()).toBe('off');
});
}); });

View File

@ -134,6 +134,7 @@ const Switch = defineComponent({
[`${prefixCls.value}-disabled`]: props.disabled, [`${prefixCls.value}-disabled`]: props.disabled,
[prefixCls.value]: true, [prefixCls.value]: true,
})); }));
return () => ( return () => (
<Wave insertExtraNode> <Wave insertExtraNode>
<button <button
@ -160,7 +161,7 @@ const Switch = defineComponent({
> >
{props.loading ? <LoadingOutlined class={`${prefixCls.value}-loading-icon`} /> : null} {props.loading ? <LoadingOutlined class={`${prefixCls.value}-loading-icon`} /> : null}
<span class={`${prefixCls.value}-inner`}> <span class={`${prefixCls.value}-inner`}>
{checked.value {checkedStatus.value
? getPropsSlot(slots, props, 'checkedChildren') ? getPropsSlot(slots, props, 'checkedChildren')
: getPropsSlot(slots, props, 'unCheckedChildren')} : getPropsSlot(slots, props, 'unCheckedChildren')}
</span> </span>

View File

@ -1,21 +1,56 @@
import type { App, Plugin } from 'vue'; import type { App, ExtractPropTypes, Plugin, PropType } from 'vue';
import { defineComponent, inject } from 'vue'; import { computed, ref, watchEffect } from 'vue';
import VcTreeSelect, { TreeNode, SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from '../vc-tree-select'; import { defineComponent } from 'vue';
import VcTreeSelect, {
TreeNode,
SHOW_ALL,
SHOW_PARENT,
SHOW_CHILD,
treeSelectProps as vcTreeSelectProps,
} from '../vc-tree-select';
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import { TreeSelectProps } from './interface';
import warning from '../_util/warning';
import { getOptionProps, getComponent, getSlot } from '../_util/props-util';
import initDefaultProps from '../_util/props-util/initDefaultProps'; import initDefaultProps from '../_util/props-util/initDefaultProps';
import { defaultConfigProvider } from '../config-provider'; import type { SizeType } from '../config-provider';
import type { DefaultValueType, FieldNames } from '../vc-tree-select/interface';
import omit from '../_util/omit';
import PropTypes from '../_util/vue-types';
import useConfigInject from '../_util/hooks/useConfigInject';
import devWarning from '../vc-util/devWarning';
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';
export { TreeData, TreeSelectProps } from './interface'; const getTransitionName = (rootPrefixCls: string, motion: string, transitionName?: string) => {
import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined'; if (transitionName !== undefined) {
import CaretDownOutlined from '@ant-design/icons-vue/CaretDownOutlined'; return transitionName;
import DownOutlined from '@ant-design/icons-vue/DownOutlined'; }
import CloseOutlined from '@ant-design/icons-vue/CloseOutlined'; return `${rootPrefixCls}-${motion}`;
import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled'; };
import omit from 'omit.js';
import { convertChildrenToData } from './utils'; type RawValue = string | number;
export interface LabeledValue {
key?: string;
value: RawValue;
label: any;
}
export type SelectValue = RawValue | RawValue[] | LabeledValue | LabeledValue[];
export interface RefTreeSelectProps {
focus: () => void;
blur: () => void;
}
export const treeSelectProps = {
...omit(vcTreeSelectProps<DefaultValueType>(), ['showTreeIcon', 'treeMotion', 'inputIcon']),
suffixIcon: PropTypes.any,
size: { type: String as PropType<SizeType> },
bordered: { type: Boolean, default: undefined },
replaceFields: { type: Object as PropType<FieldNames> },
};
export type TreeSelectProps = Partial<ExtractPropTypes<typeof treeSelectProps>>;
const TreeSelect = defineComponent({ const TreeSelect = defineComponent({
TreeNode, TreeNode,
@ -24,176 +59,181 @@ const TreeSelect = defineComponent({
SHOW_CHILD, SHOW_CHILD,
name: 'ATreeSelect', name: 'ATreeSelect',
inheritAttrs: false, inheritAttrs: false,
props: initDefaultProps(TreeSelectProps(), { props: initDefaultProps(treeSelectProps, {
transitionName: 'slide-up', transitionName: 'slide-up',
choiceTransitionName: '', choiceTransitionName: '',
listHeight: 256,
treeIcon: false,
listItemHeight: 26,
bordered: true,
}), }),
setup() { slots: [
return { 'title',
vcTreeSelect: null, 'titleRender',
configProvider: inject('configProvider', defaultConfigProvider), 'placeholder',
}; 'maxTagPlaceholder',
}, 'treeIcon',
created() { 'switcherIcon',
'notFoundContent',
],
setup(props, { attrs, slots, expose, emit }) {
warning( warning(
this.multiple !== false || !this.treeCheckable, !(props.treeData === undefined && slots.default),
'TreeSelect', '`children` of TreeSelect is deprecated. Please use `treeData` instead.',
'`multiple` will alway be `true` when `treeCheckable` is true',
); );
}, watchEffect(() => {
methods: { devWarning(
saveTreeSelect(node: any) { props.multiple !== false || !props.treeCheckable,
this.vcTreeSelect = node; 'TreeSelect',
}, '`multiple` will alway be `true` when `treeCheckable` is true',
focus() { );
this.vcTreeSelect.focus(); devWarning(
}, props.replaceFields === undefined,
'TreeSelect',
blur() { '`replaceFields` is deprecated, please use fieldNames instead',
this.vcTreeSelect.blur(); );
}, });
renderSwitcherIcon(prefixCls: string, { isLeaf, loading }) {
if (loading) {
return <LoadingOutlined class={`${prefixCls}-switcher-loading-icon`} />;
}
if (isLeaf) {
return null;
}
return <CaretDownOutlined class={`${prefixCls}-switcher-icon`} />;
},
handleChange(...args: any[]) {
this.$emit('update:value', args[0]);
this.$emit('change', ...args);
},
handleTreeExpand(...args: any[]) {
this.$emit('update:treeExpandedKeys', args[0]);
this.$emit('treeExpand', ...args);
},
handleSearch(...args: any[]) {
this.$emit('update:searchValue', args[0]);
this.$emit('search', ...args);
},
updateTreeData(treeData: any[]) {
const { $slots } = this;
const defaultFields = {
children: 'children',
title: 'title',
key: 'key',
label: 'label',
value: 'value',
};
const replaceFields = { ...defaultFields, ...this.$props.replaceFields };
return treeData.map(item => {
const { slots = {} } = item;
const label = item[replaceFields.label];
const title = item[replaceFields.title];
const value = item[replaceFields.value];
const key = item[replaceFields.key];
const children = item[replaceFields.children];
let newLabel = typeof label === 'function' ? label() : label;
let newTitle = typeof title === 'function' ? title() : title;
if (!newLabel && slots.label && $slots[slots.label]) {
newLabel = <>{$slots[slots.label](item)}</>;
}
if (!newTitle && slots.title && $slots[slots.title]) {
newTitle = <>{$slots[slots.title](item)}</>;
}
const treeNodeProps = {
...item,
title: newTitle || newLabel,
value,
dataRef: item,
key,
};
if (children) {
return { ...treeNodeProps, children: this.updateTreeData(children) };
}
return treeNodeProps;
});
},
},
render() {
const props: any = getOptionProps(this);
const { const {
prefixCls: customizePrefixCls, configProvider,
size,
dropdownStyle,
dropdownClassName,
getPopupContainer,
...restProps
} = props;
const { class: className } = this.$attrs;
const { renderEmpty, getPrefixCls } = this.configProvider;
const prefixCls = getPrefixCls('select', customizePrefixCls);
const notFoundContent = getComponent(this, 'notFoundContent');
const removeIcon = getComponent(this, 'removeIcon');
const clearIcon = getComponent(this, 'clearIcon');
const { getPopupContainer: getContextPopupContainer } = this.configProvider;
const rest = omit(restProps, [
'inputIcon',
'removeIcon',
'clearIcon',
'switcherIcon',
'suffixIcon',
]);
let suffixIcon = getComponent(this, 'suffixIcon');
suffixIcon = Array.isArray(suffixIcon) ? suffixIcon[0] : suffixIcon;
let treeData = props.treeData;
if (treeData) {
treeData = this.updateTreeData(treeData);
}
const cls = {
[`${prefixCls}-lg`]: size === 'large',
[`${prefixCls}-sm`]: size === 'small',
[className as string]: className,
};
// showSearch: single - false, multiple - true
let { showSearch } = restProps;
if (!('showSearch' in restProps)) {
showSearch = !!(restProps.multiple || restProps.treeCheckable);
}
let checkable = getComponent(this, 'treeCheckable');
if (checkable) {
checkable = <span class={`${prefixCls}-tree-checkbox-inner`} />;
}
const inputIcon = suffixIcon || <DownOutlined class={`${prefixCls}-arrow-icon`} />;
const finalRemoveIcon = removeIcon || <CloseOutlined class={`${prefixCls}-remove-icon`} />;
const finalClearIcon = clearIcon || <CloseCircleFilled class={`${prefixCls}-clear-icon`} />;
const VcTreeSelectProps = {
...this.$attrs,
switcherIcon: nodeProps => this.renderSwitcherIcon(prefixCls, nodeProps),
inputIcon,
removeIcon: finalRemoveIcon,
clearIcon: finalClearIcon,
...rest,
showSearch,
getPopupContainer: getPopupContainer || getContextPopupContainer,
dropdownClassName: classNames(dropdownClassName, `${prefixCls}-tree-dropdown`),
prefixCls, prefixCls,
dropdownStyle: { maxHeight: '100vh', overflow: 'auto', ...dropdownStyle }, renderEmpty,
treeCheckable: checkable, direction,
notFoundContent: notFoundContent || renderEmpty('Select'), virtual,
class: cls, dropdownMatchSelectWidth,
onChange: this.handleChange, size,
onSearch: this.handleSearch, getPopupContainer,
onTreeExpand: this.handleTreeExpand, } = useConfigInject('select', props);
ref: this.saveTreeSelect, const treePrefixCls = computed(() =>
treeData: treeData ? treeData : convertChildrenToData(getSlot(this)), configProvider.getPrefixCls('select-tree', props.prefixCls),
};
return (
<VcTreeSelect
{...VcTreeSelectProps}
v-slots={omit(this.$slots, ['default'])}
__propsSymbol__={[]}
/>
); );
const treeSelectPrefixCls = computed(() =>
configProvider.getPrefixCls('tree-select', props.prefixCls),
);
const mergedDropdownClassName = computed(() =>
classNames(props.dropdownClassName, `${treeSelectPrefixCls.value}-dropdown`, {
[`${treeSelectPrefixCls.value}-dropdown-rtl`]: direction.value === 'rtl',
}),
);
const isMultiple = computed(() => !!(props.treeCheckable || props.multiple));
const treeSelectRef = ref();
expose({
focus() {
treeSelectRef.value.focus?.();
},
blur() {
treeSelectRef.value.blur?.();
},
});
const handleChange = (...args: any[]) => {
emit('update:value', args[0]);
emit('change', ...args);
};
const handleTreeExpand = (...args: any[]) => {
emit('update:treeExpandedKeys', args[0]);
emit('treeExpand', ...args);
};
const handleSearch = (...args: any[]) => {
emit('update:searchValue', args[0]);
emit('search', ...args);
};
return () => {
const {
notFoundContent = slots.notFoundContent?.(),
prefixCls: customizePrefixCls,
bordered,
listHeight,
listItemHeight,
multiple,
treeIcon,
transitionName,
choiceTransitionName,
treeLine,
switcherIcon = slots.switcherIcon?.(),
fieldNames = props.replaceFields,
} = props;
// ===================== Icons =====================
const { suffixIcon, removeIcon, clearIcon } = getIcons(
{
...props,
multiple: isMultiple.value,
prefixCls: prefixCls.value,
},
slots,
);
// ===================== Empty =====================
let mergedNotFound;
if (notFoundContent !== undefined) {
mergedNotFound = notFoundContent;
} else {
mergedNotFound = renderEmpty.value('Select');
}
// ==================== Render =====================
const selectProps = omit(props as typeof props & { itemIcon: any; switcherIcon: any }, [
'suffixIcon',
'itemIcon',
'removeIcon',
'clearIcon',
'switcherIcon',
]);
const mergedClassName = classNames(
!customizePrefixCls && treeSelectPrefixCls.value,
{
[`${prefixCls.value}-lg`]: size.value === 'large',
[`${prefixCls.value}-sm`]: size.value === 'small',
[`${prefixCls.value}-rtl`]: direction.value === 'rtl',
[`${prefixCls.value}-borderless`]: !bordered,
},
attrs.class,
);
const rootPrefixCls = configProvider.getPrefixCls();
const otherProps: any = {};
if (props.treeData === undefined && slots.default) {
otherProps.children = flattenChildren(slots.default());
}
return (
<VcTreeSelect
{...attrs}
virtual={virtual.value}
dropdownMatchSelectWidth={dropdownMatchSelectWidth.value}
{...selectProps}
fieldNames={fieldNames}
ref={treeSelectRef}
prefixCls={prefixCls.value}
class={mergedClassName}
listHeight={listHeight}
listItemHeight={listItemHeight}
inputIcon={suffixIcon}
multiple={multiple}
removeIcon={removeIcon}
clearIcon={clearIcon}
switcherIcon={(nodeProps: AntTreeNodeProps) =>
renderSwitcherIcon(treePrefixCls.value, switcherIcon, treeLine, nodeProps)
}
showTreeIcon={treeIcon as any}
notFoundContent={mergedNotFound}
getPopupContainer={getPopupContainer.value}
treeMotion={null}
dropdownClassName={mergedDropdownClassName.value}
choiceTransitionName={getTransitionName(rootPrefixCls, '', choiceTransitionName)}
transitionName={getTransitionName(rootPrefixCls, 'slide-up', transitionName)}
onChange={handleChange}
onSearch={handleSearch}
onTreeExpand={handleTreeExpand}
v-slots={{
...slots,
treeCheckable: () => <span class={`${prefixCls.value}-tree-checkbox-inner`} />,
}}
{...otherProps}
/>
);
};
}, },
}); });

View File

@ -1,65 +0,0 @@
import PropTypes, { withUndefined } from '../_util/vue-types';
import { SelectProps } from '../select';
import { tuple } from '../_util/type';
export const TreeData = PropTypes.shape({
key: PropTypes.string,
value: PropTypes.string,
label: PropTypes.VNodeChild,
slots: PropTypes.object,
children: PropTypes.array,
}).loose;
export const TreeSelectProps = () => ({
...SelectProps(),
autofocus: PropTypes.looseBool,
dropdownStyle: PropTypes.object,
filterTreeNode: withUndefined(PropTypes.oneOfType([Function, Boolean])),
getPopupContainer: PropTypes.func,
labelInValue: PropTypes.looseBool,
loadData: PropTypes.func,
maxTagCount: PropTypes.number,
maxTagPlaceholder: PropTypes.VNodeChild,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
PropTypes.array,
PropTypes.number,
]),
defaultValue: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
PropTypes.array,
PropTypes.number,
]),
multiple: PropTypes.looseBool,
notFoundContent: PropTypes.VNodeChild,
searchPlaceholder: PropTypes.string,
searchValue: PropTypes.string,
showCheckedStrategy: PropTypes.oneOf(tuple('SHOW_ALL', 'SHOW_PARENT', 'SHOW_CHILD')),
suffixIcon: PropTypes.VNodeChild,
treeCheckable: PropTypes.looseBool,
treeCheckStrictly: PropTypes.looseBool,
treeData: PropTypes.arrayOf(Object),
treeDataSimpleMode: withUndefined(PropTypes.oneOfType([PropTypes.looseBool, Object])),
dropdownClassName: PropTypes.string,
dropdownMatchSelectWidth: PropTypes.looseBool,
treeDefaultExpandAll: PropTypes.looseBool,
treeExpandedKeys: PropTypes.array,
treeIcon: PropTypes.looseBool,
treeDefaultExpandedKeys: PropTypes.array,
treeNodeFilterProp: PropTypes.string,
treeNodeLabelProp: PropTypes.string,
replaceFields: PropTypes.object.def({}),
clearIcon: PropTypes.VNodeChild,
removeIcon: PropTypes.VNodeChild,
onSelect: PropTypes.func,
onChange: PropTypes.func,
onSearch: PropTypes.func,
onTreeExpand: PropTypes.func,
'onUpdate:treeExpandedKeys': PropTypes.func,
'onUpdate:searchValue': PropTypes.func,
'onUpdate:value': PropTypes.func,
});

View File

@ -3,189 +3,57 @@
@import '../../tree/style/mixin'; @import '../../tree/style/mixin';
@import '../../checkbox/style/mixin'; @import '../../checkbox/style/mixin';
@select-prefix-cls: ~'@{ant-prefix}-select'; @tree-select-prefix-cls: ~'@{ant-prefix}-tree-select';
@select-tree-prefix-cls: ~'@{ant-prefix}-select-tree'; @select-tree-prefix-cls: ~'@{ant-prefix}-select-tree';
.antCheckboxFn(@checkbox-prefix-cls: ~'@{ant-prefix}-select-tree-checkbox'); .antCheckboxFn(@checkbox-prefix-cls: ~'@{select-tree-prefix-cls}-checkbox');
.@{tree-select-prefix-cls} {
// ======================= Dropdown =======================
&-dropdown {
padding: @padding-xs (@padding-xs / 2) 0;
&-rtl {
direction: rtl;
}
// ======================== Tree ========================
.@{select-tree-prefix-cls} {
border-radius: 0;
&-list-holder-inner {
align-items: stretch;
.@{select-tree-prefix-cls}-treenode {
padding-bottom: @padding-xs;
.@{select-tree-prefix-cls}-node-content-wrapper {
flex: auto;
}
}
}
}
}
}
.@{select-tree-prefix-cls} { .@{select-tree-prefix-cls} {
.reset-component(); .antTreeFn(@select-tree-prefix-cls);
margin: 0; // change switcher icon rotation in rtl direction
margin-top: -4px; & &-switcher {
padding: 0 4px; &_close {
li { .@{select-tree-prefix-cls}-switcher-icon {
margin: 8px 0; svg {
padding: 0; .@{tree-select-prefix-cls}-dropdown-rtl & {
white-space: nowrap; transform: rotate(90deg);
list-style: none;
outline: 0;
&.filter-node {
> span {
font-weight: 500;
}
}
ul {
margin: 0;
padding: 0 0 0 18px;
}
.@{select-tree-prefix-cls}-node-content-wrapper {
display: inline-block;
width: ~'calc(100% - 24px)';
margin: 0;
padding: 3px 5px;
color: @text-color;
text-decoration: none;
border-radius: @border-radius-sm;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: @item-hover-bg;
}
&.@{select-tree-prefix-cls}-node-selected {
background-color: @primary-2;
}
}
span {
&.@{select-tree-prefix-cls}-checkbox {
margin: 0 4px 0 0;
+ .@{select-tree-prefix-cls}-node-content-wrapper {
width: ~'calc(100% - 46px)';
}
}
&.@{select-tree-prefix-cls}-switcher,
&.@{select-tree-prefix-cls}-iconEle {
display: inline-block;
width: 24px;
height: 24px;
margin: 0;
line-height: 22px;
text-align: center;
vertical-align: middle;
border: 0 none;
outline: none;
cursor: pointer;
}
&.@{select-prefix-cls}-icon_loading {
.@{select-prefix-cls}-switcher-loading-icon {
position: absolute;
left: 0;
display: inline-block;
color: @primary-color;
font-size: 14px;
transform: none;
svg {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}
}
}
&.@{select-tree-prefix-cls}-switcher {
position: relative;
&.@{select-tree-prefix-cls}-switcher-noop {
cursor: auto;
}
&.@{select-tree-prefix-cls}-switcher_open {
.antTreeSwitcherIcon();
}
&.@{select-tree-prefix-cls}-switcher_close {
.antTreeSwitcherIcon();
.@{select-prefix-cls}-switcher-icon {
svg {
transform: rotate(-90deg);
}
}
}
&.@{select-tree-prefix-cls}-switcher_open,
&.@{select-tree-prefix-cls}-switcher_close {
.@{select-prefix-cls}-switcher-loading-icon {
position: absolute;
left: 0;
display: inline-block;
width: 24px;
height: 24px;
color: @primary-color;
font-size: 14px;
transform: none;
svg {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}
} }
} }
} }
} }
}
.@{select-tree-prefix-cls}-treenode-loading { &-loading-icon {
.@{select-tree-prefix-cls}-iconEle { .@{tree-select-prefix-cls}-dropdown-rtl & {
display: none; transform: scaleY(-1);
}
} }
} }
&-child-tree {
display: none;
&-open {
display: block;
}
}
li&-treenode-disabled {
> span:not(.@{select-tree-prefix-cls}-switcher),
> .@{select-tree-prefix-cls}-node-content-wrapper,
> .@{select-tree-prefix-cls}-node-content-wrapper span {
color: @disabled-color;
cursor: not-allowed;
}
> .@{select-tree-prefix-cls}-node-content-wrapper:hover {
background: transparent;
}
}
&-icon__open {
margin-right: 2px;
vertical-align: top;
}
&-icon__close {
margin-right: 2px;
vertical-align: top;
}
}
.@{select-prefix-cls}-tree-dropdown {
.reset-component();
.@{select-prefix-cls}-dropdown-search {
position: sticky;
top: 0;
z-index: 1;
display: block;
padding: 4px;
background: @component-background;
.@{select-prefix-cls}-search__field__wrap {
width: 100%;
}
.@{select-prefix-cls}-search__field {
box-sizing: border-box;
width: 100%;
padding: 4px 7px;
border: @border-width-base @border-style-base @border-color-base;
border-radius: 4px;
outline: none;
}
&.@{select-prefix-cls}-search--hide {
display: none;
}
}
.@{select-prefix-cls}-not-found {
display: block;
padding: 7px 16px;
color: @disabled-color;
cursor: not-allowed;
}
} }

View File

@ -2,6 +2,6 @@ import '../../style/index.less';
import './index.less'; import './index.less';
// style dependencies // style dependencies
// deps-lint-skip: select // deps-lint-skip: tree
import '../../select/style'; import '../../select/style';
import '../../empty/style'; import '../../empty/style';

View File

@ -1,37 +1,33 @@
import type { VNode } from 'vue'; import type { ExtractPropTypes, PropType } from 'vue';
import { defineComponent, inject } from 'vue'; import { nextTick, onUpdated, ref, watch } from 'vue';
import omit from 'omit.js'; import { defineComponent } from 'vue';
import debounce from 'lodash-es/debounce'; import debounce from 'lodash-es/debounce';
import FolderOpenOutlined from '@ant-design/icons-vue/FolderOpenOutlined'; import FolderOpenOutlined from '@ant-design/icons-vue/FolderOpenOutlined';
import FolderOutlined from '@ant-design/icons-vue/FolderOutlined'; import FolderOutlined from '@ant-design/icons-vue/FolderOutlined';
import FileOutlined from '@ant-design/icons-vue/FileOutlined'; import FileOutlined from '@ant-design/icons-vue/FileOutlined';
import PropTypes from '../_util/vue-types';
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import { conductExpandParent, convertTreeToEntities } from '../vc-tree/src/util'; import type { AntdTreeNodeAttribute } from './Tree';
import type { CheckEvent, ExpendEvent, SelectEvent } from './Tree'; import { treeProps } from './Tree';
import Tree, { TreeProps } from './Tree'; import type { TreeProps } from './Tree';
import { import Tree from './Tree';
calcRangeKeys,
getFullKeyList,
convertDirectoryKeysToNodes,
getFullKeyListByTreeData,
} from './util';
import BaseMixin from '../_util/BaseMixin';
import { getOptionProps, getComponent, getSlot } from '../_util/props-util';
import initDefaultProps from '../_util/props-util/initDefaultProps'; import initDefaultProps from '../_util/props-util/initDefaultProps';
import { defaultConfigProvider } from '../config-provider'; import { convertDataToEntities, convertTreeToData } from '../vc-tree/utils/treeUtil';
import type { DataNode, EventDataNode, Key } from '../vc-tree/interface';
import { conductExpandParent } from '../vc-tree/util';
import { calcRangeKeys, convertDirectoryKeysToNodes } from './utils/dictUtil';
import useConfigInject from '../_util/hooks/useConfigInject';
import { filterEmpty } from '../_util/props-util';
// export type ExpandAction = false | 'click' | 'dblclick'; export interface export type ExpandAction = false | 'click' | 'doubleclick' | 'dblclick';
// DirectoryTreeProps extends TreeProps { expandAction?: ExpandAction; }
// export interface DirectoryTreeState { expandedKeys?: string[];
// selectedKeys?: string[]; }
export interface DirectoryTreeState { const directoryTreeProps = {
_expandedKeys?: (string | number)[]; ...treeProps(),
_selectedKeys?: (string | number)[]; expandAction: { type: [Boolean, String] as PropType<ExpandAction> },
} };
function getIcon(props: { isLeaf: boolean; expanded: boolean } & VNode) { export type DirectoryTreeProps = Partial<ExtractPropTypes<typeof directoryTreeProps>>;
function getIcon(props: AntdTreeNodeAttribute) {
const { isLeaf, expanded } = props; const { isLeaf, expanded } = props;
if (isLeaf) { if (isLeaf) {
return <FileOutlined />; return <FileOutlined />;
@ -41,211 +37,239 @@ function getIcon(props: { isLeaf: boolean; expanded: boolean } & VNode) {
export default defineComponent({ export default defineComponent({
name: 'ADirectoryTree', name: 'ADirectoryTree',
mixins: [BaseMixin],
inheritAttrs: false, inheritAttrs: false,
props: initDefaultProps( props: initDefaultProps(directoryTreeProps, {
{ showIcon: true,
...TreeProps(), expandAction: 'click',
expandAction: PropTypes.oneOf([false, 'click', 'doubleclick', 'dblclick']), }),
}, slots: ['icon', 'title', 'switcherIcon', 'titleRender'],
{ emits: [
showIcon: true, 'update:selectedKeys',
expandAction: 'click', 'update:checkedKeys',
}, 'update:expandedKeys',
), 'expand',
setup() { 'select',
return { 'check',
children: null, 'doubleclick',
onDebounceExpand: null, 'dblclick',
tree: null, 'click',
lastSelectedKey: '', ],
cachedSelectedKeys: [], setup(props, { attrs, slots, emit }) {
configProvider: inject('configProvider', defaultConfigProvider), // convertTreeToData a-tree-node a-tree-noderender treeData
}; const treeData = ref<DataNode[]>(
}, props.treeData || convertTreeToData(filterEmpty(slots.default?.())),
data() { );
const props = getOptionProps(this); watch(
const { defaultExpandAll, defaultExpandParent, expandedKeys, defaultExpandedKeys } = props; () => props.treeData,
const children = getSlot(this); () => {
const { keyEntities } = convertTreeToEntities(children); treeData.value = props.treeData;
const state: DirectoryTreeState = {}; },
// Selected keys );
state._selectedKeys = props.selectedKeys || props.defaultSelectedKeys || []; onUpdated(() => {
nextTick(() => {
if (props.treeData === undefined && slots.default) {
treeData.value = convertTreeToData(filterEmpty(slots.default?.()));
}
});
});
// Shift click usage
const lastSelectedKey = ref<Key>();
// Expanded keys const cachedSelectedKeys = ref<Key[]>();
if (defaultExpandAll) {
if (props.treeData) {
state._expandedKeys = getFullKeyListByTreeData(props.treeData, props.replaceFields);
} else {
state._expandedKeys = getFullKeyList(children);
}
} else if (defaultExpandParent) {
state._expandedKeys = conductExpandParent(expandedKeys || defaultExpandedKeys, keyEntities);
} else {
state._expandedKeys = expandedKeys || defaultExpandedKeys;
}
return {
_selectedKeys: [],
_expandedKeys: [],
...state,
};
},
watch: {
expandedKeys(val) {
this.setState({ _expandedKeys: val });
},
selectedKeys(val) {
this.setState({ _selectedKeys: val });
},
},
created() {
this.onDebounceExpand = debounce(this.expandFolderNode, 200, { leading: true });
},
methods: {
handleExpand(expandedKeys: (string | number)[], info: ExpendEvent) {
this.setUncontrolledState({ _expandedKeys: expandedKeys });
this.$emit('update:expandedKeys', expandedKeys);
this.$emit('expand', expandedKeys, info);
return undefined; const treeRef = ref();
},
handleClick(event: MouseEvent, node: VNode) { const getInitExpandedKeys = () => {
const { expandAction } = this.$props; const { keyEntities } = convertDataToEntities(treeData.value);
// Expand the tree let initExpandedKeys: any;
if (expandAction === 'click') {
this.onDebounceExpand(event, node);
}
this.$emit('click', event, node);
},
handleDoubleClick(event: MouseEvent, node: VNode) { // Expanded keys
const { expandAction } = this.$props; if (props.defaultExpandAll) {
initExpandedKeys = Object.keys(keyEntities);
// Expand the tree } else if (props.defaultExpandParent) {
if (expandAction === 'dblclick' || expandAction === 'doubleclick') { initExpandedKeys = conductExpandParent(
this.onDebounceExpand(event, node); props.expandedKeys || props.defaultExpandedKeys,
} keyEntities,
this.$emit('doubleclick', event, node);
this.$emit('dblclick', event, node);
},
hanldeSelect(keys: (string | number)[], event: SelectEvent) {
const { multiple } = this.$props;
const children = this.children || [];
const { _expandedKeys: expandedKeys = [] } = this.$data;
const { node, nativeEvent } = event;
const { eventKey = '' } = node;
const newState: DirectoryTreeState = {};
// We need wrap this event since some value is not same
const newEvent = {
...event,
selected: true, // Directory selected always true
};
// Windows / Mac single pick
const ctrlPick = nativeEvent.ctrlKey || nativeEvent.metaKey;
const shiftPick = nativeEvent.shiftKey;
// Generate new selected keys
let newSelectedKeys: (string | number)[];
if (multiple && ctrlPick) {
// Control click
newSelectedKeys = keys;
this.lastSelectedKey = eventKey;
this.cachedSelectedKeys = newSelectedKeys;
newEvent.selectedNodes = convertDirectoryKeysToNodes(children, newSelectedKeys);
} else if (multiple && shiftPick) {
// Shift click
newSelectedKeys = Array.from(
new Set([
...(this.cachedSelectedKeys || []),
...calcRangeKeys(children, expandedKeys, eventKey, this.lastSelectedKey),
]),
); );
newEvent.selectedNodes = convertDirectoryKeysToNodes(children, newSelectedKeys);
} else { } else {
// Single click initExpandedKeys = props.expandedKeys || props.defaultExpandedKeys;
newSelectedKeys = [eventKey];
this.lastSelectedKey = eventKey;
this.cachedSelectedKeys = newSelectedKeys;
newEvent.selectedNodes = [event.node];
} }
newState._selectedKeys = newSelectedKeys; return initExpandedKeys;
};
this.$emit('update:selectedKeys', newSelectedKeys); const selectedKeys = ref(props.selectedKeys || props.defaultSelectedKeys || []);
this.$emit('select', newSelectedKeys, newEvent);
this.setUncontrolledState(newState); const expandedKeys = ref<Key[]>(getInitExpandedKeys());
},
setTreeRef(node: VNode) {
this.tree = node;
},
expandFolderNode(event: MouseEvent, node: { isLeaf: boolean } & VNode) { watch(
() => props.selectedKeys,
() => {
if (props.selectedKeys !== undefined) {
selectedKeys.value = props.selectedKeys;
}
},
{ immediate: true },
);
watch(
() => props.expandedKeys,
() => {
if (props.expandedKeys !== undefined) {
expandedKeys.value = props.expandedKeys;
}
},
{ immediate: true },
);
const expandFolderNode = (event: MouseEvent, node: any) => {
const { isLeaf } = node; const { isLeaf } = node;
if (isLeaf || event.shiftKey || event.metaKey || event.ctrlKey) { if (isLeaf || event.shiftKey || event.metaKey || event.ctrlKey) {
return; return;
} }
// Call internal rc-tree expand function
if (this.tree.tree) { // https://github.com/ant-design/ant-design/issues/12567
// Get internal vc-tree treeRef.value!.onNodeExpand(event as any, node);
const internalTree = this.tree.tree; };
const onDebounceExpand = debounce(expandFolderNode, 200, {
// Call internal rc-tree expand function leading: true,
// https://github.com/ant-design/ant-design/issues/12567 });
internalTree.onNodeExpand(event, node); const onExpand = (
} keys: Key[],
}, info: {
node: EventDataNode;
setUncontrolledState(state: unknown) { expanded: boolean;
const newState = omit( nativeEvent: MouseEvent;
state, },
Object.keys(getOptionProps(this)).map(p => `_${p}`), ) => {
); if (props.expandedKeys === undefined) {
if (Object.keys(newState).length) { expandedKeys.value = keys;
this.setState(newState); }
} // Call origin function
}, emit('update:expandedKeys', keys);
handleCheck(checkedObj: (string | number)[], eventObj: CheckEvent) { emit('expand', keys, info);
this.$emit('update:checkedKeys', checkedObj); };
this.$emit('check', checkedObj, eventObj);
}, const onClick = (event: MouseEvent, node: EventDataNode) => {
}, const { expandAction } = props;
render() { // Expand the tree
this.children = getSlot(this); if (expandAction === 'click') {
const { prefixCls: customizePrefixCls, ...props } = getOptionProps(this); onDebounceExpand(event, node);
const getPrefixCls = this.configProvider.getPrefixCls; }
const prefixCls = getPrefixCls('tree', customizePrefixCls); emit('click', event, node);
const { _expandedKeys: expandedKeys, _selectedKeys: selectedKeys } = this.$data; };
const { class: className, ...restAttrs } = this.$attrs;
const connectClassName = classNames(`${prefixCls}-directory`, className); const onDoubleClick = (event: MouseEvent, node: EventDataNode) => {
const treeProps = { const { expandAction } = props;
icon: getIcon, // Expand the tree
...restAttrs, if (expandAction === 'dblclick' || expandAction === 'doubleclick') {
...omit(props, ['onUpdate:selectedKeys', 'onUpdate:checkedKeys', 'onUpdate:expandedKeys']), onDebounceExpand(event, node);
prefixCls, }
expandedKeys,
selectedKeys, emit('doubleclick', event, node);
switcherIcon: getComponent(this, 'switcherIcon'), emit('dblclick', event, node);
ref: this.setTreeRef, };
class: connectClassName,
onSelect: this.hanldeSelect, const onSelect = (
onClick: this.handleClick, keys: Key[],
onDblclick: this.handleDoubleClick, event: {
onExpand: this.handleExpand, event: 'select';
onCheck: this.handleCheck, selected: boolean;
node: any;
selectedNodes: DataNode[];
nativeEvent: MouseEvent;
},
) => {
const { multiple } = props;
const { node, nativeEvent } = event;
const { key = '' } = node;
// const newState: DirectoryTreeState = {};
// We need wrap this event since some value is not same
const newEvent: any = {
...event,
selected: true, // Directory selected always true
};
// Windows / Mac single pick
const ctrlPick: boolean = nativeEvent.ctrlKey || nativeEvent.metaKey;
const shiftPick: boolean = nativeEvent.shiftKey;
// Generate new selected keys
let newSelectedKeys: Key[];
if (multiple && ctrlPick) {
// Control click
newSelectedKeys = keys;
lastSelectedKey.value = key;
cachedSelectedKeys.value = newSelectedKeys;
newEvent.selectedNodes = convertDirectoryKeysToNodes(treeData.value, newSelectedKeys);
} else if (multiple && shiftPick) {
// Shift click
newSelectedKeys = Array.from(
new Set([
...(cachedSelectedKeys.value || []),
...calcRangeKeys({
treeData: treeData.value,
expandedKeys: expandedKeys.value,
startKey: key,
endKey: lastSelectedKey.value,
}),
]),
);
newEvent.selectedNodes = convertDirectoryKeysToNodes(treeData.value, newSelectedKeys);
} else {
// Single click
newSelectedKeys = [key];
lastSelectedKey.value = key;
cachedSelectedKeys.value = newSelectedKeys;
newEvent.selectedNodes = convertDirectoryKeysToNodes(treeData.value, newSelectedKeys);
}
emit('update:selectedKeys', newSelectedKeys);
emit('select', newSelectedKeys, newEvent);
if (props.selectedKeys === undefined) {
selectedKeys.value = newSelectedKeys;
}
};
const onCheck: TreeProps['onCheck'] = (checkedObjOrKeys, eventObj) => {
emit('update:checkedKeys', checkedObjOrKeys);
emit('check', checkedObjOrKeys, eventObj);
};
const { prefixCls, direction } = useConfigInject('tree', props);
return () => {
const connectClassName = classNames(
`${prefixCls.value}-directory`,
{
[`${prefixCls.value}-directory-rtl`]: direction.value === 'rtl',
},
attrs.class,
);
const { icon = slots.icon, blockNode = true, ...otherProps } = props;
return (
<Tree
{...attrs}
icon={icon || getIcon}
ref={treeRef}
blockNode={blockNode}
{...otherProps}
prefixCls={prefixCls.value}
class={connectClassName}
expandedKeys={expandedKeys.value}
selectedKeys={selectedKeys.value}
onSelect={onSelect}
onClick={onClick}
onDblclick={onDoubleClick}
onExpand={onExpand}
onCheck={onCheck}
v-slots={slots}
/>
);
}; };
return (
<Tree {...treeProps} v-slots={omit(this.$slots, ['default'])}>
{this.children}
</Tree>
);
}, },
}); });

View File

@ -1,290 +1,248 @@
import type { VNode, PropType, CSSProperties } from 'vue'; import type { PropType, ExtractPropTypes } from 'vue';
import { defineComponent, inject } from 'vue'; import { watchEffect } from 'vue';
import { ref } from 'vue';
import { defineComponent } from 'vue';
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined'; import VcTree, { TreeNode } from '../vc-tree';
import FileOutlined from '@ant-design/icons-vue/FileOutlined';
import CaretDownFilled from '@ant-design/icons-vue/CaretDownFilled';
import MinusSquareOutlined from '@ant-design/icons-vue/MinusSquareOutlined';
import PlusSquareOutlined from '@ant-design/icons-vue/PlusSquareOutlined';
import VcTree from '../vc-tree';
import animation from '../_util/openAnimation';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import { getOptionProps, getComponent, getSlot } from '../_util/props-util'; import { filterEmpty } from '../_util/props-util';
import initDefaultProps from '../_util/props-util/initDefaultProps'; import initDefaultProps from '../_util/props-util/initDefaultProps';
import { cloneElement } from '../_util/vnode'; import type { DataNode, EventDataNode, FieldNames, Key } from '../vc-tree/interface';
import { defaultConfigProvider } from '../config-provider'; import type { TreeNodeProps } from '../vc-tree/props';
import { treeProps as vcTreeProps } from '../vc-tree/props';
import useConfigInject from '../_util/hooks/useConfigInject';
import renderSwitcherIcon from './utils/iconUtil';
import dropIndicatorRender from './utils/dropIndicator';
import devWarning from '../vc-util/devWarning';
import { warning } from '../vc-util/warning';
const TreeNode = VcTree.TreeNode; export interface AntdTreeNodeAttribute {
eventKey: string;
export interface TreeDataItem { prefixCls: string;
key?: string | number; class: string;
title?: string;
isLeaf?: boolean;
selectable?: boolean;
children?: TreeDataItem[];
disableCheckbox?: boolean;
disabled?: boolean;
class?: string;
style?: CSSProperties;
checkable?: boolean;
icon?: VNode;
slots?: Record<string, string>;
switcherIcon?: VNode;
// support custom field
[key: string]: any;
}
interface DefaultEvent {
nativeEvent: MouseEvent;
node: Record<string, any>;
}
export interface CheckEvent extends DefaultEvent {
checked: boolean;
checkedNodes: Array<Record<string, any>>;
checkedNodesPositions: { node: Record<string, any>; pos: string | number }[];
event: string;
halfCheckedKeys: (string | number)[];
}
export interface ExpendEvent extends DefaultEvent {
expanded: boolean; expanded: boolean;
}
export interface SelectEvent extends DefaultEvent {
event: string;
selected: boolean; selected: boolean;
selectedNodes: Array<Record<string, any>>; checked: boolean;
halfChecked: boolean;
children: any;
title: any;
pos: string;
dragOver: boolean;
dragOverGapTop: boolean;
dragOverGapBottom: boolean;
isLeaf: boolean;
selectable: boolean;
disabled: boolean;
disableCheckbox: boolean;
} }
export interface TreeDragEvent { export type AntTreeNodeProps = TreeNodeProps;
// [Legacy] Compatible for v2
export type TreeDataItem = DataNode;
export interface AntTreeNodeBaseEvent {
node: EventDataNode;
nativeEvent: MouseEvent;
}
export interface AntTreeNodeCheckedEvent extends AntTreeNodeBaseEvent {
event: 'check';
checked?: boolean;
checkedNodes?: DataNode[];
}
export interface AntTreeNodeSelectedEvent extends AntTreeNodeBaseEvent {
event: 'select';
selected?: boolean;
selectedNodes?: DataNode[];
}
export interface AntTreeNodeExpandedEvent extends AntTreeNodeBaseEvent {
expanded?: boolean;
}
export interface AntTreeNodeMouseEvent {
node: EventDataNode;
event: DragEvent; event: DragEvent;
expandedKeys: (string | number)[];
node: Record<string, any>;
} }
export interface DropEvent { export interface AntTreeNodeDragEnterEvent extends AntTreeNodeMouseEvent {
dragNode: Record<string, any>; expandedKeys: Key[];
dragNodesKeys: (string | number)[]; }
export interface AntTreeNodeDropEvent {
node: EventDataNode;
dragNode: EventDataNode;
dragNodesKeys: Key[];
dropPosition: number; dropPosition: number;
dropToGap: boolean; dropToGap?: boolean;
event: DragEvent; event: MouseEvent;
node: Record<string, any>;
} }
function TreeProps() { export const treeProps = () => {
return { return {
showLine: PropTypes.looseBool, ...vcTreeProps(),
showLine: { type: Boolean, default: undefined },
/** 是否支持多选 */ /** 是否支持多选 */
multiple: PropTypes.looseBool, multiple: { type: Boolean, default: undefined },
/** 是否自动展开父节点 */ /** 是否自动展开父节点 */
autoExpandParent: PropTypes.looseBool, autoExpandParent: { type: Boolean, default: undefined },
/** checkable状态下节点选择完全受控父子节点选中状态不再关联*/ /** checkable状态下节点选择完全受控父子节点选中状态不再关联*/
checkStrictly: PropTypes.looseBool, checkStrictly: { type: Boolean, default: undefined },
/** 是否支持选中 */ /** 是否支持选中 */
checkable: PropTypes.looseBool, checkable: { type: Boolean, default: undefined },
/** 是否禁用树 */ /** 是否禁用树 */
disabled: PropTypes.looseBool, disabled: { type: Boolean, default: undefined },
/** 默认展开所有树节点 */ /** 默认展开所有树节点 */
defaultExpandAll: PropTypes.looseBool, defaultExpandAll: { type: Boolean, default: undefined },
/** 默认展开对应树节点 */ /** 默认展开对应树节点 */
defaultExpandParent: PropTypes.looseBool, defaultExpandParent: { type: Boolean, default: undefined },
/** 默认展开指定的树节点 */ /** 默认展开指定的树节点 */
defaultExpandedKeys: PropTypes.arrayOf( defaultExpandedKeys: { type: Array as PropType<Key[]> },
PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
),
/** (受控)展开指定的树节点 */ /** (受控)展开指定的树节点 */
expandedKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), expandedKeys: { type: Array as PropType<Key[]> },
/** (受控)选中复选框的树节点 */ /** (受控)选中复选框的树节点 */
checkedKeys: PropTypes.oneOfType([ checkedKeys: {
PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), type: [Array, Object] as PropType<Key[] | { checked: Key[]; halfChecked: Key[] }>,
PropTypes.shape({ },
checked: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
halfChecked: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
}).loose,
]),
/** 默认选中复选框的树节点 */ /** 默认选中复选框的树节点 */
defaultCheckedKeys: PropTypes.arrayOf( defaultCheckedKeys: { type: Array as PropType<Key[]> },
PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
),
/** (受控)设置选中的树节点 */ /** (受控)设置选中的树节点 */
selectedKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), selectedKeys: { type: Array as PropType<Key[]> },
/** 默认选中的树节点 */ /** 默认选中的树节点 */
defaultSelectedKeys: PropTypes.arrayOf( defaultSelectedKeys: { type: Array as PropType<Key[]> },
PropTypes.oneOfType([PropTypes.string, PropTypes.number]), selectable: { type: Boolean, default: undefined },
),
selectable: PropTypes.looseBool,
/** filter some AntTreeNodes as you need. it should return true */ loadedKeys: { type: Array as PropType<Key[]> },
filterAntTreeNode: PropTypes.func, draggable: { type: Boolean, default: undefined },
/** 异步加载数据 */ showIcon: { type: Boolean, default: undefined },
loadData: PropTypes.func, icon: { type: Function as PropType<(nodeProps: AntdTreeNodeAttribute) => any> },
loadedKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
// onLoaded: (loadedKeys: string[], info: { event: 'load', node: AntTreeNode; }) => void,
/** 响应右键点击 */
// onRightClick: (options: AntTreeNodeMouseEvent) => void,
/** 设置节点可拖拽IE>8*/
draggable: PropTypes.looseBool,
// /** */
// onDragStart: (options: AntTreeNodeMouseEvent) => void,
// /** dragenter */
// onDragEnter: (options: AntTreeNodeMouseEvent) => void,
// /** dragover */
// onDragOver: (options: AntTreeNodeMouseEvent) => void,
// /** dragleave */
// onDragLeave: (options: AntTreeNodeMouseEvent) => void,
// /** drop */
// onDrop: (options: AntTreeNodeMouseEvent) => void,
showIcon: PropTypes.looseBool,
icon: PropTypes.func,
switcherIcon: PropTypes.any, switcherIcon: PropTypes.any,
prefixCls: PropTypes.string, prefixCls: PropTypes.string,
filterTreeNode: PropTypes.func,
openAnimation: PropTypes.any,
treeData: {
type: Array as PropType<TreeDataItem[]>,
},
/** /**
* @default{title,key,children} * @default{title,key,children}
* deprecated, please use `fieldNames` instead
* 替换treeNode中 title,key,children字段为treeData中对应的字段 * 替换treeNode中 title,key,children字段为treeData中对应的字段
*/ */
replaceFields: PropTypes.object, replaceFields: { type: Object as PropType<FieldNames> },
blockNode: PropTypes.looseBool, blockNode: { type: Boolean, default: undefined },
/** 展开/收起节点时触发 */ openAnimation: PropTypes.any,
onExpand: PropTypes.func,
/** 点击复选框触发 */
onCheck: PropTypes.func,
/** 点击树节点触发 */
onSelect: PropTypes.func,
/** 单击树节点触发 */
onClick: PropTypes.func,
/** 双击树节点触发 */
onDoubleclick: PropTypes.func,
onDblclick: PropTypes.func,
'onUpdate:selectedKeys': PropTypes.func,
'onUpdate:checkedKeys': PropTypes.func,
'onUpdate:expandedKeys': PropTypes.func,
}; };
} };
export { TreeProps }; export type TreeProps = Partial<ExtractPropTypes<ReturnType<typeof treeProps>>>;
export default defineComponent({ export default defineComponent({
name: 'ATree', name: 'ATree',
inheritAttrs: false, inheritAttrs: false,
props: initDefaultProps(TreeProps(), { props: initDefaultProps(treeProps(), {
checkable: false, checkable: false,
selectable: true,
showIcon: false, showIcon: false,
openAnimation: {
...animation,
appear: null,
},
blockNode: false, blockNode: false,
}), }),
setup() { slots: ['icon', 'title', 'switcherIcon', 'titleRender'],
return { emits: [
tree: null, 'update:selectedKeys',
configProvider: inject('configProvider', defaultConfigProvider), 'update:checkedKeys',
'update:expandedKeys',
'expand',
'select',
'check',
'doubleclick',
'dblclick',
],
TreeNode,
setup(props, { attrs, expose, emit, slots }) {
warning(
!(props.treeData === undefined && slots.default),
'`children` of Tree is deprecated. Please use `treeData` instead.',
);
const { prefixCls, direction, virtual } = useConfigInject('tree', props);
const treeRef = ref();
expose({
treeRef,
onNodeExpand: (...args) => {
treeRef.value?.onNodeExpand(...args);
},
});
watchEffect(() => {
devWarning(
props.replaceFields === undefined,
'Tree',
'`replaceFields` is deprecated, please use fieldNames instead',
);
});
const handleCheck: TreeProps['onCheck'] = (checkedObjOrKeys, eventObj) => {
emit('update:checkedKeys', checkedObjOrKeys);
emit('check', checkedObjOrKeys, eventObj);
};
const handleExpand: TreeProps['onExpand'] = (expandedKeys, eventObj) => {
emit('update:expandedKeys', expandedKeys);
emit('expand', expandedKeys, eventObj);
};
const handleSelect: TreeProps['onSelect'] = (selectedKeys, eventObj) => {
emit('update:selectedKeys', selectedKeys);
emit('select', selectedKeys, eventObj);
};
return () => {
const {
showIcon,
showLine,
switcherIcon = slots.switcherIcon,
icon = slots.icon,
blockNode,
checkable,
selectable,
fieldNames = props.replaceFields,
motion = props.openAnimation,
itemHeight = 20,
} = props;
const newProps = {
...attrs,
...props,
showLine: Boolean(showLine),
dropIndicatorRender,
fieldNames,
icon,
itemHeight,
};
return (
<VcTree
{...newProps}
virtual={virtual.value}
motion={motion}
ref={treeRef}
prefixCls={prefixCls.value}
class={classNames(
{
[`${prefixCls.value}-icon-hide`]: !showIcon,
[`${prefixCls.value}-block-node`]: blockNode,
[`${prefixCls.value}-unselectable`]: !selectable,
[`${prefixCls.value}-rtl`]: direction.value === 'rtl',
},
attrs.class,
)}
direction={direction.value}
checkable={checkable}
selectable={selectable}
switcherIcon={(nodeProps: AntTreeNodeProps) =>
renderSwitcherIcon(prefixCls.value, switcherIcon, showLine, nodeProps)
}
onCheck={handleCheck}
onExpand={handleExpand}
onSelect={handleSelect}
v-slots={{
...slots,
checkable: () => <span class={`${prefixCls.value}-checkbox-inner`} />,
}}
children={filterEmpty(slots.default?.())}
></VcTree>
);
}; };
}, },
TreeNode,
methods: {
renderSwitcherIcon(prefixCls: string, switcherIcon: VNode, { isLeaf, loading, expanded }) {
const { showLine } = this.$props;
if (loading) {
return <LoadingOutlined class={`${prefixCls}-switcher-loading-icon`} />;
}
if (isLeaf) {
return showLine ? <FileOutlined class={`${prefixCls}-switcher-line-icon`} /> : null;
}
const switcherCls = `${prefixCls}-switcher-icon`;
if (switcherIcon) {
return cloneElement(switcherIcon, {
class: switcherCls,
});
}
return showLine ? (
expanded ? (
<MinusSquareOutlined class={`${prefixCls}-switcher-line-icon`} />
) : (
<PlusSquareOutlined class={`${prefixCls}-switcher-line-icon`} />
)
) : (
<CaretDownFilled class={switcherCls} />
);
},
updateTreeData(treeData: TreeDataItem[]) {
const { $slots } = this;
const defaultFields = { children: 'children', title: 'title', key: 'key' };
const replaceFields = { ...defaultFields, ...this.$props.replaceFields };
return treeData.map(item => {
const key = item[replaceFields.key];
const children = item[replaceFields.children];
const { slots = {}, class: cls, style, ...restProps } = item;
const treeNodeProps = {
...restProps,
icon: $slots[slots.icon] || restProps.icon,
switcherIcon: $slots[slots.switcherIcon] || restProps.switcherIcon,
title: $slots[slots.title] || $slots.title || restProps[replaceFields.title],
dataRef: item,
key,
class: cls,
style,
};
if (children) {
return { ...treeNodeProps, children: this.updateTreeData(children) };
}
return treeNodeProps;
});
},
setTreeRef(node: VNode) {
this.tree = node;
},
handleCheck(checkedObj: (number | string)[], eventObj: CheckEvent) {
this.$emit('update:checkedKeys', checkedObj);
this.$emit('check', checkedObj, eventObj);
},
handleExpand(expandedKeys: (number | string)[], eventObj: ExpendEvent) {
this.$emit('update:expandedKeys', expandedKeys);
this.$emit('expand', expandedKeys, eventObj);
},
handleSelect(selectedKeys: (number | string)[], eventObj: SelectEvent) {
this.$emit('update:selectedKeys', selectedKeys);
this.$emit('select', selectedKeys, eventObj);
},
},
render() {
const props = getOptionProps(this);
const { prefixCls: customizePrefixCls, showIcon, treeNodes, blockNode } = props;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('tree', customizePrefixCls);
const switcherIcon = getComponent(this, 'switcherIcon');
const checkable = props.checkable;
let treeData = props.treeData || treeNodes;
if (treeData) {
treeData = this.updateTreeData(treeData);
}
const { class: className, ...restAttrs } = this.$attrs;
const vcTreeProps = {
...props,
prefixCls,
checkable: checkable ? <span class={`${prefixCls}-checkbox-inner`} /> : checkable,
children: getSlot(this),
switcherIcon: nodeProps => this.renderSwitcherIcon(prefixCls, switcherIcon, nodeProps),
ref: this.setTreeRef,
...restAttrs,
class: classNames(className, {
[`${prefixCls}-icon-hide`]: !showIcon,
[`${prefixCls}-block-node`]: blockNode,
}),
onCheck: this.handleCheck,
onExpand: this.handleExpand,
onSelect: this.handleSelect,
} as Record<string, any>;
if (treeData) {
vcTreeProps.treeData = treeData;
}
return <VcTree {...vcTreeProps} __propsSymbol__={[]} />;
},
}); });

View File

@ -1,43 +1,45 @@
import { mount } from '@vue/test-utils'; import { calcRangeKeys } from '../utils/dictUtil';
import Tree from '../index';
import { calcRangeKeys } from '../util';
const TreeNode = Tree.TreeNode;
describe('Tree util', () => { describe('Tree util', () => {
it('calc range keys', () => { describe('calcRangeKeys', () => {
const wrapper = mount({ const treeData = [
render() { { key: '0-0', children: [{ key: '0-0-0' }, { key: '0-0-1' }] },
return ( { key: '0-1', children: [{ key: '0-1-0' }, { key: '0-1-1' }] },
<Tree> {
<TreeNode key="0-0"> key: '0-2',
<TreeNode key="0-0-0" /> children: [
<TreeNode key="0-0-1" /> { key: '0-2-0', children: [{ key: '0-2-0-0' }, { key: '0-2-0-1' }, { key: '0-2-0-2' }] },
</TreeNode> ],
<TreeNode key="0-1">
<TreeNode key="0-1-0" />
<TreeNode key="0-1-1" />
</TreeNode>
<TreeNode key="0-2">
<TreeNode key="0-2-0">
<TreeNode key="0-2-0-0" />
<TreeNode key="0-2-0-1" />
<TreeNode key="0-2-0-2" />
</TreeNode>
</TreeNode>
</Tree>
);
}, },
];
it('calc range keys', () => {
const keys = calcRangeKeys({
treeData,
expandedKeys: ['0-0', '0-2', '0-2-0'],
startKey: '0-2-0-1',
endKey: '0-0-0',
});
const target = ['0-0-0', '0-0-1', '0-1', '0-2', '0-2-0', '0-2-0-0', '0-2-0-1'];
expect(keys.sort()).toEqual(target.sort());
}); });
const treeWrapper = wrapper.findComponent({ name: 'ATree' }); it('return startKey when startKey === endKey', () => {
const keys = calcRangeKeys( const keys = calcRangeKeys({
treeWrapper.vm.$slots.default(), treeData,
['0-0', '0-2', '0-2-0'], expandedKeys: ['0-0', '0-2', '0-2-0'],
'0-2-0-1', startKey: '0-0-0',
'0-0-0', endKey: '0-0-0',
); });
const target = ['0-0-0', '0-0-1', '0-1', '0-2', '0-2-0', '0-2-0-0', '0-2-0-1']; expect(keys).toEqual(['0-0-0']);
expect(keys.sort()).toEqual(target.sort()); });
it('return empty array without startKey and endKey', () => {
const keys = calcRangeKeys({
treeData,
expandedKeys: ['0-0', '0-2', '0-2-0'],
});
expect(keys).toEqual([]);
});
}); });
}); });

View File

@ -0,0 +1 @@
title 的渲染逻辑

View File

@ -2,6 +2,25 @@ import type { App, Plugin } from 'vue';
import Tree from './Tree'; import Tree from './Tree';
import DirectoryTree from './DirectoryTree'; import DirectoryTree from './DirectoryTree';
export type { EventDataNode, DataNode } from '../vc-tree/interface';
export type {
TreeProps,
AntTreeNodeMouseEvent,
AntTreeNodeExpandedEvent,
AntTreeNodeCheckedEvent,
AntTreeNodeSelectedEvent,
AntTreeNodeDragEnterEvent,
AntTreeNodeDropEvent,
AntdTreeNodeAttribute,
TreeDataItem,
} from './Tree';
export type {
ExpandAction as DirectoryTreeExpandAction,
DirectoryTreeProps,
} from './DirectoryTree';
Tree.TreeNode.name = 'ATreeNode'; Tree.TreeNode.name = 'ATreeNode';
Tree.DirectoryTree = DirectoryTree; Tree.DirectoryTree = DirectoryTree;
/* istanbul ignore next */ /* istanbul ignore next */

View File

@ -2,93 +2,70 @@
@tree-prefix-cls: ~'@{ant-prefix}-tree'; @tree-prefix-cls: ~'@{ant-prefix}-tree';
.@{tree-prefix-cls} { .@{tree-prefix-cls}.@{tree-prefix-cls}-directory {
&.@{tree-prefix-cls}-directory { // ================== TreeNode ==================
.@{tree-prefix-cls}-treenode {
position: relative; position: relative;
// Stretch selector width // Hover color
> li, &::before {
.@{tree-prefix-cls}-child-tree > li { position: absolute;
span { top: 0;
&.@{tree-prefix-cls}-switcher { right: 0;
position: relative; bottom: 4px;
z-index: 1; left: 0;
transition: background-color 0.3s;
content: '';
pointer-events: none;
}
&.@{tree-prefix-cls}-switcher-noop { &:hover {
pointer-events: none; &::before {
} background: @item-hover-bg;
} }
}
&.@{tree-prefix-cls}-checkbox { // Elements
position: relative; > * {
z-index: 1; z-index: 1;
} }
&.@{tree-prefix-cls}-node-content-wrapper { // >>> Switcher
border-radius: 0; .@{tree-prefix-cls}-switcher {
user-select: none; transition: color 0.3s;
}
&:hover { // >>> Title
background: transparent; .@{tree-prefix-cls}-node-content-wrapper {
border-radius: 0;
user-select: none;
&::before { &:hover {
background: @item-hover-bg; background: transparent;
}
}
&.@{tree-prefix-cls}-node-selected {
color: @tree-directory-selected-color;
background: transparent;
}
&::before {
position: absolute;
right: 0;
left: 0;
height: @tree-title-height;
transition: all 0.3s;
content: '';
}
> span {
position: relative;
z-index: 1;
}
}
} }
&.@{tree-prefix-cls}-treenode-selected { &.@{tree-prefix-cls}-node-selected {
> span { color: @tree-directory-selected-color;
&.@{tree-prefix-cls}-switcher { background: transparent;
color: @tree-directory-selected-color; }
} }
&.@{tree-prefix-cls}-checkbox { // ============= Selected =============
.@{tree-prefix-cls}-checkbox-inner { &-selected {
border-color: @primary-color; &:hover::before,
} &::before {
background: @tree-directory-selected-bg;
}
&.@{tree-prefix-cls}-checkbox-checked { // >>> Switcher
&::after { .@{tree-prefix-cls}-switcher {
border-color: @checkbox-check-color; color: @tree-directory-selected-color;
} }
.@{tree-prefix-cls}-checkbox-inner { // >>> Title
background: @checkbox-check-color; .@{tree-prefix-cls}-node-content-wrapper {
color: @tree-directory-selected-color;
&::after { background: transparent;
border-color: @primary-color;
}
}
}
}
&.@{tree-prefix-cls}-node-content-wrapper {
&::before {
background: @tree-directory-selected-bg;
}
}
}
} }
} }
} }

View File

@ -5,276 +5,12 @@
@import './directory'; @import './directory';
@tree-prefix-cls: ~'@{ant-prefix}-tree'; @tree-prefix-cls: ~'@{ant-prefix}-tree';
@tree-showline-icon-color: @text-color-secondary; @tree-node-prefix-cls: ~'@{tree-prefix-cls}-treenode';
@tree-node-padding: 4px;
.antCheckboxFn(@checkbox-prefix-cls: ~'@{ant-prefix}-tree-checkbox'); .antCheckboxFn(@checkbox-prefix-cls: ~'@{ant-prefix}-tree-checkbox');
.@{tree-prefix-cls} { .@{tree-prefix-cls} {
/* see https://github.com/ant-design/ant-design/issues/16259 */ .antTreeFn(@tree-prefix-cls);
&-checkbox-checked::after {
position: absolute;
top: 16.67%;
left: 0;
width: 100%;
height: 66.67%;
}
.reset-component();
margin: 0;
padding: 0;
ol,
ul {
margin: 0;
padding: 0;
list-style: none;
}
li {
margin: 0;
padding: @tree-node-padding 0;
white-space: nowrap;
list-style: none;
outline: 0;
span[draggable],
span[draggable='true'] {
line-height: @tree-title-height - 4px;
border-top: 2px transparent solid;
border-bottom: 2px transparent solid;
user-select: none;
/* Required to make elements draggable in old WebKit */
-khtml-user-drag: element;
-webkit-user-drag: element;
}
&.drag-over {
> span[draggable] {
color: white;
background-color: @primary-color;
opacity: 0.8;
}
}
&.drag-over-gap-top {
> span[draggable] {
border-top-color: @primary-color;
}
}
&.drag-over-gap-bottom {
> span[draggable] {
border-bottom-color: @primary-color;
}
}
&.filter-node {
> span {
color: @highlight-color !important;
font-weight: 500 !important;
}
}
// When node is loading
&.@{tree-prefix-cls}-treenode-loading {
span {
&.@{tree-prefix-cls}-switcher {
&.@{tree-prefix-cls}-switcher_open,
&.@{tree-prefix-cls}-switcher_close {
.@{tree-prefix-cls}-switcher-loading-icon {
position: absolute;
left: 0;
display: inline-block;
width: 24px;
height: @tree-title-height;
color: @primary-color;
font-size: 14px;
transform: none;
svg {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}
}
:root &::after {
opacity: 0;
}
}
}
}
}
ul {
margin: 0;
padding: 0 0 0 @tree-child-padding;
}
.@{tree-prefix-cls}-node-content-wrapper {
display: inline-block;
height: @tree-title-height;
margin: 0;
padding: 0 5px;
color: @text-color;
line-height: @tree-title-height;
text-decoration: none;
vertical-align: top;
border-radius: @border-radius-sm;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: @tree-node-hover-bg;
}
&.@{tree-prefix-cls}-node-selected {
background-color: @tree-node-selected-bg;
}
}
span {
&.@{tree-prefix-cls}-checkbox {
top: initial;
height: @tree-title-height;
margin: 0 4px 0 2px;
padding: ((@tree-title-height - 16px) / 2) 0;
}
&.@{tree-prefix-cls}-switcher,
&.@{tree-prefix-cls}-iconEle {
display: inline-block;
width: 24px;
height: @tree-title-height;
margin: 0;
line-height: @tree-title-height;
text-align: center;
vertical-align: top;
border: 0 none;
outline: none;
cursor: pointer;
}
&.@{tree-prefix-cls}-iconEle:empty {
display: none;
}
&.@{tree-prefix-cls}-switcher {
position: relative;
&.@{tree-prefix-cls}-switcher-noop {
cursor: default;
}
&.@{tree-prefix-cls}-switcher_open {
.antTreeSwitcherIcon();
}
&.@{tree-prefix-cls}-switcher_close {
.antTreeSwitcherIcon();
.@{tree-prefix-cls}-switcher-icon {
svg {
transform: rotate(-90deg);
}
}
}
}
}
&:last-child > span {
&.@{tree-prefix-cls}-switcher,
&.@{tree-prefix-cls}-iconEle {
&::before {
display: none;
}
}
}
}
> li {
&:first-child {
padding-top: 7px;
}
&:last-child {
padding-bottom: 7px;
}
}
&-child-tree {
// https://github.com/ant-design/ant-design/issues/14958
> li {
// Provide additional padding between top child node and parent node
&:first-child {
padding-top: 2 * @tree-node-padding;
}
// Hide additional padding between last child node and next parent node
&:last-child {
padding-bottom: 0;
}
}
}
li&-treenode-disabled {
> span:not(.@{tree-prefix-cls}-switcher),
> .@{tree-prefix-cls}-node-content-wrapper,
> .@{tree-prefix-cls}-node-content-wrapper span {
color: @disabled-color;
cursor: not-allowed;
}
> .@{tree-prefix-cls}-node-content-wrapper:hover {
background: transparent;
}
}
&-icon__open {
margin-right: 2px;
vertical-align: top;
}
&-icon__close {
margin-right: 2px;
vertical-align: top;
}
// Tree with line
&&-show-line {
li {
position: relative;
span {
&.@{tree-prefix-cls}-switcher {
color: @tree-showline-icon-color;
background: @component-background;
&.@{tree-prefix-cls}-switcher-noop {
.antTreeShowLineIcon('tree-doc-icon');
}
&.@{tree-prefix-cls}-switcher_open {
.antTreeShowLineIcon('tree-showline-open-icon');
}
&.@{tree-prefix-cls}-switcher_close {
.antTreeShowLineIcon('tree-showline-close-icon');
}
}
}
}
li:not(:last-child)::before {
position: absolute;
left: 12px;
width: 1px;
height: 100%;
height: calc(100% - 22px); // Remove additional height if support
margin: 22px 0 0;
border-left: 1px solid @border-color-base;
content: ' ';
}
}
&.@{tree-prefix-cls}-icon-hide {
.@{tree-prefix-cls}-treenode-loading {
.@{tree-prefix-cls}-iconEle {
display: none;
}
}
}
&.@{tree-prefix-cls}-block-node {
li {
.@{tree-prefix-cls}-node-content-wrapper {
width: ~'calc(100% - 24px)';
}
span {
&.@{tree-prefix-cls}-checkbox {
+ .@{tree-prefix-cls}-node-content-wrapper {
width: ~'calc(100% - 46px)';
}
}
}
}
}
} }
@import './rtl';

View File

@ -1,29 +1,274 @@
@import '../../style/mixins/index'; @import '../../style/mixins/index';
@tree-prefix-cls: ~'@{ant-prefix}-tree'; @tree-prefix-cls: ~'@{ant-prefix}-tree';
@tree-select-prefix-cls: ~'@{ant-prefix}-select'; @tree-node-prefix-cls: ~'@{tree-prefix-cls}-treenode';
@select-tree-prefix-cls: ~'@{ant-prefix}-select-tree';
@tree-motion: ~'@{ant-prefix}-motion-collapse';
@tree-node-padding: (@padding-xs / 2);
@tree-node-hightlight-color: inherit;
.antTreeSwitcherIcon(@type: 'tree-default-open-icon') { .antTreeSwitcherIcon(@type: 'tree-default-open-icon') {
.@{tree-prefix-cls}-switcher-icon, .@{tree-prefix-cls}-switcher-icon,
.@{tree-select-prefix-cls}-switcher-icon { .@{select-tree-prefix-cls}-switcher-icon {
.iconfont-size-under-12px(10px);
display: inline-block; display: inline-block;
font-weight: bold; font-size: 10px;
vertical-align: baseline;
svg { svg {
transition: transform 0.3s; transition: transform 0.3s;
} }
} }
} }
.antTreeShowLineIcon(@type) { .drop-indicator() {
.@{tree-prefix-cls}-switcher-icon, .@{tree-prefix-cls}-drop-indicator {
.@{tree-select-prefix-cls}-switcher-icon { position: absolute;
display: inline-block; // it should displayed over the following node
font-weight: normal; z-index: 1;
font-size: 12px; height: 2px;
svg { background-color: @primary-color;
transition: transform 0.3s; border-radius: 1px;
pointer-events: none;
&::after {
position: absolute;
top: -3px;
left: -6px;
width: 8px;
height: 8px;
background-color: transparent;
border: 2px solid @primary-color;
border-radius: 50%;
content: '';
}
}
}
.antTreeFn(@custom-tree-prefix-cls) {
@custom-tree-node-prefix-cls: ~'@{custom-tree-prefix-cls}-treenode';
.reset-component();
background: @tree-bg;
border-radius: @border-radius-base;
transition: background-color 0.3s;
&-focused:not(:hover):not(&-active-focused) {
background: @primary-1;
}
// =================== Virtual List ===================
&-list-holder-inner {
align-items: flex-start;
}
&.@{custom-tree-prefix-cls}-block-node {
.@{custom-tree-prefix-cls}-list-holder-inner {
align-items: stretch;
// >>> Title
.@{custom-tree-prefix-cls}-node-content-wrapper {
flex: auto;
}
}
}
// ===================== TreeNode =====================
.@{custom-tree-node-prefix-cls} {
display: flex;
align-items: flex-start;
padding: 0 0 @tree-node-padding 0;
outline: none;
// Disabled
&-disabled {
// >>> Title
.@{custom-tree-prefix-cls}-node-content-wrapper {
color: @disabled-color;
cursor: not-allowed;
&:hover {
background: transparent;
}
}
}
&-active .@{custom-tree-prefix-cls}-node-content-wrapper {
background: @tree-node-hover-bg;
}
&:not(&-disabled).filter-node .@{custom-tree-prefix-cls}-title {
color: @tree-node-hightlight-color;
font-weight: 500;
}
}
// >>> Indent
&-indent {
align-self: stretch;
white-space: nowrap;
user-select: none;
&-unit {
display: inline-block;
width: @tree-title-height;
}
}
// >>> Switcher
&-switcher {
.antTreeSwitcherIcon();
position: relative;
flex: none;
align-self: stretch;
width: @tree-title-height;
margin: 0;
line-height: @tree-title-height;
text-align: center;
cursor: pointer;
user-select: none;
&-noop {
cursor: default;
}
&_close {
.@{custom-tree-prefix-cls}-switcher-icon {
svg {
transform: rotate(-90deg);
}
}
}
&-loading-icon {
color: @primary-color;
}
&-leaf-line {
position: relative;
z-index: 1;
display: inline-block;
width: 100%;
height: 100%;
&::before {
position: absolute;
top: 0;
bottom: -@tree-node-padding;
margin-left: -1px;
border-left: 1px solid @normal-color;
content: ' ';
}
&::after {
position: absolute;
width: @tree-title-height - 14px;
height: @tree-title-height - 10px;
margin-left: -1px;
border-bottom: 1px solid @normal-color;
content: ' ';
}
}
}
// >>> Checkbox
&-checkbox {
top: initial;
margin: ((@tree-title-height - @checkbox-size) / 2) 8px 0 0;
}
// >>> Title
& &-node-content-wrapper {
position: relative;
z-index: auto;
min-height: @tree-title-height;
margin: 0;
padding: 0 4px;
color: inherit;
line-height: @tree-title-height;
background: transparent;
border-radius: @border-radius-base;
cursor: pointer;
transition: all 0.3s, border 0s, line-height 0s, box-shadow 0s;
&:hover {
background-color: @tree-node-hover-bg;
}
&.@{custom-tree-prefix-cls}-node-selected {
background-color: @tree-node-selected-bg;
}
// Icon
.@{custom-tree-prefix-cls}-iconEle {
display: inline-block;
width: @tree-title-height;
height: @tree-title-height;
line-height: @tree-title-height;
text-align: center;
vertical-align: top;
&:empty {
display: none;
}
}
}
// https://github.com/ant-design/ant-design/issues/28217
&-unselectable &-node-content-wrapper:hover {
background-color: transparent;
}
// ==================== Draggable =====================
&-node-content-wrapper[draggable='true'] {
line-height: @tree-title-height;
user-select: none;
.drop-indicator();
}
.@{custom-tree-node-prefix-cls}.drop-container {
> [draggable] {
box-shadow: 0 0 0 2px @primary-color;
}
}
// ==================== Show Line =====================
&-show-line {
// ================ Indent lines ================
.@{custom-tree-prefix-cls}-indent {
&-unit {
position: relative;
height: 100%;
&::before {
position: absolute;
top: 0;
right: (@tree-title-height / 2);
bottom: -@tree-node-padding;
border-right: 1px solid @border-color-base;
content: '';
}
&-end {
&::before {
display: none;
}
}
}
}
// ============== Cover Background ==============
.@{custom-tree-prefix-cls}-switcher {
background: @component-background;
&-line-icon {
vertical-align: -0.225em;
}
}
}
}
.@{tree-node-prefix-cls}-leaf-last {
.@{tree-prefix-cls}-switcher {
&-leaf-line {
&::before {
top: auto !important;
bottom: auto !important;
height: @tree-title-height - 10px !important;
}
} }
} }
} }

View File

@ -0,0 +1,72 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@import '../../checkbox/style/mixin';
@tree-prefix-cls: ~'@{ant-prefix}-tree';
@select-tree-prefix-cls: ~'@{ant-prefix}-select-tree';
@tree-node-prefix-cls: ~'@{tree-prefix-cls}-treenode';
.@{tree-prefix-cls} {
&-rtl {
direction: rtl;
.@{tree-prefix-cls}-node-content-wrapper[draggable='true'] {
.@{tree-prefix-cls}-drop-indicator {
&::after {
right: -6px;
left: unset;
}
}
}
}
// ===================== TreeNode =====================
.@{tree-node-prefix-cls} {
&-rtl {
direction: rtl;
}
}
// >>> Switcher
&-switcher {
&_close {
.@{tree-prefix-cls}-switcher-icon {
svg {
.@{tree-prefix-cls}-rtl & {
transform: rotate(90deg);
}
}
}
}
}
// ==================== Show Line =====================
&-show-line {
// ================ Indent lines ================
.@{tree-prefix-cls}-indent {
&-unit {
&::before {
.@{tree-prefix-cls}-rtl& {
right: auto;
left: -(@tree-title-height / 2) - 1px;
border-right: none;
border-left: 1px solid @border-color-base;
}
}
}
}
}
// >>> Checkbox
&-checkbox {
.@{tree-prefix-cls}-rtl& {
margin: ((@tree-title-height - @checkbox-size) / 2) 0 0 8px;
}
}
}
.@{select-tree-prefix-cls} {
// >>> Checkbox
&-checkbox {
.@{tree-prefix-cls}-select-dropdown-rtl & {
margin: ((@tree-title-height - @checkbox-size) / 2) 0 0 8px;
}
}
}

View File

@ -1,110 +0,0 @@
import type { VNode } from 'vue';
import { getNodeChildren, convertTreeToEntities } from '../vc-tree/src/util';
import { getSlot } from '../_util/props-util';
import type { TreeDataItem } from './Tree';
enum Record {
None,
Start,
End,
}
type TreeKey = string | number;
// TODO: Move this logic into `rc-tree`
function traverseNodesKey(rootChildren: VNode[], callback?: Function) {
const nodeList = getNodeChildren(rootChildren) || [];
function processNode(node: VNode) {
const { key } = node;
const children = getSlot(node);
if (callback(key, node) !== false) {
traverseNodesKey(children, callback);
}
}
nodeList.forEach(processNode);
}
export function getFullKeyList(children: VNode[]) {
const { keyEntities } = convertTreeToEntities(children);
return [...keyEntities.keys()];
}
/** 计算选中范围只考虑expanded情况以优化性能 */
export function calcRangeKeys(
rootChildren: VNode[],
expandedKeys: TreeKey[],
startKey: TreeKey,
endKey: TreeKey,
) {
const keys = [];
let record = Record.None;
if (startKey && startKey === endKey) {
return [startKey];
}
if (!startKey || !endKey) {
return [];
}
function matchKey(key: TreeKey) {
return key === startKey || key === endKey;
}
traverseNodesKey(rootChildren, (key: TreeKey) => {
if (record === Record.End) {
return false;
}
if (matchKey(key)) {
// Match test
keys.push(key);
if (record === Record.None) {
record = Record.Start;
} else if (record === Record.Start) {
record = Record.End;
return false;
}
} else if (record === Record.Start) {
// Append selection
keys.push(key);
}
if (expandedKeys.indexOf(key) === -1) {
return false;
}
return true;
});
return keys;
}
export function convertDirectoryKeysToNodes(rootChildren: VNode[], keys: TreeKey[]) {
const restKeys = [...keys];
const nodes = [];
traverseNodesKey(rootChildren, (key: TreeKey, node: VNode) => {
const index = restKeys.indexOf(key);
if (index !== -1) {
nodes.push(node);
restKeys.splice(index, 1);
}
return !!restKeys.length;
});
return nodes;
}
export function getFullKeyListByTreeData(treeData: TreeDataItem[], replaceFields: any = {}) {
let keys = [];
const { key = 'key', children = 'children' } = replaceFields;
(treeData || []).forEach((item: TreeDataItem) => {
keys.push(item[key]);
if (item[children]) {
keys = [...keys, ...getFullKeyListByTreeData(item[children], replaceFields)];
}
});
return keys;
}

View File

@ -0,0 +1,92 @@
import type { DataNode, Key } from '../../vc-tree/interface';
enum Record {
None,
Start,
End,
}
function traverseNodesKey(
treeData: DataNode[],
callback: (key: Key | number | null, node: DataNode) => boolean,
) {
function processNode(dataNode: DataNode) {
const { key, children } = dataNode;
if (callback(key, dataNode) !== false) {
traverseNodesKey(children || [], callback);
}
}
treeData.forEach(processNode);
}
/** 计算选中范围只考虑expanded情况以优化性能 */
export function calcRangeKeys({
treeData,
expandedKeys,
startKey,
endKey,
}: {
treeData: DataNode[];
expandedKeys: Key[];
startKey?: Key;
endKey?: Key;
}): Key[] {
const keys: Key[] = [];
let record: Record = Record.None;
if (startKey && startKey === endKey) {
return [startKey];
}
if (!startKey || !endKey) {
return [];
}
function matchKey(key: Key) {
return key === startKey || key === endKey;
}
traverseNodesKey(treeData, (key: Key) => {
if (record === Record.End) {
return false;
}
if (matchKey(key)) {
// Match test
keys.push(key);
if (record === Record.None) {
record = Record.Start;
} else if (record === Record.Start) {
record = Record.End;
return false;
}
} else if (record === Record.Start) {
// Append selection
keys.push(key);
}
if (expandedKeys.indexOf(key) === -1) {
return false;
}
return true;
});
return keys;
}
export function convertDirectoryKeysToNodes(treeData: DataNode[], keys: Key[]) {
const restKeys: Key[] = [...keys];
const nodes: DataNode[] = [];
traverseNodesKey(treeData, (key: Key, node: DataNode) => {
const index = restKeys.indexOf(key);
if (index !== -1) {
nodes.push(node);
restKeys.splice(index, 1);
}
return !!restKeys.length;
});
return nodes;
}

View File

@ -0,0 +1,33 @@
import type { CSSProperties } from 'vue';
export const offset = 4;
export default function dropIndicatorRender(props: {
dropPosition: -1 | 0 | 1;
dropLevelOffset: number;
indent: number;
prefixCls: string;
direction: 'ltr' | 'rtl';
}) {
const { dropPosition, dropLevelOffset, prefixCls, indent, direction = 'ltr' } = props;
const startPosition = direction === 'ltr' ? 'left' : 'right';
const endPosition = direction === 'ltr' ? 'right' : 'left';
const style: CSSProperties = {
[startPosition]: `${-dropLevelOffset * indent + offset}px`,
[endPosition]: 0,
};
switch (dropPosition) {
case -1:
style.top = `${-3}px`;
break;
case 1:
style.bottom = `${-3}px`;
break;
default:
// dropPosition === 0
style.bottom = `${-3}px`;
style[startPosition] = `${indent + offset}px`;
break;
}
return <div style={style} class={`${prefixCls}-drop-indicator`} />;
}

View File

@ -0,0 +1,57 @@
import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined';
import FileOutlined from '@ant-design/icons-vue/FileOutlined';
import MinusSquareOutlined from '@ant-design/icons-vue/MinusSquareOutlined';
import PlusSquareOutlined from '@ant-design/icons-vue/PlusSquareOutlined';
import CaretDownFilled from '@ant-design/icons-vue/CaretDownFilled';
import type { AntTreeNodeProps } from '../Tree';
import { isValidElement } from '../../_util/props-util';
import { cloneVNode } from 'vue';
export default function renderSwitcherIcon(
prefixCls: string,
switcherIcon: any,
showLine: boolean | { showLeafIcon: boolean } | undefined,
props: AntTreeNodeProps,
) {
const { isLeaf, expanded, loading } = props;
let icon = switcherIcon;
if (loading) {
return <LoadingOutlined class={`${prefixCls}-switcher-loading-icon`} />;
}
let showLeafIcon: boolean;
if (showLine && typeof showLine === 'object') {
showLeafIcon = showLine.showLeafIcon;
}
let defaultIcon = null;
const switcherCls = `${prefixCls}-switcher-icon`;
if (isLeaf) {
if (showLine) {
if (typeof showLine === 'object' && !showLeafIcon) {
defaultIcon = <span class={`${prefixCls}-switcher-leaf-line`} />;
} else {
defaultIcon = <FileOutlined class={`${prefixCls}-switcher-line-icon`} />;
}
}
return defaultIcon;
} else {
defaultIcon = <CaretDownFilled class={switcherCls} />;
if (showLine) {
defaultIcon = expanded ? (
<MinusSquareOutlined class={`${prefixCls}-switcher-line-icon`} />
) : (
<PlusSquareOutlined class={`${prefixCls}-switcher-line-icon`} />
);
}
}
if (typeof switcherIcon === 'function') {
icon = switcherIcon({ ...props, defaultIcon, switcherCls });
} else if (isValidElement(icon)) {
icon = cloneVNode(icon, {
class: switcherCls,
});
}
return icon || defaultIcon;
}

View File

@ -13,7 +13,28 @@ import UploadList from './UploadList';
import { UploadProps } from './interface'; import { UploadProps } from './interface';
import { T, fileToObject, genPercentAdd, getFileItem, removeFileItem } from './utils'; import { T, fileToObject, genPercentAdd, getFileItem, removeFileItem } from './utils';
import { defineComponent, inject } from 'vue'; import { defineComponent, inject } from 'vue';
import { getDataAndAria } from '../vc-tree/src/util'; import { getDataAndAriaProps } from '../_util/util';
export type UploadFileStatus = 'error' | 'success' | 'done' | 'uploading' | 'removed';
export interface UploadFile<T = any> {
uid: string;
size?: number;
name: string;
fileName?: string;
lastModified?: number;
lastModifiedDate?: Date;
url?: string;
status?: UploadFileStatus;
percent?: number;
thumbUrl?: string;
originFileObj?: any;
response?: T;
error?: any;
linkProps?: any;
type?: string;
xhr?: T;
preview?: string;
}
export default defineComponent({ export default defineComponent({
name: 'AUpload', name: 'AUpload',
@ -185,7 +206,10 @@ export default defineComponent({
if (result === false) { if (result === false) {
this.handleChange({ this.handleChange({
file, file,
fileList: uniqBy(stateFileList.concat(fileList.map(fileToObject)), item => item.uid), fileList: uniqBy(
stateFileList.concat(fileList.map(fileToObject)),
(item: UploadFile) => item.uid,
),
}); });
return false; return false;
} }
@ -280,7 +304,7 @@ export default defineComponent({
[`${prefixCls}-disabled`]: disabled, [`${prefixCls}-disabled`]: disabled,
}); });
return ( return (
<span class={className} {...getDataAndAria(this.$attrs)}> <span class={className} {...getDataAndAriaProps(this.$attrs)}>
<div <div
class={dragCls} class={dragCls}
onDrop={this.onFileDrop} onDrop={this.onFileDrop}

View File

@ -114,6 +114,9 @@ export default defineComponent({
const { measureText: prevMeasureText, measuring } = state; const { measureText: prevMeasureText, measuring } = state;
const { prefix, validateSearch } = props; const { prefix, validateSearch } = props;
const target = event.target as HTMLTextAreaElement; const target = event.target as HTMLTextAreaElement;
if (target.composing) {
return;
}
const selectionStartText = getBeforeSelectionText(target); const selectionStartText = getBeforeSelectionText(target);
const { location: measureIndex, prefix: measurePrefix } = getLastMeasureIndex( const { location: measureIndex, prefix: measurePrefix } = getLastMeasureIndex(
selectionStartText, selectionStartText,

View File

@ -5,7 +5,7 @@ import classNames from '../_util/classNames';
import pickAttrs from '../_util/pickAttrs'; import pickAttrs from '../_util/pickAttrs';
import { isValidElement } from '../_util/props-util'; import { isValidElement } from '../_util/props-util';
import createRef from '../_util/createRef'; import createRef from '../_util/createRef';
import type { PropType, VNodeChild } from 'vue'; import type { PropType } from 'vue';
import { computed, defineComponent, nextTick, reactive, watch } from 'vue'; import { computed, defineComponent, nextTick, reactive, watch } from 'vue';
import List from '../vc-virtual-list/List'; import List from '../vc-virtual-list/List';
import type { import type {
@ -16,18 +16,24 @@ import type {
} from './interface'; } from './interface';
import type { RawValueType, FlattenOptionsType } from './interface/generator'; import type { RawValueType, FlattenOptionsType } from './interface/generator';
import useMemo from '../_util/hooks/useMemo'; import useMemo from '../_util/hooks/useMemo';
export interface OptionListProps {
export interface RefOptionListProps {
onKeydown: (e?: KeyboardEvent) => void;
onKeyup: (e?: KeyboardEvent) => void;
scrollTo?: (index: number) => void;
}
export interface OptionListProps<OptionType extends object> {
prefixCls: string; prefixCls: string;
id: string; id: string;
options: SelectOptionsType; options: OptionType[];
flattenOptions: FlattenOptionsType<SelectOptionsType>; flattenOptions: FlattenOptionsType<OptionType>;
height: number; height: number;
itemHeight: number; itemHeight: number;
values: Set<RawValueType>; values: Set<RawValueType>;
multiple: boolean; multiple: boolean;
open: boolean; open: boolean;
defaultActiveFirstOption?: boolean; defaultActiveFirstOption?: boolean;
notFoundContent?: VNodeChild; notFoundContent?: any;
menuItemSelectedIcon?: RenderNode; menuItemSelectedIcon?: RenderNode;
childrenAsData: boolean; childrenAsData: boolean;
searchValue: string; searchValue: string;
@ -74,7 +80,7 @@ const OptionListProps = {
* Using virtual list of option display. * Using virtual list of option display.
* Will fallback to dom if use customize render. * Will fallback to dom if use customize render.
*/ */
const OptionList = defineComponent<OptionListProps, { state?: any }>({ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, { state?: any }>({
name: 'OptionList', name: 'OptionList',
inheritAttrs: false, inheritAttrs: false,
slots: ['option'], slots: ['option'],
@ -147,20 +153,22 @@ const OptionList = defineComponent<OptionListProps, { state?: any }>({
watch( watch(
() => props.open, () => props.open,
() => { () => {
nextTick(() => { if (!props.multiple && props.open && props.values.size === 1) {
if (!props.multiple && props.open && props.values.size === 1) { const value = Array.from(props.values)[0];
const value = Array.from(props.values)[0]; const index = memoFlattenOptions.value.findIndex(({ data }) => data.value === value);
const index = memoFlattenOptions.value.findIndex(({ data }) => data.value === value); setActive(index);
setActive(index); nextTick(() => {
scrollIntoView(index); scrollIntoView(index);
} });
// Force trigger scrollbar visible when open }
if (props.open) { // Force trigger scrollbar visible when open
if (props.open) {
nextTick(() => {
listRef.current?.scrollTo(undefined); listRef.current?.scrollTo(undefined);
} });
}); }
}, },
{ immediate: true }, { immediate: true, flush: 'post' },
); );
// ========================== Values ========================== // ========================== Values ==========================
@ -282,7 +290,7 @@ const OptionList = defineComponent<OptionListProps, { state?: any }>({
virtual, virtual,
onScroll, onScroll,
onMouseenter, onMouseenter,
} = this.$props as OptionListProps; } = this.$props;
const renderOption = $slots.option; const renderOption = $slots.option;
const { activeIndex } = this.state; const { activeIndex } = this.state;
// ========================== Render ========================== // ========================== Render ==========================

View File

@ -43,13 +43,13 @@ import {
fillOptionsWithMissingValue, fillOptionsWithMissingValue,
} from './utils/valueUtil'; } from './utils/valueUtil';
import type { 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';
import { defineComponent, ref } from 'vue'; import { defineComponent, ref } from 'vue';
import omit from 'lodash-es/omit';
const RefSelect = generateSelector<SelectOptionsType>({ const RefSelect = generateSelector<SelectOptionsType[number]>({
prefixCls: 'rc-select', prefixCls: 'rc-select',
components: { components: {
optionList: SelectOptionList as any, optionList: SelectOptionList as any,
@ -64,12 +64,23 @@ const RefSelect = generateSelector<SelectOptionsType>({
fillOptionsWithMissingValue, fillOptionsWithMissingValue,
}); });
export type ExportedSelectProps<ValueType extends DefaultValueType = DefaultValueType> = export type ExportedSelectProps<T extends DefaultValueType = DefaultValueType> = SelectProps<
SelectProps<SelectOptionsType, ValueType>; SelectOptionsType[number],
T
>;
const Select = defineComponent<Omit<ExportedSelectProps, 'children'>>({ export function selectProps<T>() {
return selectBaseProps<SelectOptionsType[number], T>();
}
const Select = defineComponent({
name: 'Select',
inheritAttrs: false,
Option,
OptGroup,
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();
@ -91,8 +102,4 @@ const Select = defineComponent<Omit<ExportedSelectProps, 'children'>>({
}; };
}, },
}); });
Select.inheritAttrs = false;
Select.props = omit(RefSelect.props, ['children']);
Select.Option = Option;
Select.OptGroup = OptGroup;
export default Select; export default Select;

View File

@ -2,8 +2,10 @@ import pickAttrs from '../../_util/pickAttrs';
import Input from './Input'; import Input from './Input';
import type { InnerSelectorProps } from './interface'; import type { InnerSelectorProps } from './interface';
import type { VNodeChild } from 'vue'; import type { VNodeChild } from 'vue';
import { computed, defineComponent, Fragment, ref, watch } from 'vue'; import { Fragment } from 'vue';
import { computed, defineComponent, ref, watch } from 'vue';
import PropTypes from '../../_util/vue-types'; import PropTypes from '../../_util/vue-types';
import { useInjectTreeSelectContext } from '../../vc-tree-select/Context';
interface SelectorProps extends InnerSelectorProps { interface SelectorProps extends InnerSelectorProps {
inputElement: VNodeChild; inputElement: VNodeChild;
@ -50,6 +52,7 @@ const SingleSelector = defineComponent<SelectorProps>({
} }
return inputValue; return inputValue;
}); });
const treeSelectContext = useInjectTreeSelectContext();
watch( watch(
[combobox, () => props.activeValue], [combobox, () => props.activeValue],
() => { () => {
@ -94,6 +97,23 @@ const SingleSelector = defineComponent<SelectorProps>({
onInputCompositionEnd, onInputCompositionEnd,
} = props; } = props;
const item = values[0]; const item = values[0];
let titleNode = null;
// custom tree-select title by slot
if (item && treeSelectContext.value.slots) {
titleNode =
treeSelectContext.value.slots[item?.option?.data?.slots?.title] ||
treeSelectContext.value.slots.title ||
item.label;
if (typeof titleNode === 'function') {
titleNode = titleNode(item.option?.data || {});
}
// else if (treeSelectContext.value.slots.titleRender) {
// // title titleRender title titleRender
// titleNode = treeSelectContext.value.slots.titleRender(item.option?.data || {});
// }
} else {
titleNode = item?.label;
}
return ( return (
<> <>
<span class={`${prefixCls}-selection-search`}> <span class={`${prefixCls}-selection-search`}>
@ -126,7 +146,7 @@ const SingleSelector = defineComponent<SelectorProps>({
{/* Display value */} {/* Display value */}
{!combobox.value && item && !hasTextInput.value && ( {!combobox.value && item && !hasTextInput.value && (
<span class={`${prefixCls}-selection-item`} title={title.value}> <span class={`${prefixCls}-selection-item`} title={title.value}>
<Fragment key={item.key || item.value}>{item.label}</Fragment> <Fragment key={item.key || item.value}>{titleNode}</Fragment>
</span> </span>
)} )}

View File

@ -52,12 +52,12 @@ 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;
onSelect: (value: RawValueType, option: { selected: boolean }) => void; onSelect: (value: RawValueType, option: { selected: boolean }) => void;
onInputKeyDown?: EventHandlerNonNull; onInputKeyDown?: (e: KeyboardEvent) => void;
/** /**
* @private get real dom for trigger align. * @private get real dom for trigger align.

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,17 @@
import type { Ref, VNodeChild } from 'vue'; import type { Ref } from 'vue';
import { computed } from 'vue'; import { computed } from 'vue';
import type { RawValueType, FlattenOptionsType, Key } from '../interface/generator'; import type { RawValueType, FlattenOptionsType, Key } from '../interface/generator';
export default function useCacheOptions< export default function useCacheOptions<
OptionsType extends { OptionType extends {
value?: RawValueType; value?: RawValueType;
label?: VNodeChild; label?: any;
key?: Key; key?: Key;
disabled?: boolean; disabled?: boolean;
}[], },
>(options: Ref) { >(options: Ref) {
const optionMap = computed(() => { const optionMap = computed(() => {
const map: Map<RawValueType, FlattenOptionsType<OptionsType>[number]> = new Map(); const map: Map<RawValueType, FlattenOptionsType<OptionType>[number]> = new Map();
options.value.forEach((item: any) => { options.value.forEach((item: any) => {
const { const {
data: { value }, data: { value },
@ -21,7 +21,7 @@ export default function useCacheOptions<
return map; return map;
}); });
const getValueOption = (vals: RawValueType[]): FlattenOptionsType<OptionsType> => const getValueOption = (vals: RawValueType[]) =>
vals.map(value => optionMap.value.get(value)).filter(Boolean); vals.map(value => optionMap.value.get(value)).filter(Boolean);
return getValueOption; return getValueOption;

View File

@ -1,10 +1,11 @@
import type { ExportedSelectProps } from './Select'; import type { ExportedSelectProps } from './Select';
import Select from './Select'; import Select, { selectProps } from './Select';
import Option from './Option'; import Option from './Option';
import OptGroup from './OptGroup'; import OptGroup from './OptGroup';
import { BaseProps } from './generate'; import { selectBaseProps } from './generate';
import type { ExtractPropTypes } from 'vue';
export type SelectProps<T = any> = ExportedSelectProps<T>; export type SelectProps<T = any> = Partial<ExtractPropTypes<ExportedSelectProps<T>>>;
export { Option, OptGroup, BaseProps }; export { Option, OptGroup, selectBaseProps, selectProps };
export default Select; export default Select;

View File

@ -56,9 +56,11 @@ export type FilterOptions<OptionsType extends object[]> = (
export type FilterFunc<OptionType> = (inputValue: string, option?: OptionType) => boolean; export type FilterFunc<OptionType> = (inputValue: string, option?: OptionType) => boolean;
export type FlattenOptionsType<OptionsType extends object[] = object[]> = { export type FlattenOptionsType<OptionType = object> = {
key: Key; key: Key;
data: OptionsType[number]; data: OptionType;
label?: any;
value?: RawValueType;
/** Used for customize data */ /** Used for customize data */
[name: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any [name: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
}[]; }[];

View File

@ -1,7 +1,6 @@
import warning, { noteOnce } from '../../vc-util/warning'; import warning, { noteOnce } from '../../vc-util/warning';
import type { SelectProps } from '..'; import type { SelectProps } from '..';
import { convertChildrenToData } from './legacyUtil'; import { convertChildrenToData } from './legacyUtil';
import type { OptionData } from '../interface';
import { toArray } from './commonUtil'; import { toArray } from './commonUtil';
import type { RawValueType, LabelValueType } from '../interface/generator'; import type { RawValueType, LabelValueType } from '../interface/generator';
import { isValidElement } from '../../_util/props-util'; import { isValidElement } from '../../_util/props-util';
@ -36,23 +35,6 @@ function warningProps(props: SelectProps) {
'Please avoid setting option to disabled in tags mode since user can always type text as tag.', 'Please avoid setting option to disabled in tags mode since user can always type text as tag.',
); );
// `combobox` & `tags` should option be `string` type
if (mode === 'tags' || mode === 'combobox') {
const hasNumberValue = mergedOptions.some(item => {
if (item.options) {
return item.options.some(
(opt: OptionData) => typeof ('value' in opt ? opt.value : opt.key) === 'number',
);
}
return typeof ('value' in item ? item.value : item.key) === 'number';
});
warning(
!hasNumberValue,
'`value` of Option should not use number type when `mode` is `tags` or `combobox`.',
);
}
// `combobox` should not use `optionLabelProp` // `combobox` should not use `optionLabelProp`
warning( warning(
mode !== 'combobox' || !optionLabelProp, mode !== 'combobox' || !optionLabelProp,

View File

@ -0,0 +1,68 @@
import type {
FlattenDataNode,
InternalDataEntity,
Key,
LegacyDataNode,
RawValueType,
} from './interface';
import type { SkipType } from './hooks/useKeyValueMapping';
import type { ComputedRef, InjectionKey, PropType } from 'vue';
import { computed, defineComponent, inject, provide } from 'vue';
interface ContextProps {
checkable: boolean;
customCheckable: () => any;
checkedKeys: Key[];
halfCheckedKeys: Key[];
treeExpandedKeys: Key[];
treeDefaultExpandedKeys: Key[];
onTreeExpand: (keys: Key[]) => void;
treeDefaultExpandAll: boolean;
treeIcon: any;
showTreeIcon: boolean;
switcherIcon: any;
treeLine: boolean;
treeNodeFilterProp: string;
treeLoadedKeys: Key[];
treeMotion: any;
loadData: (treeNode: LegacyDataNode) => Promise<unknown>;
onTreeLoad: (loadedKeys: Key[]) => void;
// Cache help content. These can be generated by parent component.
// Let's reuse this.
getEntityByKey: (key: Key, skipType?: SkipType, ignoreDisabledCheck?: boolean) => FlattenDataNode;
getEntityByValue: (
value: RawValueType,
skipType?: SkipType,
ignoreDisabledCheck?: boolean,
) => FlattenDataNode;
slots: {
title?: (data: InternalDataEntity) => any;
titleRender?: (data: InternalDataEntity) => any;
[key: string]: (d: any) => any | undefined;
};
}
const SelectContextKey: InjectionKey<ComputedRef<ContextProps>> = Symbol('SelectContextKey');
export const SelectContext = defineComponent({
name: 'SelectContext',
props: {
value: { type: Object as PropType<ContextProps> },
},
setup(props, { slots }) {
provide(
SelectContextKey,
computed(() => props.value),
);
return () => slots.default?.();
},
});
export const useInjectTreeSelectContext = () => {
return inject(
SelectContextKey,
computed(() => ({} as ContextProps)),
);
};

View File

@ -0,0 +1,262 @@
import type { DataNode, TreeDataNode, Key } from './interface';
import { useInjectTreeSelectContext } from './Context';
import type { RefOptionListProps } from '../vc-select/OptionList';
import type { ScrollTo } from '../vc-virtual-list/List';
import { computed, defineComponent, nextTick, ref, watch } from 'vue';
import { optionListProps } from './props';
import useMemo from '../_util/hooks/useMemo';
import type { EventDataNode } from '../tree';
import KeyCode from '../_util/KeyCode';
import Tree from '../vc-tree/Tree';
import type { TreeProps } from '../vc-tree/props';
const HIDDEN_STYLE = {
width: 0,
height: 0,
display: 'flex',
overflow: 'hidden',
opacity: 0,
border: 0,
padding: 0,
margin: 0,
};
interface TreeEventInfo {
node: { key: Key };
selected?: boolean;
checked?: boolean;
}
type ReviseRefOptionListProps = Omit<RefOptionListProps, 'scrollTo'> & { scrollTo: ScrollTo };
export default defineComponent({
name: 'OptionList',
inheritAttrs: false,
props: optionListProps<DataNode>(),
slots: ['notFoundContent', 'menuItemSelectedIcon'],
setup(props, { slots, expose }) {
const context = useInjectTreeSelectContext();
const treeRef = ref();
const memoOptions = useMemo(
() => props.options,
[() => props.open, () => props.options],
(next, prev) => next[0] && prev[1] !== next[1],
);
const valueKeys = computed(() => {
const { checkedKeys, getEntityByValue } = context.value;
return checkedKeys.map(val => {
const entity = getEntityByValue(val);
return entity ? entity.key : null;
});
});
const mergedCheckedKeys = computed(() => {
const { checkable, halfCheckedKeys } = context.value;
if (!checkable) {
return null;
}
return {
checked: valueKeys.value,
halfChecked: halfCheckedKeys,
};
});
watch(
() => props.open,
() => {
nextTick(() => {
if (props.open && !props.multiple && valueKeys.value.length) {
treeRef.value?.scrollTo({ key: valueKeys.value[0] });
}
});
},
{ immediate: true, flush: 'post' },
);
// ========================== Search ==========================
const lowerSearchValue = computed(() => String(props.searchValue).toLowerCase());
const filterTreeNode = (treeNode: EventDataNode) => {
if (!lowerSearchValue.value) {
return false;
}
return String(treeNode[context.value.treeNodeFilterProp])
.toLowerCase()
.includes(lowerSearchValue.value);
};
// =========================== Keys ===========================
const expandedKeys = ref<Key[]>(context.value.treeDefaultExpandedKeys);
const searchExpandedKeys = ref<Key[]>(null);
watch(
() => props.searchValue,
() => {
if (props.searchValue) {
searchExpandedKeys.value = props.flattenOptions.map(o => o.key);
}
},
{
immediate: true,
},
);
const mergedExpandedKeys = computed(() => {
if (context.value.treeExpandedKeys) {
return [...context.value.treeExpandedKeys];
}
return props.searchValue ? searchExpandedKeys.value : expandedKeys.value;
});
const onInternalExpand = (keys: Key[]) => {
expandedKeys.value = keys;
searchExpandedKeys.value = keys;
context.value.onTreeExpand?.(keys);
};
// ========================== Events ==========================
const onListMouseDown = (event: MouseEvent) => {
event.preventDefault();
};
const onInternalSelect = (_: Key[], { node: { key } }: TreeEventInfo) => {
const { getEntityByKey, checkable, checkedKeys } = context.value;
const entity = getEntityByKey(key, checkable ? 'checkbox' : 'select');
if (entity !== null) {
props.onSelect?.(entity.data.value, {
selected: !checkedKeys.includes(entity.data.value),
});
}
if (!props.multiple) {
props.onToggleOpen?.(false);
}
};
// ========================= Keyboard =========================
const activeKey = ref<Key>(null);
const activeEntity = computed(() => context.value.getEntityByKey(activeKey.value));
const setActiveKey = (key: Key) => {
activeKey.value = key;
};
expose({
scrollTo: (...args: any[]) => treeRef.value?.scrollTo?.(...args),
onKeydown: (event: KeyboardEvent) => {
const { which } = event;
switch (which) {
// >>> Arrow keys
case KeyCode.UP:
case KeyCode.DOWN:
case KeyCode.LEFT:
case KeyCode.RIGHT:
treeRef.value?.onKeydown(event);
break;
// >>> Select item
case KeyCode.ENTER: {
const { selectable, value } = activeEntity.value?.data.node || {};
if (selectable !== false) {
onInternalSelect(null, {
node: { key: activeKey.value },
selected: !context.value.checkedKeys.includes(value),
});
}
break;
}
// >>> Close
case KeyCode.ESC: {
props.onToggleOpen(false);
}
}
},
onKeyup: () => {},
} as ReviseRefOptionListProps);
return () => {
const {
prefixCls,
height,
itemHeight,
virtual,
multiple,
searchValue,
open,
notFoundContent = slots.notFoundContent?.(),
onMouseenter,
} = props;
const {
checkable,
treeDefaultExpandAll,
treeIcon,
showTreeIcon,
switcherIcon,
treeLine,
loadData,
treeLoadedKeys,
treeMotion,
onTreeLoad,
} = context.value;
// ========================== Render ==========================
if (memoOptions.value.length === 0) {
return (
<div role="listbox" class={`${prefixCls}-empty`} onMousedown={onListMouseDown}>
{notFoundContent}
</div>
);
}
const treeProps: Partial<TreeProps> = {};
if (treeLoadedKeys) {
treeProps.loadedKeys = treeLoadedKeys;
}
if (mergedExpandedKeys.value) {
treeProps.expandedKeys = mergedExpandedKeys.value;
}
return (
<div onMousedown={onListMouseDown} onMouseenter={onMouseenter}>
{activeEntity.value && open && (
<span style={HIDDEN_STYLE} aria-live="assertive">
{activeEntity.value.data.value}
</span>
)}
<Tree
ref={treeRef}
focusable={false}
prefixCls={`${prefixCls}-tree`}
treeData={memoOptions.value as TreeDataNode[]}
height={height}
itemHeight={itemHeight}
virtual={virtual}
multiple={multiple}
icon={treeIcon}
showIcon={showTreeIcon}
switcherIcon={switcherIcon}
showLine={treeLine}
loadData={searchValue ? null : (loadData as any)}
motion={treeMotion}
// We handle keys by out instead tree self
checkable={checkable}
checkStrictly
checkedKeys={mergedCheckedKeys.value}
selectedKeys={!checkable ? valueKeys.value : []}
defaultExpandAll={treeDefaultExpandAll}
{...treeProps}
// Proxy event out
onActiveChange={setActiveKey}
onSelect={onInternalSelect}
onCheck={onInternalSelect as any}
onExpand={onInternalExpand}
onLoad={onTreeLoad}
filterTreeNode={filterTreeNode}
v-slots={{ ...slots, checkable: context.value.customCheckable }}
/>
</div>
);
};
},
});

View File

@ -0,0 +1,15 @@
/* istanbul ignore file */
import type { FunctionalComponent } from 'vue';
import type { DataNode, Key } from './interface';
export interface TreeNodeProps extends Omit<DataNode, 'children'> {
value: Key;
}
/** This is a placeholder, not real render in dom */
const TreeNode: FunctionalComponent<TreeNodeProps> & { isTreeSelectNode: boolean } = () => null;
TreeNode.inheritAttrs = false;
TreeNode.displayName = 'ATreeSelectNode';
TreeNode.isTreeSelectNode = true;
export default TreeNode;

View File

@ -0,0 +1,6 @@
import generate from './generate';
import OptionList from './OptionList';
const TreeSelect = generate({ prefixCls: 'vc-tree-select', optionList: OptionList as any });
export default TreeSelect;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,2 +0,0 @@
@import './select.less';
@import './tree.less';

Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 B

View File

@ -1,541 +0,0 @@
@selectPrefixCls: rc-tree-select;
.effect() {
animation-duration: 0.3s;
animation-fill-mode: both;
transform-origin: 0 0;
}
.@{selectPrefixCls} {
box-sizing: border-box;
display: inline-block;
position: relative;
vertical-align: middle;
color: #666;
&-allow-clear {
.@{selectPrefixCls}-selection--single .@{selectPrefixCls}-selection__rendered {
padding-right: 40px;
}
}
ul,
li {
margin: 0;
padding: 0;
list-style: none;
}
> ul > li > a {
padding: 0;
background-color: #fff;
}
// arrow
&-arrow {
height: 26px;
position: absolute;
top: 1px;
right: 1px;
width: 20px;
&:after {
content: '';
border-color: #999999 transparent transparent transparent;
border-style: solid;
border-width: 5px 4px 0 4px;
height: 0;
width: 0;
margin-left: -4px;
margin-top: -2px;
position: absolute;
top: 50%;
left: 50%;
}
}
&-selection {
outline: none;
user-select: none;
-webkit-user-select: none;
box-sizing: border-box;
display: block;
background-color: #fff;
border-radius: 6px;
border: 1px solid #d9d9d9;
&__clear {
font-weight: bold;
position: absolute;
}
}
&-enabled {
.@{selectPrefixCls}-selection {
&:hover {
border-color: #23c0fa;
box-shadow: 0 0 2px fadeout(#2db7f5, 20%);
}
&:active {
border-color: #2db7f5;
}
}
&.@{selectPrefixCls}-focused {
.@{selectPrefixCls}-selection {
//border-color: #23c0fa;
border-color: #7700fa;
box-shadow: 0 0 2px fadeout(#2db7f5, 20%);
}
}
}
&-selection--single {
height: 28px;
cursor: pointer;
position: relative;
.@{selectPrefixCls}-selection__rendered {
display: block;
padding-left: 10px;
padding-right: 20px;
line-height: 28px;
}
.@{selectPrefixCls}-selection-selected-value {
display: block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.@{selectPrefixCls}-selection__clear {
top: 5px;
right: 20px;
&:after {
content: '×';
}
}
}
&-disabled {
color: #ccc;
cursor: not-allowed;
.@{selectPrefixCls}-selection--single,
.@{selectPrefixCls}-selection__choice__remove {
cursor: not-allowed;
color: #ccc;
&:hover {
cursor: not-allowed;
color: #ccc;
}
}
}
&-search__field__wrap {
display: inline-block;
position: relative;
}
&-search__field__placeholder {
display: block;
position: absolute;
top: 0;
left: 3px;
color: #aaa;
}
&-search__field__mirror {
position: absolute;
top: 0;
left: -9999px;
white-space: pre;
pointer-events: none;
}
&-search--inline {
float: left;
width: 100%;
.@{selectPrefixCls}-search__field__wrap {
width: 100%;
}
.@{selectPrefixCls}-search__field {
border: none;
font-size: 100%;
//margin-top: 5px;
background: transparent;
outline: 0;
width: 100%;
}
> i {
float: right;
}
}
&-enabled&-selection--multiple {
cursor: text;
}
&-selection--multiple {
min-height: 28px;
.@{selectPrefixCls}-search--inline {
width: auto;
.@{selectPrefixCls}-search__field {
width: 0.75em;
}
}
.@{selectPrefixCls}-search__field__placeholder {
top: 5px;
left: 8px;
}
.@{selectPrefixCls}-selection__rendered {
//display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
padding-left: 8px;
padding-bottom: 2px;
padding-right: 10px;
}
> ul > li {
margin-top: 4px;
height: 20px;
line-height: 20px;
}
.@{selectPrefixCls}-selection__clear {
top: 5px;
right: 8px;
}
}
&-enabled {
.@{selectPrefixCls}-selection__choice {
cursor: default;
&:hover {
.@{selectPrefixCls}-selection__choice__remove {
opacity: 1;
transform: scale(1);
}
.@{selectPrefixCls}-selection__choice__remove
+ .@{selectPrefixCls}-selection__choice__content {
margin-left: -8px;
margin-right: 8px;
}
}
}
}
& &-selection__choice {
background-color: #f3f3f3;
border-radius: 4px;
float: left;
padding: 0 15px;
margin-right: 4px;
position: relative;
overflow: hidden;
transition: padding 0.3s cubic-bezier(0.6, -0.28, 0.735, 0.045),
width 0.3s cubic-bezier(0.6, -0.28, 0.735, 0.045);
&__content {
margin-left: 0;
margin-right: 0;
transition: margin 0.3s cubic-bezier(0.165, 0.84, 0.44, 1);
}
&-zoom-enter,
&-zoom-appear,
&-zoom-leave {
.effect();
opacity: 0;
animation-play-state: paused;
animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
&-zoom-leave {
opacity: 1;
animation-timing-function: cubic-bezier(0.6, -0.28, 0.735, 0.045);
}
&-zoom-enter.@{selectPrefixCls}-selection__choice-zoom-enter-active,
&-zoom-appear.@{selectPrefixCls}-selection__choice-zoom-appear-active {
animation-play-state: running;
animation-name: rcSelectChoiceZoomIn;
}
&-zoom-leave.@{selectPrefixCls}-selection__choice-zoom-leave-active {
animation-play-state: running;
animation-name: rcSelectChoiceZoomOut;
}
@keyframes rcSelectChoiceZoomIn {
0% {
transform: scale(0.6);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
}
@keyframes rcSelectChoiceZoomOut {
to {
transform: scale(0);
opacity: 0;
}
}
&__remove {
color: #919191;
cursor: pointer;
font-weight: bold;
padding: 0 0 0 8px;
position: absolute;
opacity: 0;
transform: scale(0);
top: 0;
right: 2px;
transition: opacity 0.3s, transform 0.3s;
&:before {
content: '×';
}
&:hover {
color: #333;
}
}
}
&-dropdown {
background-color: white;
border: 1px solid #d9d9d9;
box-shadow: 0 0px 4px #d9d9d9;
border-radius: 4px;
box-sizing: border-box;
z-index: 100;
left: -9999px;
top: -9999px;
//border-top: none;
//border-top-left-radius: 0;
//border-top-right-radius: 0;
position: absolute;
outline: none;
&-hidden {
display: none;
}
&-menu {
outline: none;
margin: 0;
padding: 0;
list-style: none;
z-index: 9999;
> li {
margin: 0;
padding: 0;
}
&-item-group-list {
margin: 0;
padding: 0;
> li.@{selectPrefixCls}-menu-item {
padding-left: 20px;
}
}
&-item-group-title {
color: #999;
line-height: 1.5;
padding: 8px 10px;
border-bottom: 1px solid #dedede;
}
li&-item {
margin: 0;
position: relative;
display: block;
padding: 7px 10px;
font-weight: normal;
color: #666666;
white-space: nowrap;
&-selected {
background-color: #ddd;
}
&-active {
background-color: #5897fb;
color: white;
cursor: pointer;
}
&-disabled {
color: #ccc;
cursor: not-allowed;
}
&-divider {
height: 1px;
margin: 1px 0;
overflow: hidden;
background-color: #e5e5e5;
line-height: 0;
}
}
}
&-slide-up-enter,
&-slide-up-appear {
.effect();
opacity: 0;
animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1);
animation-play-state: paused;
}
&-slide-up-leave {
.effect();
opacity: 1;
animation-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.34);
animation-play-state: paused;
}
&-slide-up-enter&-slide-up-enter-active&-placement-bottomLeft,
&-slide-up-appear&-slide-up-appear-active&-placement-bottomLeft {
animation-name: rcSelectDropdownSlideUpIn;
animation-play-state: running;
}
&-slide-up-leave&-slide-up-leave-active&-placement-bottomLeft {
animation-name: rcSelectDropdownSlideUpOut;
animation-play-state: running;
}
&-slide-up-enter&-slide-up-enter-active&-placement-topLeft,
&-slide-up-appear&-slide-up-appear-active&-placement-topLeft {
animation-name: rcSelectDropdownSlideDownIn;
animation-play-state: running;
}
&-slide-up-leave&-slide-up-leave-active&-placement-topLeft {
animation-name: rcSelectDropdownSlideDownOut;
animation-play-state: running;
}
@keyframes rcSelectDropdownSlideUpIn {
0% {
opacity: 0;
transform-origin: 0% 0%;
transform: scaleY(0);
}
100% {
opacity: 1;
transform-origin: 0% 0%;
transform: scaleY(1);
}
}
@keyframes rcSelectDropdownSlideUpOut {
0% {
opacity: 1;
transform-origin: 0% 0%;
transform: scaleY(1);
}
100% {
opacity: 0;
transform-origin: 0% 0%;
transform: scaleY(0);
}
}
@keyframes rcSelectDropdownSlideDownIn {
0% {
opacity: 0;
transform-origin: 0% 100%;
transform: scaleY(0);
}
100% {
opacity: 1;
transform-origin: 0% 100%;
transform: scaleY(1);
}
}
@keyframes rcSelectDropdownSlideDownOut {
0% {
opacity: 1;
transform-origin: 0% 100%;
transform: scaleY(1);
}
100% {
opacity: 0;
transform-origin: 0% 100%;
transform: scaleY(0);
}
}
}
&-dropdown-search {
display: block;
padding: 4px;
.@{selectPrefixCls}-search__field__wrap {
width: 100%;
}
.@{selectPrefixCls}-search__field__placeholder {
top: 4px;
}
.@{selectPrefixCls}-search__field {
padding: 4px;
width: 100%;
box-sizing: border-box;
border: 1px solid #d9d9d9;
border-radius: 4px;
outline: none;
}
&.@{selectPrefixCls}-search--hide {
display: none;
}
}
&-open {
.@{selectPrefixCls}-arrow:after {
border-color: transparent transparent #888 transparent;
border-width: 0 4px 5px 4px;
}
}
&-not-found {
display: inline-block;
padding: 8px;
}
}
.custom-icon-demo {
.@{selectPrefixCls} {
&-selection__choice__remove {
&:before {
content: '';
}
}
&-arrow {
&:after {
display: none;
}
}
&-selection__clear {
&:after {
content: '';
}
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,515 @@
import type { GenerateConfig } from '../vc-select/generate';
import generateSelector from '../vc-select/generate';
import TreeNode from './TreeNode';
import type {
DefaultValueType,
DataNode,
LabelValueType,
RawValueType,
ChangeEventExtra,
SelectSource,
FlattenDataNode,
} from './interface';
import {
flattenOptions,
filterOptions,
isValueDisabled,
findValueOption,
addValue,
removeValue,
getRawValueLabeled,
toArray,
fillFieldNames,
} from './utils/valueUtil';
import warningProps from './utils/warningPropsUtil';
import { SelectContext } from './Context';
import useTreeData from './hooks/useTreeData';
import useKeyValueMap from './hooks/useKeyValueMap';
import useKeyValueMapping from './hooks/useKeyValueMapping';
import { formatStrategyKeys, SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from './utils/strategyUtil';
import { fillAdditionalInfo } from './utils/legacyUtil';
import useSelectValues from './hooks/useSelectValues';
import type { TreeSelectProps } from './props';
import { treeSelectProps } from './props';
import { getLabeledValue } from '../vc-select/utils/valueUtil';
import omit from '../_util/omit';
import { computed, defineComponent, ref, toRef, watch, watchEffect } from 'vue';
import { convertDataToEntities } from '../vc-tree/utils/treeUtil';
import { conductCheck } from '../vc-tree/utils/conductUtil';
import { warning } from '../vc-util/warning';
import { INTERNAL_PROPS_MARK } from '../vc-select/interface/generator';
const OMIT_PROPS: (keyof TreeSelectProps)[] = [
'expandedKeys' as any,
'treeData',
'treeCheckable',
'showCheckedStrategy',
'searchPlaceholder',
'treeLine',
'treeIcon',
'showTreeIcon',
'switcherIcon',
'treeNodeFilterProp',
'filterTreeNode',
'dropdownPopupAlign',
'treeDefaultExpandAll',
'treeCheckStrictly',
'treeExpandedKeys',
'treeLoadedKeys',
'treeMotion',
'onTreeExpand',
'onTreeLoad',
'labelRender',
'loadData',
'treeDataSimpleMode',
'treeNodeLabelProp',
'treeDefaultExpandedKeys',
'bordered',
];
export default function generate(config: {
prefixCls: string;
optionList: GenerateConfig<DataNode>['components']['optionList'];
}) {
const { prefixCls, optionList } = config;
const RefSelect = generateSelector<DataNode>({
prefixCls,
components: {
optionList,
},
// Not use generate since we will handle ourself
convertChildrenToData: () => null,
flattenOptions,
// Handle `optionLabelProp` in TreeSelect component
getLabeledValue: getLabeledValue as any,
filterOptions,
isValueDisabled,
findValueOption,
omitDOMProps: (props: TreeSelectProps<any>) => omit(props, OMIT_PROPS),
});
return defineComponent({
name: 'TreeSelect',
props: treeSelectProps(),
slots: [
'title',
'placeholder',
'maxTagPlaceholder',
'treeIcon',
'switcherIcon',
'notFoundContent',
'treeCheckable',
],
TreeNode,
SHOW_ALL,
SHOW_PARENT,
SHOW_CHILD,
setup(props, { expose, slots, attrs }) {
const mergedCheckable = computed(() => props.treeCheckable || props.treeCheckStrictly);
const mergedMultiple = computed(() => props.multiple || mergedCheckable.value);
const treeConduction = computed(() => props.treeCheckable && !props.treeCheckStrictly);
const mergedLabelInValue = computed(() => props.treeCheckStrictly || props.labelInValue);
// ======================= Tree Data =======================
// FieldNames
const mergedFieldNames = computed(() => fillFieldNames(props.fieldNames, true));
// Legacy both support `label` or `title` if not set.
// We have to fallback to function to handle this
const getTreeNodeTitle = (node: DataNode) => {
if (!props.treeData) {
return node.title;
}
if (mergedFieldNames.value?.label) {
return node[mergedFieldNames.value.label];
}
return node.label || node.title;
};
const getTreeNodeLabelProp = (entity: FlattenDataNode) => {
const { labelRender, treeNodeLabelProp } = props;
const { node } = entity.data;
if (labelRender) {
return labelRender(entity);
}
if (treeNodeLabelProp) {
return node[treeNodeLabelProp];
}
return getTreeNodeTitle(node);
};
const mergedTreeData = useTreeData(toRef(props, 'treeData'), toRef(props, 'children'), {
getLabelProp: getTreeNodeTitle,
simpleMode: toRef(props, 'treeDataSimpleMode'),
fieldNames: mergedFieldNames,
});
const flattedOptions = computed(() => flattenOptions(mergedTreeData.value));
const [cacheKeyMap, cacheValueMap] = useKeyValueMap(flattedOptions);
const [getEntityByKey, getEntityByValue] = useKeyValueMapping(cacheKeyMap, cacheValueMap);
// Only generate keyEntities for check conduction when is `treeCheckable`
const conductKeyEntities = computed(() => {
if (treeConduction.value) {
return convertDataToEntities(mergedTreeData.value).keyEntities;
}
return null;
});
// ========================== Ref ==========================
const selectRef = ref();
expose({
scrollTo: (...args: any[]) => selectRef.value.scrollTo?.(...args),
focus: () => selectRef.value.focus?.(),
blur: () => selectRef.value?.blur(),
/** @private Internal usage. It's save to remove if `rc-cascader` not use it any longer */
getEntityByValue,
});
const valueRef = ref<DefaultValueType>(props.defaultValue);
watch(
() => props.value,
() => {
if (props.value !== undefined) {
valueRef.value = props.value;
}
},
{ immediate: true },
);
/** Get `missingRawValues` which not exist in the tree yet */
const splitRawValues = (newRawValues: RawValueType[]) => {
const missingRawValues = [];
const existRawValues = [];
// Keep missing value in the cache
newRawValues.forEach(val => {
if (getEntityByValue(val)) {
existRawValues.push(val);
} else {
missingRawValues.push(val);
}
});
return { missingRawValues, existRawValues };
};
const rawValues = ref<RawValueType[]>([]);
const rawHalfCheckedKeys = ref<RawValueType[]>([]);
watchEffect(() => {
const valueHalfCheckedKeys: RawValueType[] = [];
const newRawValues: RawValueType[] = [];
toArray(valueRef.value).forEach(item => {
if (item && typeof item === 'object' && 'value' in item) {
if (item.halfChecked && props.treeCheckStrictly) {
const entity = getEntityByValue(item.value);
valueHalfCheckedKeys.push(entity ? entity.key : item.value);
} else {
newRawValues.push(item.value);
}
} else {
newRawValues.push(item as RawValueType);
}
});
// We need do conduction of values
if (treeConduction.value) {
const { missingRawValues, existRawValues } = splitRawValues(newRawValues);
const keyList = existRawValues.map(val => getEntityByValue(val).key);
const { checkedKeys, halfCheckedKeys } = conductCheck(
keyList,
true,
conductKeyEntities.value,
);
rawValues.value = [
...missingRawValues,
...checkedKeys.map(key => getEntityByKey(key).data.value),
];
rawHalfCheckedKeys.value = halfCheckedKeys;
} else {
[rawValues.value, rawHalfCheckedKeys.value] = [newRawValues, valueHalfCheckedKeys];
}
});
const selectValues = useSelectValues(rawValues, {
treeConduction,
value: valueRef,
showCheckedStrategy: toRef(props, 'showCheckedStrategy'),
conductKeyEntities,
getEntityByValue,
getEntityByKey,
getLabelProp: getTreeNodeLabelProp,
});
const triggerChange = (
newRawValues: RawValueType[],
extra: { triggerValue: RawValueType; selected: boolean },
source: SelectSource,
) => {
const { onChange, showCheckedStrategy, treeCheckStrictly } = props;
const preValue = valueRef.value;
valueRef.value = mergedMultiple.value ? newRawValues : newRawValues[0];
if (onChange) {
let eventValues: RawValueType[] = newRawValues;
if (treeConduction.value && showCheckedStrategy !== 'SHOW_ALL') {
const keyList = newRawValues.map(val => {
const entity = getEntityByValue(val);
return entity ? entity.key : val;
});
const formattedKeyList = formatStrategyKeys(
keyList,
showCheckedStrategy,
conductKeyEntities.value,
);
eventValues = formattedKeyList.map(key => {
const entity = getEntityByKey(key);
return entity ? entity.data.value : key;
});
}
const { triggerValue, selected } = extra || {
triggerValue: undefined,
selected: undefined,
};
let returnValues = mergedLabelInValue.value
? getRawValueLabeled(eventValues, preValue, getEntityByValue, getTreeNodeLabelProp)
: eventValues;
// We need fill half check back
if (treeCheckStrictly) {
const halfValues = rawHalfCheckedKeys.value
.map(key => {
const entity = getEntityByKey(key);
return entity ? entity.data.value : key;
})
.filter(val => !eventValues.includes(val));
returnValues = [
...(returnValues as LabelValueType[]),
...getRawValueLabeled(halfValues, preValue, getEntityByValue, getTreeNodeLabelProp),
];
}
const additionalInfo = {
// [Legacy] Always return as array contains label & value
preValue: selectValues.value,
triggerValue,
} as ChangeEventExtra;
// [Legacy] Fill legacy data if user query.
// This is expansive that we only fill when user query
// https://github.com/react-component/tree-select/blob/fe33eb7c27830c9ac70cd1fdb1ebbe7bc679c16a/src/Select.jsx
let showPosition = true;
if (treeCheckStrictly || (source === 'selection' && !selected)) {
showPosition = false;
}
fillAdditionalInfo(
additionalInfo,
triggerValue,
newRawValues,
mergedTreeData.value,
showPosition,
);
if (mergedCheckable.value) {
additionalInfo.checked = selected;
} else {
additionalInfo.selected = selected;
}
onChange(
mergedMultiple.value ? returnValues : returnValues[0],
mergedLabelInValue.value
? null
: eventValues.map(val => {
const entity = getEntityByValue(val);
return entity ? entity.data.title : null;
}),
additionalInfo,
);
}
};
const onInternalSelect = (
selectValue: RawValueType,
option: DataNode,
source: SelectSource,
) => {
const eventValue = mergedLabelInValue.value ? selectValue : selectValue;
if (!mergedMultiple.value) {
// Single mode always set value
triggerChange([selectValue], { selected: true, triggerValue: selectValue }, source);
} else {
let newRawValues = addValue(rawValues.value, selectValue);
// Add keys if tree conduction
if (treeConduction.value) {
// Should keep missing values
const { missingRawValues, existRawValues } = splitRawValues(newRawValues);
const keyList = existRawValues.map(val => getEntityByValue(val).key);
const { checkedKeys } = conductCheck(keyList, true, conductKeyEntities.value);
newRawValues = [
...missingRawValues,
...checkedKeys.map(key => getEntityByKey(key).data.value),
];
}
triggerChange(newRawValues, { selected: true, triggerValue: selectValue }, source);
}
props.onSelect?.(eventValue, option);
};
const onInternalDeselect = (
selectValue: RawValueType,
option: DataNode,
source: SelectSource,
) => {
const eventValue = selectValue;
let newRawValues = removeValue(rawValues.value, selectValue);
// Remove keys if tree conduction
if (treeConduction.value) {
const { missingRawValues, existRawValues } = splitRawValues(newRawValues);
const keyList = existRawValues.map(val => getEntityByValue(val).key);
const { checkedKeys } = conductCheck(
keyList,
{ checked: false, halfCheckedKeys: rawHalfCheckedKeys.value },
conductKeyEntities.value,
);
newRawValues = [
...missingRawValues,
...checkedKeys.map(key => getEntityByKey(key).data.value),
];
}
triggerChange(newRawValues, { selected: false, triggerValue: selectValue }, source);
props.onDeselect?.(eventValue, option);
};
const onInternalClear = () => {
triggerChange([], null, 'clear');
};
// ========================= Open ==========================
const onInternalDropdownVisibleChange = (open: boolean) => {
if (props.onDropdownVisibleChange) {
const legacyParam = {};
Object.defineProperty(legacyParam, 'documentClickClose', {
get() {
warning(false, 'Second param of `onDropdownVisibleChange` has been removed.');
return false;
},
});
(props.onDropdownVisibleChange as any)(open, legacyParam);
}
};
// ======================== Warning ========================
if (process.env.NODE_ENV !== 'production') {
warningProps(props);
}
return () => {
const {
treeNodeFilterProp,
dropdownPopupAlign,
filterTreeNode,
treeDefaultExpandAll,
treeExpandedKeys,
treeDefaultExpandedKeys,
onTreeExpand,
treeIcon,
treeMotion,
showTreeIcon,
switcherIcon,
treeLine,
loadData,
treeLoadedKeys,
onTreeLoad,
} = props;
// ======================== Render =========================
// We pass some props into select props style
const selectProps = {
optionLabelProp: null,
optionFilterProp: treeNodeFilterProp,
dropdownAlign: dropdownPopupAlign,
internalProps: {
mark: INTERNAL_PROPS_MARK,
onClear: onInternalClear,
skipTriggerChange: true,
skipTriggerSelect: true,
onRawSelect: onInternalSelect,
onRawDeselect: onInternalDeselect,
},
filterOption: filterTreeNode,
};
if (props.filterTreeNode === undefined) {
delete selectProps.filterOption;
}
const selectContext = {
checkable: mergedCheckable.value,
loadData,
treeLoadedKeys,
onTreeLoad,
checkedKeys: rawValues.value,
halfCheckedKeys: rawHalfCheckedKeys.value,
treeDefaultExpandAll,
treeExpandedKeys,
treeDefaultExpandedKeys,
onTreeExpand,
treeIcon,
treeMotion,
showTreeIcon,
switcherIcon,
treeLine,
treeNodeFilterProp,
getEntityByKey,
getEntityByValue,
customCheckable: slots.treeCheckable,
slots,
};
return (
<SelectContext value={selectContext}>
<RefSelect
{...attrs}
ref={selectRef}
mode={mergedMultiple.value ? 'multiple' : null}
{...props}
{...selectProps}
value={selectValues.value}
// We will handle this ourself since we need calculate conduction
labelInValue
options={mergedTreeData.value}
onChange={null}
onSelect={null}
onDeselect={null}
onDropdownVisibleChange={onInternalDropdownVisibleChange}
v-slots={slots}
/>
</SelectContext>
);
};
},
});
}

View File

@ -0,0 +1,26 @@
import type { ComputedRef, Ref } from 'vue';
import { ref } from 'vue';
import { watchEffect } from 'vue';
import type { FlattenDataNode, Key, RawValueType } from '../interface';
/**
* Return cached Key Value map with DataNode.
* Only re-calculate when `flattenOptions` changed.
*/
export default function useKeyValueMap(flattenOptions: ComputedRef<FlattenDataNode[]>) {
const cacheKeyMap: Ref<Map<Key, FlattenDataNode>> = ref(new Map());
const cacheValueMap: Ref<Map<RawValueType, FlattenDataNode>> = ref(new Map());
watchEffect(() => {
const newCacheKeyMap = new Map();
const newCacheValueMap = new Map();
// Cache options by key
flattenOptions.value.forEach((dataNode: FlattenDataNode) => {
newCacheKeyMap.set(dataNode.key, dataNode);
newCacheValueMap.set(dataNode.data.value, dataNode);
});
cacheKeyMap.value = newCacheKeyMap;
cacheValueMap.value = newCacheValueMap;
});
return [cacheKeyMap, cacheValueMap];
}

View File

@ -0,0 +1,58 @@
import type { Ref } from 'vue';
import type { FlattenDataNode, Key, RawValueType } from '../interface';
export type SkipType = null | 'select' | 'checkbox';
export function isDisabled(dataNode: FlattenDataNode, skipType: SkipType): boolean {
if (!dataNode) {
return true;
}
const { disabled, disableCheckbox } = dataNode.data.node;
switch (skipType) {
case 'checkbox':
return disabled || disableCheckbox;
default:
return disabled;
}
}
export default function useKeyValueMapping(
cacheKeyMap: Ref<Map<Key, FlattenDataNode>>,
cacheValueMap: Ref<Map<RawValueType, FlattenDataNode>>,
): [
(key: Key, skipType?: SkipType, ignoreDisabledCheck?: boolean) => FlattenDataNode,
(value: RawValueType, skipType?: SkipType, ignoreDisabledCheck?: boolean) => FlattenDataNode,
] {
const getEntityByKey = (
key: Key,
skipType: SkipType = 'select',
ignoreDisabledCheck?: boolean,
) => {
const dataNode = cacheKeyMap.value.get(key);
if (!ignoreDisabledCheck && isDisabled(dataNode, skipType)) {
return null;
}
return dataNode;
};
const getEntityByValue = (
value: RawValueType,
skipType: SkipType = 'select',
ignoreDisabledCheck?: boolean,
) => {
const dataNode = cacheValueMap.value.get(value);
if (!ignoreDisabledCheck && isDisabled(dataNode, skipType)) {
return null;
}
return dataNode;
};
return [getEntityByKey, getEntityByValue];
}

View File

@ -0,0 +1,67 @@
import type { RawValueType, FlattenDataNode, Key, LabelValueType } from '../interface';
import type { SkipType } from './useKeyValueMapping';
import { getRawValueLabeled } from '../utils/valueUtil';
import type { CheckedStrategy } from '../utils/strategyUtil';
import { formatStrategyKeys } from '../utils/strategyUtil';
import type { DefaultValueType } from '../../vc-select/interface/generator';
import type { DataEntity } from '../../vc-tree/interface';
import type { Ref } from 'vue';
import { ref, watchEffect } from 'vue';
interface Config {
treeConduction: Ref<boolean>;
/** Current `value` of TreeSelect */
value: Ref<DefaultValueType>;
showCheckedStrategy: Ref<CheckedStrategy>;
conductKeyEntities: Ref<Record<Key, DataEntity>>;
getEntityByKey: (key: Key, skipType?: SkipType, ignoreDisabledCheck?: boolean) => FlattenDataNode;
getEntityByValue: (
value: RawValueType,
skipType?: SkipType,
ignoreDisabledCheck?: boolean,
) => FlattenDataNode;
getLabelProp: (entity: FlattenDataNode) => any;
}
/** Return */
export default function useSelectValues(
rawValues: Ref<RawValueType[]>,
{
value,
getEntityByValue,
getEntityByKey,
treeConduction,
showCheckedStrategy,
conductKeyEntities,
getLabelProp,
}: Config,
): Ref<LabelValueType[]> {
const rawValueLabeled = ref([]);
watchEffect(() => {
let mergedRawValues = rawValues.value;
if (treeConduction.value) {
const rawKeys = formatStrategyKeys(
rawValues.value.map(val => {
const entity = getEntityByValue(val);
return entity ? entity.key : val;
}),
showCheckedStrategy.value,
conductKeyEntities.value,
);
mergedRawValues = rawKeys.map(key => {
const entity = getEntityByKey(key);
return entity ? entity.data.value : key;
});
}
rawValueLabeled.value = getRawValueLabeled(
mergedRawValues,
value.value,
getEntityByValue,
getLabelProp,
);
});
return rawValueLabeled;
}

View File

@ -0,0 +1,152 @@
import { warning } from '../../vc-util/warning';
import type { ComputedRef, Ref } from 'vue';
import { computed } from 'vue';
import type {
DataNode,
InternalDataEntity,
SimpleModeConfig,
RawValueType,
FieldNames,
} from '../interface';
import { convertChildrenToData } from '../utils/legacyUtil';
const MAX_WARNING_TIMES = 10;
function parseSimpleTreeData(
treeData: DataNode[],
{ id, pId, rootPId }: SimpleModeConfig,
): DataNode[] {
const keyNodes = {};
const rootNodeList = [];
// Fill in the map
const nodeList = treeData.map(node => {
const clone = { ...node };
const key = clone[id];
keyNodes[key] = clone;
clone.key = clone.key || key;
return clone;
});
// Connect tree
nodeList.forEach(node => {
const parentKey = node[pId];
const parent = keyNodes[parentKey];
// Fill parent
if (parent) {
parent.children = parent.children || [];
parent.children.push(node);
}
// Fill root tree node
if (parentKey === rootPId || (!parent && rootPId === null)) {
rootNodeList.push(node);
}
});
return rootNodeList;
}
/**
* Format `treeData` with `value` & `key` which is used for calculation
*/
function formatTreeData(
treeData: DataNode[],
getLabelProp: (node: DataNode) => any,
fieldNames: FieldNames,
): InternalDataEntity[] {
let warningTimes = 0;
const valueSet = new Set<RawValueType>();
// Field names
const { value: fieldValue, children: fieldChildren } = fieldNames;
function dig(dataNodes: DataNode[]) {
return (dataNodes || []).map(node => {
const { key, disableCheckbox, disabled } = node;
const value = node[fieldValue];
const mergedValue = fieldValue in node ? value : key;
const dataNode: InternalDataEntity = {
disableCheckbox,
disabled,
key: key !== null && key !== undefined ? key : mergedValue,
value: mergedValue,
title: getLabelProp(node),
node,
dataRef: node,
};
if (node.slots) {
dataNode.slots = node.slots;
}
// Check `key` & `value` and warning user
if (process.env.NODE_ENV !== 'production') {
if (
key !== null &&
key !== undefined &&
value !== undefined &&
String(key) !== String(value) &&
warningTimes < MAX_WARNING_TIMES
) {
warningTimes += 1;
warning(
false,
`\`key\` or \`value\` with TreeNode must be the same or you can remove one of them. key: ${key}, value: ${value}.`,
);
}
warning(!valueSet.has(value), `Same \`value\` exist in the tree: ${value}`);
valueSet.add(value);
}
if (fieldChildren in node) {
dataNode.children = dig(node[fieldChildren]);
}
return dataNode;
});
}
return dig(treeData);
}
/**
* Convert `treeData` or `children` into formatted `treeData`.
* Will not re-calculate if `treeData` or `children` not change.
*/
export default function useTreeData(
treeData: Ref<DataNode[]>,
children: Ref<any[]>,
{
getLabelProp,
simpleMode,
fieldNames,
}: {
getLabelProp: (node: DataNode) => any;
simpleMode: Ref<boolean | SimpleModeConfig>;
fieldNames: Ref<FieldNames>;
},
): ComputedRef<InternalDataEntity[]> {
return computed(() => {
if (treeData.value) {
return formatTreeData(
simpleMode.value
? parseSimpleTreeData(treeData.value, {
id: 'id',
pId: 'pId',
rootPId: null,
...(simpleMode.value !== true ? simpleMode.value : {}),
})
: treeData.value,
getLabelProp,
fieldNames.value,
);
} else {
return formatTreeData(convertChildrenToData(children.value), getLabelProp, fieldNames.value);
}
});
}

View File

@ -1,6 +0,0 @@
// export this package's api
// base 2.9.3
import TreeSelect from './src';
export default TreeSelect;
export { TreeNode, SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from './src';

View File

@ -0,0 +1,8 @@
import TreeSelect from './TreeSelect';
import TreeNode from './TreeNode';
import { SHOW_ALL, SHOW_CHILD, SHOW_PARENT } from './utils/strategyUtil';
import { TreeSelectProps, treeSelectProps } from './props';
export { TreeNode, SHOW_ALL, SHOW_CHILD, SHOW_PARENT, TreeSelectProps, treeSelectProps };
export default TreeSelect;

View File

@ -0,0 +1,97 @@
export type SelectSource = 'option' | 'selection' | 'input' | 'clear';
export type Key = string | number;
export type RawValueType = string | number;
export interface LabelValueType {
key?: Key;
value?: RawValueType;
label?: any;
/** Only works on `treeCheckStrictly` */
halfChecked?: boolean;
}
export type DefaultValueType = RawValueType | RawValueType[] | LabelValueType | LabelValueType[];
export interface DataNode {
value?: RawValueType;
title?: any;
label?: any;
key?: Key;
disabled?: boolean;
disableCheckbox?: boolean;
checkable?: boolean;
children?: DataNode[];
/** Customize data info */
[prop: string]: any;
}
export interface InternalDataEntity {
key: Key;
value: RawValueType;
title?: any;
disableCheckbox?: boolean;
disabled?: boolean;
children?: InternalDataEntity[];
/** Origin DataNode */
node: DataNode;
dataRef: DataNode;
slots?: Record<string, string>; // 兼容 V2
}
export interface LegacyDataNode extends DataNode {
props: any;
}
export interface TreeDataNode extends DataNode {
key: Key;
children?: TreeDataNode[];
}
export interface FlattenDataNode {
data: InternalDataEntity;
key: Key;
value: RawValueType;
level: number;
parent?: FlattenDataNode;
}
export interface SimpleModeConfig {
id?: Key;
pId?: Key;
rootPId?: Key;
}
/** @deprecated This is only used for legacy compatible. Not works on new code. */
export interface LegacyCheckedNode {
pos: string;
node: any;
children?: LegacyCheckedNode[];
}
export interface ChangeEventExtra {
/** @deprecated Please save prev value by control logic instead */
preValue: LabelValueType[];
triggerValue: RawValueType;
/** @deprecated Use `onSelect` or `onDeselect` instead. */
selected?: boolean;
/** @deprecated Use `onSelect` or `onDeselect` instead. */
checked?: boolean;
// Not sure if exist user still use this. We have to keep but not recommend user to use
/** @deprecated This prop not work as react node anymore. */
triggerNode: any;
/** @deprecated This prop not work as react node anymore. */
allCheckedNodes: LegacyCheckedNode[];
}
export interface FieldNames {
value?: string;
label?: string;
children?: string;
}

View File

@ -0,0 +1,140 @@
import type { ExtractPropTypes, PropType } from 'vue';
import type { DataNode } from './interface';
import { selectBaseProps } from '../vc-select';
import type { FilterFunc } from '../vc-select/interface/generator';
import omit from '../_util/omit';
import type { Key } from '../_util/type';
import PropTypes from '../_util/vue-types';
import type {
ChangeEventExtra,
DefaultValueType,
FieldNames,
FlattenDataNode,
LabelValueType,
LegacyDataNode,
RawValueType,
SimpleModeConfig,
} from './interface';
import type { CheckedStrategy } from './utils/strategyUtil';
export function optionListProps<OptionsType>() {
return {
prefixCls: String,
id: String,
options: { type: Array as PropType<OptionsType[]> },
flattenOptions: { type: Array as PropType<FlattenDataNode[]> },
height: Number,
itemHeight: Number,
virtual: { type: Boolean, default: undefined },
values: { type: Set as PropType<Set<RawValueType>> },
multiple: { type: Boolean, default: undefined },
open: { type: Boolean, default: undefined },
defaultActiveFirstOption: { type: Boolean, default: undefined },
notFoundContent: PropTypes.any,
menuItemSelectedIcon: PropTypes.any,
childrenAsData: { type: Boolean, default: undefined },
searchValue: String,
onSelect: {
type: Function as PropType<(value: RawValueType, option: { selected: boolean }) => void>,
},
onToggleOpen: { type: Function as PropType<(open?: boolean) => void> },
/** Tell Select that some value is now active to make accessibility work */
onActiveValue: { type: Function as PropType<(value: RawValueType, index: number) => void> },
onScroll: { type: Function as PropType<(e: UIEvent) => void> },
onMouseenter: { type: Function as PropType<() => void> },
};
}
export function treeSelectProps<ValueType = DefaultValueType>() {
const selectProps = omit(selectBaseProps<DataNode, ValueType>(), [
'onChange',
'mode',
'menuItemSelectedIcon',
'dropdownAlign',
'backfill',
'getInputElement',
'optionLabelProp',
'tokenSeparators',
'filterOption',
]);
return {
...selectProps,
multiple: { type: Boolean, default: undefined },
showArrow: { type: Boolean, default: undefined },
showSearch: { type: Boolean, default: undefined },
open: { type: Boolean, default: undefined },
defaultOpen: { type: Boolean, default: undefined },
value: { type: [String, Number, Object, Array] as PropType<ValueType> },
defaultValue: { type: [String, Number, Object, Array] as PropType<ValueType> },
disabled: { type: Boolean, default: undefined },
placeholder: PropTypes.any,
/** @deprecated Use `searchValue` instead */
inputValue: String,
searchValue: String,
autoClearSearchValue: { type: Boolean, default: undefined },
maxTagPlaceholder: { type: Function as PropType<(omittedValues: LabelValueType[]) => any> },
fieldNames: { type: Object as PropType<FieldNames> },
loadData: { type: Function as PropType<(dataNode: LegacyDataNode) => Promise<unknown>> },
treeNodeFilterProp: String,
treeNodeLabelProp: String,
treeDataSimpleMode: {
type: [Boolean, Object] as PropType<boolean | SimpleModeConfig>,
default: undefined,
},
treeExpandedKeys: { type: Array as PropType<Key[]> },
treeDefaultExpandedKeys: { type: Array as PropType<Key[]> },
treeLoadedKeys: { type: Array as PropType<Key[]> },
treeCheckable: { type: Boolean, default: undefined },
treeCheckStrictly: { type: Boolean, default: undefined },
showCheckedStrategy: { type: String as PropType<CheckedStrategy> },
treeDefaultExpandAll: { type: Boolean, default: undefined },
treeData: { type: Array as PropType<DataNode[]> },
treeLine: { type: Boolean, default: undefined },
treeIcon: PropTypes.any,
showTreeIcon: { type: Boolean, default: undefined },
switcherIcon: PropTypes.any,
treeMotion: PropTypes.any,
children: Array,
filterTreeNode: {
type: [Boolean, Function] as PropType<boolean | FilterFunc<LegacyDataNode>>,
default: undefined,
},
dropdownPopupAlign: PropTypes.any,
// Event
onSearch: { type: Function as PropType<(value: string) => void> },
onChange: {
type: Function as PropType<
(value: ValueType, labelList: any[], extra: ChangeEventExtra) => void
>,
},
onTreeExpand: { type: Function as PropType<(expandedKeys: Key[]) => void> },
onTreeLoad: { type: Function as PropType<(loadedKeys: Key[]) => void> },
onDropdownVisibleChange: { type: Function as PropType<(open: boolean) => void> },
// Legacy
/** `searchPlaceholder` has been removed since search box has been merged into input box */
searchPlaceholder: PropTypes.any,
/** @private This is not standard API since we only used in `rc-cascader`. Do not use in your production */
labelRender: { type: Function as PropType<(entity: FlattenDataNode) => any> },
};
}
class Helper<T> {
ReturnOptionListProps = optionListProps<T>();
ReturnTreeSelectProps = treeSelectProps<T>();
}
export type OptionListProps = Partial<ExtractPropTypes<Helper<DataNode>['ReturnOptionListProps']>>;
export type TreeSelectProps<T = DefaultValueType> = Partial<
ExtractPropTypes<Helper<T>['ReturnTreeSelectProps']>
>;

View File

@ -1,285 +0,0 @@
import { inject } from 'vue';
import warning from 'warning';
import PropTypes from '../../../_util/vue-types';
import Tree from '../../../vc-tree';
import BaseMixin from '../../../_util/BaseMixin';
import { createRef } from '../util';
// export const popupContextTypes = {
// onPopupKeyDown: PropTypes.func.isRequired,
// onTreeNodeSelect: PropTypes.func.isRequired,
// onTreeNodeCheck: PropTypes.func.isRequired,
// }
function getDerivedState(nextProps, prevState) {
const {
_prevProps: prevProps = {},
_loadedKeys: loadedKeys,
_expandedKeyList: expandedKeyList,
_cachedExpandedKeyList: cachedExpandedKeyList,
} = prevState || {};
const {
valueList,
valueEntities,
keyEntities,
treeExpandedKeys,
filteredTreeNodes,
upperSearchValue,
} = nextProps;
const newState = {
_prevProps: { ...nextProps },
};
// Check value update
if (valueList !== prevProps.valueList) {
newState._keyList = valueList
.map(({ value }) => valueEntities[value])
.filter(entity => entity)
.map(({ key }) => key);
}
// Show all when tree is in filter mode
if (
!treeExpandedKeys &&
filteredTreeNodes &&
filteredTreeNodes.length &&
filteredTreeNodes !== prevProps.filteredTreeNodes
) {
newState._expandedKeyList = [...keyEntities.keys()];
}
// Cache `expandedKeyList` when filter set
if (upperSearchValue && !prevProps.upperSearchValue) {
newState._cachedExpandedKeyList = expandedKeyList;
} else if (!upperSearchValue && prevProps.upperSearchValue && !treeExpandedKeys) {
newState._expandedKeyList = cachedExpandedKeyList || [];
newState._cachedExpandedKeyList = [];
}
// Use expandedKeys if provided
if (prevProps.treeExpandedKeys !== treeExpandedKeys) {
newState._expandedKeyList = treeExpandedKeys;
}
// Clean loadedKeys if key not exist in keyEntities anymore
if (nextProps.loadData) {
newState._loadedKeys = loadedKeys.filter(key => keyEntities.has(key));
}
return newState;
}
const BasePopup = {
mixins: [BaseMixin],
inheritAttrs: false,
name: 'BasePopup',
props: {
prefixCls: PropTypes.string,
upperSearchValue: PropTypes.string,
valueList: PropTypes.array,
searchHalfCheckedKeys: PropTypes.array,
valueEntities: PropTypes.object,
keyEntities: Map,
treeIcon: PropTypes.looseBool,
treeLine: PropTypes.looseBool,
treeNodeFilterProp: PropTypes.string,
treeCheckable: PropTypes.any,
treeCheckStrictly: PropTypes.looseBool,
treeDefaultExpandAll: PropTypes.looseBool,
treeDefaultExpandedKeys: PropTypes.array,
treeExpandedKeys: PropTypes.array,
loadData: PropTypes.func,
multiple: PropTypes.looseBool,
// onTreeExpand: PropTypes.func,
searchValue: PropTypes.string,
treeNodes: PropTypes.any,
filteredTreeNodes: PropTypes.any,
notFoundContent: PropTypes.any,
ariaId: PropTypes.string,
switcherIcon: PropTypes.any,
// HOC
renderSearch: PropTypes.func,
// onTreeExpanded: PropTypes.func,
__propsSymbol__: PropTypes.any,
},
setup() {
return {
vcTreeSelect: inject('vcTreeSelect', {}),
};
},
watch: {
__propsSymbol__() {
const state = getDerivedState(this.$props, this.$data);
this.setState(state);
},
},
data() {
this.treeRef = createRef();
warning(this.$props.__propsSymbol__, 'must pass __propsSymbol__');
const { treeDefaultExpandAll, treeDefaultExpandedKeys, keyEntities } = this.$props;
// TODO: make `expandedKeyList` control
let expandedKeyList = treeDefaultExpandedKeys;
if (treeDefaultExpandAll) {
expandedKeyList = [...keyEntities.keys()];
}
const state = {
_keyList: [],
_expandedKeyList: expandedKeyList,
// Cache `expandedKeyList` when tree is in filter. This is used in `getDerivedState`
_cachedExpandedKeyList: [],
_loadedKeys: [],
_prevProps: {},
};
return {
...state,
...getDerivedState(this.$props, state),
};
},
methods: {
onTreeExpand(expandedKeyList) {
const { treeExpandedKeys } = this.$props;
// Set uncontrolled state
if (!treeExpandedKeys) {
this.setState({ _expandedKeyList: expandedKeyList }, () => {
this.__emit('treeExpanded');
});
}
this.__emit('treeExpand', expandedKeyList);
},
onLoad(loadedKeys) {
this.setState({ _loadedKeys: loadedKeys });
},
getTree() {
return this.treeRef.current;
},
/**
* Not pass `loadData` when searching. To avoid loop ajax call makes browser crash.
*/
getLoadData() {
const { loadData, upperSearchValue } = this.$props;
if (upperSearchValue) return null;
return loadData;
},
/**
* This method pass to Tree component which is used for add filtered class
* in TreeNode > li
*/
filterTreeNode(treeNode) {
const { upperSearchValue, treeNodeFilterProp } = this.$props;
const filterVal = treeNode[treeNodeFilterProp];
if (typeof filterVal === 'string') {
return upperSearchValue && filterVal.toUpperCase().indexOf(upperSearchValue) !== -1;
}
return false;
},
renderNotFound() {
const { prefixCls, notFoundContent } = this.$props;
return <span class={`${prefixCls}-not-found`}>{notFoundContent}</span>;
},
},
render() {
const {
_keyList: keyList,
_expandedKeyList: expandedKeyList,
_loadedKeys: loadedKeys,
} = this.$data;
const {
prefixCls,
treeNodes,
filteredTreeNodes,
treeIcon,
treeLine,
treeCheckable,
treeCheckStrictly,
multiple,
ariaId,
renderSearch,
switcherIcon,
searchHalfCheckedKeys,
} = this.$props;
const {
vcTreeSelect: { onPopupKeyDown, onTreeNodeSelect, onTreeNodeCheck },
} = this;
const loadData = this.getLoadData();
const treeProps = {};
if (treeCheckable) {
treeProps.checkedKeys = keyList;
} else {
treeProps.selectedKeys = keyList;
}
let $notFound;
let $treeNodes;
if (filteredTreeNodes) {
if (filteredTreeNodes.length) {
treeProps.checkStrictly = true;
$treeNodes = filteredTreeNodes;
// Fill halfCheckedKeys
if (treeCheckable && !treeCheckStrictly) {
treeProps.checkedKeys = {
checked: keyList,
halfChecked: searchHalfCheckedKeys,
};
}
} else {
$notFound = this.renderNotFound();
}
} else if (!treeNodes || !treeNodes.length) {
$notFound = this.renderNotFound();
} else {
$treeNodes = treeNodes;
}
let $tree;
if ($notFound) {
$tree = $notFound;
} else {
const treeAllProps = {
prefixCls: `${prefixCls}-tree`,
showIcon: treeIcon,
showLine: treeLine,
selectable: !treeCheckable,
checkable: treeCheckable,
checkStrictly: treeCheckStrictly,
multiple,
loadData,
loadedKeys,
expandedKeys: expandedKeyList,
filterTreeNode: this.filterTreeNode,
switcherIcon,
...treeProps,
children: $treeNodes,
onSelect: onTreeNodeSelect,
onCheck: onTreeNodeCheck,
onExpand: this.onTreeExpand,
onLoad: this.onLoad,
};
$tree = <Tree {...treeAllProps} ref={this.treeRef} __propsSymbol__={[]} />;
}
return (
<div role="listbox" id={ariaId} onKeydown={onPopupKeyDown} tabindex={-1}>
{renderSearch ? renderSearch() : null}
{$tree}
</div>
);
},
};
export default BasePopup;

View File

@ -1,201 +0,0 @@
/**
* Input Box is in different position for different mode.
* This not the same design as `Select` cause it's followed by antd 0.x `Select`.
* We will not follow the new design immediately since antd 3.x is already released.
*
* So this file named as Selector to avoid confuse.
*/
import { inject } from 'vue';
import { createRef } from '../util';
import PropTypes from '../../../_util/vue-types';
import classNames from '../../../_util/classNames';
import { initDefaultProps, getComponent } from '../../../_util/props-util';
import BaseMixin from '../../../_util/BaseMixin';
export const selectorPropTypes = () => ({
prefixCls: PropTypes.string,
open: PropTypes.looseBool,
selectorValueList: PropTypes.array,
allowClear: PropTypes.looseBool,
showArrow: PropTypes.looseBool,
// onClick: PropTypes.func,
// onBlur: PropTypes.func,
// onFocus: PropTypes.func,
removeSelected: PropTypes.func,
choiceTransitionName: PropTypes.string,
// Pass by component
ariaId: PropTypes.string,
inputIcon: PropTypes.any,
clearIcon: PropTypes.any,
removeIcon: PropTypes.any,
placeholder: PropTypes.any,
disabled: PropTypes.looseBool,
focused: PropTypes.looseBool,
isMultiple: PropTypes.looseBool,
showSearch: PropTypes.looseBool,
searchValue: PropTypes.string,
});
function noop() {}
export default function () {
const BaseSelector = {
name: 'BaseSelector',
inheritAttrs: false,
mixins: [BaseMixin],
props: initDefaultProps(
{
...selectorPropTypes(),
// Pass by HOC
renderSelection: PropTypes.func.isRequired,
renderPlaceholder: PropTypes.func,
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
},
{
tabindex: 0,
},
),
setup() {
return {
vcTreeSelect: inject('vcTreeSelect', {}),
};
},
created() {
this.domRef = createRef();
},
methods: {
onFocus(e) {
const { focused } = this.$props;
const {
vcTreeSelect: { onSelectorFocus },
} = this;
if (!focused) {
onSelectorFocus();
}
this.__emit('focus', e);
},
onBlur(e) {
const {
vcTreeSelect: { onSelectorBlur },
} = this;
// TODO: Not trigger when is inner component get focused
onSelectorBlur();
this.__emit('blur', e);
},
focus() {
this.domRef.current.focus();
},
blur() {
this.domRef.current.blur();
},
renderClear() {
const { prefixCls, allowClear, selectorValueList } = this.$props;
const {
vcTreeSelect: { onSelectorClear },
} = this;
if (!allowClear || !selectorValueList.length || !selectorValueList[0].value) {
return null;
}
const clearIcon = getComponent(this, 'clearIcon');
return (
<span
key="clear"
unselectable="on"
aria-hidden="true"
style="user-select: none;"
class={`${prefixCls}-clear`}
onClick={onSelectorClear}
>
{clearIcon}
</span>
);
},
renderArrow() {
const { prefixCls, showArrow } = this.$props;
if (!showArrow) {
return null;
}
const inputIcon = getComponent(this, 'inputIcon');
return (
<span
key="arrow"
class={`${prefixCls}-arrow`}
style={{ outline: 'none', userSelect: 'none' }}
>
{inputIcon}
</span>
);
},
},
render() {
const {
prefixCls,
open,
focused,
disabled,
allowClear,
ariaId,
renderSelection,
renderPlaceholder,
tabindex,
isMultiple,
showArrow,
showSearch,
} = this.$props;
const { class: className, style, onClick = noop } = this.$attrs;
const {
vcTreeSelect: { onSelectorKeyDown },
} = this;
let myTabIndex = tabindex;
if (disabled) {
myTabIndex = null;
}
const mergedClassName = classNames(prefixCls, className, {
[`${prefixCls}-focused`]: open || focused,
[`${prefixCls}-multiple`]: isMultiple,
[`${prefixCls}-single`]: !isMultiple,
[`${prefixCls}-allow-clear`]: allowClear,
[`${prefixCls}-show-arrow`]: showArrow,
[`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-open`]: open,
[`${prefixCls}-show-search`]: showSearch,
});
return (
<div
style={style}
onClick={onClick}
class={mergedClassName}
ref={this.domRef}
role="combobox"
aria-expanded={open}
aria-owns={open ? ariaId : undefined}
aria-controls={open ? ariaId : undefined}
aria-haspopup="listbox"
aria-disabled={disabled}
tabindex={myTabIndex}
onFocus={this.onFocus}
onBlur={this.onBlur}
onKeydown={onSelectorKeyDown}
>
<span class={`${prefixCls}-selector`}>
{renderSelection()}
{renderPlaceholder && renderPlaceholder()}
</span>
{this.renderArrow()}
{this.renderClear()}
</div>
);
},
};
return BaseSelector;
}

View File

@ -1,3 +0,0 @@
import BasePopup from '../Base/BasePopup';
export default BasePopup;

View File

@ -1,82 +0,0 @@
import PropTypes from '../../../_util/vue-types';
import BasePopup from '../Base/BasePopup';
import SearchInput from '../SearchInput';
import { createRef } from '../util';
const SinglePopup = {
name: 'SinglePopup',
inheritAttrs: false,
props: {
...BasePopup.props,
...SearchInput.props,
searchValue: PropTypes.string,
showSearch: PropTypes.looseBool,
dropdownPrefixCls: PropTypes.string,
disabled: PropTypes.looseBool,
searchPlaceholder: PropTypes.string,
},
created() {
this.inputRef = createRef();
this.searchRef = createRef();
this.popupRef = createRef();
},
methods: {
onPlaceholderClick() {
this.inputRef.current.focus();
},
getTree() {
return this.popupRef.current && this.popupRef.current.getTree();
},
_renderPlaceholder() {
const { searchPlaceholder, searchValue, prefixCls } = this.$props;
if (!searchPlaceholder) {
return null;
}
return (
<span
style={{
display: searchValue ? 'none' : 'block',
}}
onClick={this.onPlaceholderClick}
class={`${prefixCls}-selection-placeholder`}
>
{searchPlaceholder}
</span>
);
},
_renderSearch() {
const { showSearch, dropdownPrefixCls } = this.$props;
if (!showSearch) {
return null;
}
return (
<span class={`${dropdownPrefixCls}-search`} ref={this.searchRef}>
<SearchInput
{...{ ...this.$props, ...this.$attrs, renderPlaceholder: this._renderPlaceholder }}
ref={this.inputRef}
/>
</span>
);
},
},
render() {
return (
<BasePopup
{...{
...this.$props,
...this.$attrs,
renderSearch: this._renderSearch,
}}
ref={this.popupRef}
__propsSymbol__={[]}
/>
);
},
};
export default SinglePopup;

View File

@ -1,170 +0,0 @@
/**
* Since search box is in different position with different mode.
* - Single: in the popup box
* - multiple: in the selector
* Move the code as a SearchInput for easy management.
*/
import BaseInput from '../../_util/BaseInput';
import { inject, ref, onMounted, computed, watch } from 'vue';
import PropTypes from '../../_util/vue-types';
import { createRef } from './util';
const SearchInput = {
name: 'SearchInput',
inheritAttrs: false,
props: {
open: PropTypes.looseBool,
searchValue: PropTypes.string,
prefixCls: PropTypes.string,
disabled: PropTypes.looseBool,
renderPlaceholder: PropTypes.func,
needAlign: PropTypes.looseBool,
ariaId: PropTypes.string,
isMultiple: PropTypes.looseBool.def(true),
showSearch: PropTypes.looseBool,
},
emits: ['mirrorSearchValueChange'],
setup(props, { emit }) {
const measureRef = ref();
const inputWidth = ref(0);
const mirrorSearchValue = ref(props.searchValue);
watch(
computed(() => props.searchValue),
() => {
mirrorSearchValue.value = props.searchValue;
},
);
watch(
mirrorSearchValue,
() => {
emit('mirrorSearchValueChange', mirrorSearchValue.value);
},
{ immediate: true },
);
// We measure width and set to the input immediately
onMounted(() => {
if (props.isMultiple) {
watch(
mirrorSearchValue,
() => {
inputWidth.value = measureRef.value.scrollWidth;
},
{ flush: 'post', immediate: true },
);
}
});
return {
measureRef,
inputWidth,
vcTreeSelect: inject('vcTreeSelect', {}),
mirrorSearchValue,
};
},
created() {
this.inputRef = createRef();
this.prevProps = { ...this.$props };
},
mounted() {
this.$nextTick(() => {
const { open } = this.$props;
if (open) {
this.focus(true);
}
});
},
updated() {
const { open } = this.$props;
const { prevProps } = this;
this.$nextTick(() => {
if (open && prevProps.open !== open) {
this.focus();
}
this.prevProps = { ...this.$props };
});
},
methods: {
/**
* Need additional timeout for focus cause parent dom is not ready when didMount trigger
*/
focus(isDidMount) {
if (this.inputRef.current) {
if (isDidMount) {
setTimeout(() => {
this.inputRef.current.focus();
}, 0);
} else {
// set it into else, Avoid scrolling when focus
this.inputRef.current.focus();
}
}
},
blur() {
if (this.inputRef.current) {
this.inputRef.current.blur();
}
},
handleInputChange(e) {
const { value, composing } = e.target;
const { searchValue = '' } = this;
if (e.isComposing || composing || searchValue === value) {
this.mirrorSearchValue = value;
return;
}
this.vcTreeSelect.onSearchInputChange(e);
},
},
render() {
const {
searchValue,
prefixCls,
disabled,
renderPlaceholder,
open,
ariaId,
isMultiple,
showSearch,
} = this.$props;
const {
vcTreeSelect: { onSearchInputKeyDown },
handleInputChange,
mirrorSearchValue,
inputWidth,
} = this;
return (
<>
<span
class={`${prefixCls}-selection-search`}
style={isMultiple ? { width: inputWidth + 'px' } : {}}
>
<BaseInput
type="text"
ref={this.inputRef}
onChange={handleInputChange}
onKeydown={onSearchInputKeyDown}
value={searchValue}
disabled={disabled}
readonly={!showSearch}
class={`${prefixCls}-selection-search-input`}
aria-label="filter select"
aria-autocomplete="list"
aria-controls={open ? ariaId : undefined}
aria-multiline="false"
/>
{isMultiple ? (
<span ref="measureRef" class={`${prefixCls}-selection-search-mirror`} aria-hidden>
{mirrorSearchValue}&nbsp;
</span>
) : null}
</span>
{renderPlaceholder && !mirrorSearchValue ? renderPlaceholder() : null}
</>
);
},
};
export default SearchInput;

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +0,0 @@
import VcTree from '../../vc-tree';
/**
* SelectNode wrapped the tree node.
* Let's use SelectNode instead of TreeNode
* since TreeNode is so confuse here.
*/
const TreeNode = VcTree.TreeNode;
function SelectNode(_, { attrs, slots }) {
return <TreeNode {...attrs} v-slots={slots} />;
}
SelectNode.isTreeNode = true;
SelectNode.inheritAttrs = false;
SelectNode.displayName = 'ATreeSelectNode';
export default SelectNode;

View File

@ -1,121 +0,0 @@
import PropTypes from '../../_util/vue-types';
import Trigger from '../../vc-trigger';
import { createRef } from './util';
import classNames from '../../_util/classNames';
import { getSlot } from '../../_util/props-util';
const BUILT_IN_PLACEMENTS = {
bottomLeft: {
points: ['tl', 'bl'],
offset: [0, 4],
overflow: {
adjustX: 0,
adjustY: 1,
},
ignoreShake: true,
},
topLeft: {
points: ['bl', 'tl'],
offset: [0, -4],
overflow: {
adjustX: 0,
adjustY: 1,
},
ignoreShake: true,
},
};
const SelectTrigger = {
name: 'SelectTrigger',
inheritAttrs: false,
props: {
// Pass by outside user props
disabled: PropTypes.looseBool,
showSearch: PropTypes.looseBool,
prefixCls: PropTypes.string,
dropdownPopupAlign: PropTypes.object,
dropdownClassName: PropTypes.string,
dropdownStyle: PropTypes.object,
transitionName: PropTypes.string,
animation: PropTypes.string,
getPopupContainer: PropTypes.func,
dropdownMatchSelectWidth: PropTypes.looseBool,
// Pass by Select
isMultiple: PropTypes.looseBool,
dropdownPrefixCls: PropTypes.string,
dropdownVisibleChange: PropTypes.func,
popupElement: PropTypes.any,
open: PropTypes.looseBool,
},
created() {
this.triggerRef = createRef();
},
methods: {
getDropdownTransitionName() {
const { transitionName, animation, dropdownPrefixCls } = this.$props;
if (!transitionName && animation) {
return `${dropdownPrefixCls}-${animation}`;
}
return transitionName;
},
forcePopupAlign() {
const $trigger = this.triggerRef.current;
if ($trigger) {
$trigger.forcePopupAlign();
}
},
},
render() {
const {
disabled,
isMultiple,
dropdownPopupAlign,
dropdownMatchSelectWidth,
dropdownClassName,
dropdownStyle,
dropdownVisibleChange,
getPopupContainer,
dropdownPrefixCls,
popupElement,
open,
} = this.$props;
// TODO: [Legacy] Use new action when trigger fixed: https://github.com/react-component/trigger/pull/86
// When false do nothing with the width
// ref: https://github.com/ant-design/ant-design/issues/10927
let stretch;
if (dropdownMatchSelectWidth !== false) {
stretch = dropdownMatchSelectWidth ? 'width' : 'minWidth';
}
return (
<Trigger
ref={this.triggerRef}
action={disabled ? [] : ['click']}
popupPlacement="bottomLeft"
builtinPlacements={BUILT_IN_PLACEMENTS}
popupAlign={dropdownPopupAlign}
prefixCls={dropdownPrefixCls}
popupTransitionName={this.getDropdownTransitionName()}
onPopupVisibleChange={dropdownVisibleChange}
popup={popupElement}
popupVisible={open}
getPopupContainer={getPopupContainer}
stretch={stretch}
popupClassName={classNames(dropdownClassName, {
[`${dropdownPrefixCls}--multiple`]: isMultiple,
[`${dropdownPrefixCls}--single`]: !isMultiple,
})}
popupStyle={dropdownStyle}
>
{getSlot(this)}
</Trigger>
);
},
};
export default SelectTrigger;

View File

@ -1,53 +0,0 @@
import classNames from '../../../../_util/classNames';
import PropTypes from '../../../../_util/vue-types';
import { toTitle, UNSELECTABLE_ATTRIBUTE, UNSELECTABLE_STYLE } from '../../util';
import { getComponent } from '../../../../_util/props-util';
import BaseMixin from '../../../../_util/BaseMixin';
const Selection = {
mixins: [BaseMixin],
inheritAttrs: false,
props: {
prefixCls: PropTypes.string,
maxTagTextLength: PropTypes.number,
// onRemove: PropTypes.func,
label: PropTypes.any,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
removeIcon: PropTypes.any,
},
methods: {
onRemove(event) {
const { value } = this.$props;
this.__emit('remove', event, value);
event.stopPropagation();
},
},
render() {
const { prefixCls, maxTagTextLength, label, value } = this.$props;
let content = label || value;
if (maxTagTextLength && typeof content === 'string' && content.length > maxTagTextLength) {
content = `${content.slice(0, maxTagTextLength)}...`;
}
const { class: className, style, onRemove } = this.$attrs;
return (
<span
style={{ ...UNSELECTABLE_STYLE, ...style }}
{...UNSELECTABLE_ATTRIBUTE}
role="menuitem"
class={classNames(`${prefixCls}-selection-item`, className)}
title={toTitle(label)}
>
<span class={`${prefixCls}-selection-item-content`}>{content}</span>
{onRemove && (
<span class={`${prefixCls}-selection-item-remove`} onClick={this.onRemove}>
{getComponent(this, 'removeIcon')}
</span>
)}
</span>
);
},
};
export default Selection;

View File

@ -1,161 +0,0 @@
import { inject } from 'vue';
import PropTypes from '../../../../_util/vue-types';
import { createRef } from '../../util';
import generateSelector, { selectorPropTypes } from '../../Base/BaseSelector';
import SearchInput from '../../SearchInput';
import Selection from './Selection';
import { getComponent, getSlot } from '../../../../_util/props-util';
import BaseMixin from '../../../../_util/BaseMixin';
const TREE_SELECT_EMPTY_VALUE_KEY = 'RC_TREE_SELECT_EMPTY_VALUE_KEY';
const Selector = generateSelector('multiple');
// export const multipleSelectorContextTypes = {
// onMultipleSelectorRemove: PropTypes.func.isRequired,
// }
const MultipleSelector = {
name: 'MultipleSelector',
mixins: [BaseMixin],
inheritAttrs: false,
props: {
...selectorPropTypes(),
...SearchInput.props,
selectorValueList: PropTypes.array,
disabled: PropTypes.looseBool,
labelInValue: PropTypes.looseBool,
maxTagCount: PropTypes.number,
maxTagPlaceholder: PropTypes.any,
// onChoiceAnimationLeave: PropTypes.func,
},
setup() {
return {
vcTreeSelect: inject('vcTreeSelect', {}),
};
},
created() {
this.inputRef = createRef();
},
methods: {
onPlaceholderClick() {
this.inputRef.current.focus();
},
focus() {
this.inputRef.current.focus();
},
blur() {
this.inputRef.current.blur();
},
_renderPlaceholder() {
const { prefixCls, placeholder, searchPlaceholder, searchValue, selectorValueList } =
this.$props;
const currentPlaceholder = placeholder || searchPlaceholder;
if (!currentPlaceholder) return null;
const hidden = searchValue || selectorValueList.length;
// [Legacy] Not remove the placeholder
return (
<span
style={{
display: hidden ? 'none' : 'block',
}}
onClick={this.onPlaceholderClick}
class={`${prefixCls}-selection-placeholder`}
>
{currentPlaceholder}
</span>
);
},
onChoiceAnimationLeave(...args) {
this.__emit('choiceAnimationLeave', ...args);
},
renderSelection() {
const { selectorValueList, labelInValue, maxTagCount } = this.$props;
const children = getSlot(this);
const {
vcTreeSelect: { onMultipleSelectorRemove },
} = this;
// Check if `maxTagCount` is set
let myValueList = selectorValueList;
if (maxTagCount >= 0) {
myValueList = selectorValueList.slice(0, maxTagCount);
}
// Selector node list
const selectedValueNodes = myValueList.map(({ label, value }) => (
<Selection
{...{
...this.$props,
label,
value,
onRemove: onMultipleSelectorRemove,
}}
key={value || TREE_SELECT_EMPTY_VALUE_KEY}
>
{children}
</Selection>
));
// Rest node count
if (maxTagCount >= 0 && maxTagCount < selectorValueList.length) {
let content = `+ ${selectorValueList.length - maxTagCount} ...`;
const maxTagPlaceholder = getComponent(this, 'maxTagPlaceholder', {}, false);
if (typeof maxTagPlaceholder === 'string') {
content = maxTagPlaceholder;
} else if (typeof maxTagPlaceholder === 'function') {
const restValueList = selectorValueList.slice(maxTagCount);
content = maxTagPlaceholder(
labelInValue ? restValueList : restValueList.map(({ value }) => value),
);
}
const restNodeSelect = (
<Selection
{...{
...this.$props,
label: content,
value: null,
}}
key="rc-tree-select-internal-max-tag-counter"
>
{children}
</Selection>
);
selectedValueNodes.push(restNodeSelect);
}
selectedValueNodes.push(
<SearchInput key="SearchInput" {...this.$props} {...this.$attrs} ref={this.inputRef}>
{children}
</SearchInput>,
);
return selectedValueNodes;
},
},
render() {
return (
<Selector
{...{
...this.$props,
...this.$attrs,
tabindex: -1,
showArrow: false,
renderSelection: this.renderSelection,
renderPlaceholder: this._renderPlaceholder,
}}
>
{getSlot(this)}
</Selector>
);
},
};
export default MultipleSelector;

View File

@ -1,97 +0,0 @@
import generateSelector, { selectorPropTypes } from '../Base/BaseSelector';
import { toTitle } from '../util';
import { getOptionProps } from '../../../_util/props-util';
import { createRef } from '../util';
import SearchInput from '../SearchInput';
const Selector = generateSelector('single');
const SingleSelector = {
name: 'SingleSelector',
inheritAttrs: false,
props: selectorPropTypes(),
created() {
this.selectorRef = createRef();
this.inputRef = createRef();
},
data() {
return {
mirrorSearchValue: this.searchValue,
};
},
watch: {
searchValue(val) {
this.mirrorSearchValue = val;
},
},
methods: {
onPlaceholderClick() {
this.inputRef.current.focus();
},
focus() {
this.selectorRef.current.focus();
},
blur() {
this.selectorRef.current.blur();
},
_renderPlaceholder() {
const { prefixCls, placeholder, searchPlaceholder, selectorValueList } = this.$props;
const currentPlaceholder = placeholder || searchPlaceholder;
if (!currentPlaceholder) return null;
const hidden = this.mirrorSearchValue || selectorValueList.length;
// [Legacy] Not remove the placeholder
return (
<span
style={{
display: hidden ? 'none' : 'block',
}}
onClick={this.onPlaceholderClick}
class={`${prefixCls}-selection-placeholder`}
>
{currentPlaceholder}
</span>
);
},
onMirrorSearchValueChange(value) {
this.mirrorSearchValue = value;
},
renderSelection() {
const { selectorValueList, prefixCls } = this.$props;
const selectedValueNodes = [];
if (selectorValueList.length && !this.mirrorSearchValue) {
const { label, value } = selectorValueList[0];
selectedValueNodes.push(
<span key={value} title={toTitle(label)} class={`${prefixCls}-selection-item`}>
{label || value}
</span>,
);
}
selectedValueNodes.push(
<SearchInput
{...this.$props}
{...this.$attrs}
ref={this.inputRef}
isMultiple={false}
onMirrorSearchValueChange={this.onMirrorSearchValueChange}
/>,
);
return selectedValueNodes;
},
},
render() {
const props = {
...getOptionProps(this),
...this.$attrs,
renderSelection: this.renderSelection,
renderPlaceholder: this._renderPlaceholder,
ref: this.selectorRef,
};
return <Selector {...props} />;
},
};
export default SingleSelector;

View File

@ -1,7 +0,0 @@
import Select from './Select';
import SelectNode from './SelectNode';
export { SHOW_ALL, SHOW_CHILD, SHOW_PARENT } from './strategies';
export const TreeNode = SelectNode;
export default Select;

View File

@ -1,42 +0,0 @@
import PropTypes from '../../_util/vue-types';
import { isLabelInValue } from './util';
const internalValProp = PropTypes.oneOfType([PropTypes.string, PropTypes.number]);
export function genArrProps(propType) {
return PropTypes.oneOfType([propType, PropTypes.arrayOf(propType)]);
}
/**
* Origin code check `multiple` is true when `treeCheckStrictly` & `labelInValue`.
* But in process logic is already cover to array.
* Check array is not necessary. Let's simplify this check logic.
*/
export function valueProp(...args) {
const [props, propName, Component] = args;
if (isLabelInValue(props)) {
const err = genArrProps(
PropTypes.shape({
label: PropTypes.any,
value: internalValProp,
}).loose,
)(...args);
if (err) {
return new Error(
`Invalid prop \`${propName}\` supplied to \`${Component}\`. ` +
`You should use { label: string, value: string | number } or [{ label: string, value: string | number }] instead.`,
);
}
return null;
}
const err = genArrProps(internalValProp)(...args);
if (err) {
return new Error(
`Invalid prop \`${propName}\` supplied to \`${Component}\`. ` +
`You should use string or [string] instead.`,
);
}
return null;
}

View File

@ -1,3 +0,0 @@
export const SHOW_ALL = 'SHOW_ALL';
export const SHOW_PARENT = 'SHOW_PARENT';
export const SHOW_CHILD = 'SHOW_CHILD';

View File

@ -1,431 +0,0 @@
import warning from 'warning';
import {
convertDataToTree as vcConvertDataToTree,
convertTreeToEntities as vcConvertTreeToEntities,
conductCheck as rcConductCheck,
} from '../../vc-tree/src/util';
import { hasClass } from '../../vc-util/Dom/class';
import { SHOW_CHILD, SHOW_PARENT } from './strategies';
import { getSlot, getPropsData, isEmptyElement } from '../../_util/props-util';
let warnDeprecatedLabel = false;
// =================== DOM =====================
export function findPopupContainer(node, prefixClass) {
let current = node;
while (current) {
if (hasClass(current, prefixClass)) {
return current;
}
current = current.parentNode;
}
return null;
}
// =================== MISC ====================
export function toTitle(title) {
if (typeof title === 'string') {
return title;
}
return null;
}
export function toArray(data) {
if (data === undefined || data === null) return [];
return Array.isArray(data) ? data : [data];
}
export function createRef() {
const func = function setRef(node) {
func.current = node;
};
return func;
}
// =============== Legacy ===============
export const UNSELECTABLE_STYLE = {
userSelect: 'none',
WebkitUserSelect: 'none',
};
export const UNSELECTABLE_ATTRIBUTE = {
unselectable: 'unselectable',
};
/**
* Convert position list to hierarchy structure.
* This is little hack since use '-' to split the position.
*/
export function flatToHierarchy(positionList) {
if (!positionList.length) {
return [];
}
const entrances = {};
// Prepare the position map
const posMap = {};
const parsedList = positionList.slice().map(entity => {
const clone = {
...entity,
fields: entity.pos.split('-'),
};
delete clone.children;
return clone;
});
parsedList.forEach(entity => {
posMap[entity.pos] = entity;
});
parsedList.sort((a, b) => {
return a.fields.length - b.fields.length;
});
// Create the hierarchy
parsedList.forEach(entity => {
const parentPos = entity.fields.slice(0, -1).join('-');
const parentEntity = posMap[parentPos];
if (!parentEntity) {
entrances[entity.pos] = entity;
} else {
parentEntity.children = parentEntity.children || [];
parentEntity.children.push(entity);
}
// Some time position list provide `key`, we don't need it
delete entity.key;
delete entity.fields;
});
return Object.keys(entrances).map(key => entrances[key]);
}
// =============== Accessibility ===============
let ariaId = 0;
export function resetAriaId() {
ariaId = 0;
}
export function generateAriaId(prefix) {
ariaId += 1;
return `${prefix}_${ariaId}`;
}
export function isLabelInValue(props) {
const { treeCheckable, treeCheckStrictly, labelInValue } = props;
if (treeCheckable && treeCheckStrictly) {
return true;
}
return labelInValue || false;
}
// =================== Tree ====================
export function parseSimpleTreeData(treeData, { id, pId, rootPId }) {
const keyNodes = {};
const rootNodeList = [];
// Fill in the map
const nodeList = treeData.map(node => {
const clone = { ...node };
const key = clone[id];
keyNodes[key] = clone;
clone.key = clone.key || key;
return clone;
});
// Connect tree
nodeList.forEach(node => {
const parentKey = node[pId];
const parent = keyNodes[parentKey];
// Fill parent
if (parent) {
parent.children = parent.children || [];
parent.children.push(node);
}
// Fill root tree node
if (parentKey === rootPId || (!parent && rootPId === null)) {
rootNodeList.push(node);
}
});
return rootNodeList;
}
/**
* Detect if position has relation.
* e.g. 1-2 related with 1-2-3
* e.g. 1-3-2 related with 1
* e.g. 1-2 not related with 1-21
*/
export function isPosRelated(pos1, pos2) {
const fields1 = pos1.split('-');
const fields2 = pos2.split('-');
const minLen = Math.min(fields1.length, fields2.length);
for (let i = 0; i < minLen; i += 1) {
if (fields1[i] !== fields2[i]) {
return false;
}
}
return true;
}
/**
* This function is only used on treeNode check (none treeCheckStrictly but has searchInput).
* We convert entity to { node, pos, children } format.
* This is legacy bug but we still need to do with it.
* @param entity
*/
export function cleanEntity({ node, pos, children }) {
const instance = {
node,
pos,
};
if (children) {
instance.children = children.map(cleanEntity);
}
return instance;
}
/**
* Get a filtered TreeNode list by provided treeNodes.
* [Legacy] Since `Tree` use `key` as map but `key` will changed by React,
* we have to convert `treeNodes > data > treeNodes` to keep the key.
* Such performance hungry!
*/
export function getFilterTree(treeNodes, searchValue, filterFunc, valueEntities, Component) {
if (!searchValue) {
return null;
}
function mapFilteredNodeToData(node) {
if (!node || isEmptyElement(node)) return null;
let match = false;
if (filterFunc(searchValue, node)) {
match = true;
}
let children = getSlot(node);
children = ((typeof children === 'function' ? children() : children) || [])
.map(mapFilteredNodeToData)
.filter(n => n);
if (children.length || match) {
return (
<Component {...node.props} key={valueEntities[getPropsData(node).value].key}>
{children}
</Component>
);
}
return null;
}
return treeNodes.map(mapFilteredNodeToData).filter(node => node);
}
// =================== Value ===================
/**
* Convert value to array format to make logic simplify.
*/
export function formatInternalValue(value, props) {
const valueList = toArray(value);
// Parse label in value
if (isLabelInValue(props)) {
return valueList.map(val => {
if (typeof val !== 'object' || !val) {
return {
value: '',
label: '',
};
}
return val;
});
}
return valueList.map(val => ({
value: val,
}));
}
export function getLabel(wrappedValue, entity, treeNodeLabelProp) {
if (wrappedValue.label) {
return wrappedValue.label;
}
if (entity) {
const props = getPropsData(entity.node);
if (Object.keys(props).length) {
return props[treeNodeLabelProp];
}
}
// Since value without entity will be in missValueList.
// This code will never reached, but we still need this in case.
return wrappedValue.value;
}
/**
* Convert internal state `valueList` to user needed value list.
* This will return an array list. You need check if is not multiple when return.
*
* `allCheckedNodes` is used for `treeCheckStrictly`
*/
export function formatSelectorValue(valueList, props, valueEntities) {
const { treeNodeLabelProp, treeCheckable, treeCheckStrictly, showCheckedStrategy } = props;
// Will hide some value if `showCheckedStrategy` is set
if (treeCheckable && !treeCheckStrictly) {
const values = {};
valueList.forEach(wrappedValue => {
values[wrappedValue.value] = wrappedValue;
});
const hierarchyList = flatToHierarchy(valueList.map(({ value }) => valueEntities[value]));
if (showCheckedStrategy === SHOW_PARENT) {
// Only get the parent checked value
return hierarchyList.map(({ node }) => {
const value = getPropsData(node).value;
return {
label: getLabel(values[value], valueEntities[value], treeNodeLabelProp),
value,
};
});
}
if (showCheckedStrategy === SHOW_CHILD) {
// Only get the children checked value
const targetValueList = [];
// Find the leaf children
const traverse = ({ node, children }) => {
const value = getPropsData(node).value;
if (!children || children.length === 0) {
targetValueList.push({
label: getLabel(values[value], valueEntities[value], treeNodeLabelProp),
value,
});
return;
}
children.forEach(entity => {
traverse(entity);
});
};
hierarchyList.forEach(entity => {
traverse(entity);
});
return targetValueList;
}
}
return valueList.map(wrappedValue => ({
label: getLabel(wrappedValue, valueEntities[wrappedValue.value], treeNodeLabelProp),
value: wrappedValue.value,
}));
}
/**
* Use `rc-tree` convertDataToTree to convert treeData to TreeNodes.
* This will change the label to title value
*/
function processProps(props) {
const { title, label, key, value } = props;
const cloneProps = { ...props };
// Warning user not to use deprecated label prop.
if (label && !title) {
if (!warnDeprecatedLabel) {
warning(false, "'label' in treeData is deprecated. Please use 'title' instead.");
warnDeprecatedLabel = true;
}
cloneProps.title = label;
}
if (!key && (key === undefined || key === null)) {
cloneProps.key = value;
}
return cloneProps;
}
export function convertDataToTree(treeData) {
return vcConvertDataToTree(treeData, { processProps });
}
/**
* Use `rc-tree` convertTreeToEntities for entities calculation.
* We have additional entities of `valueEntities`
*/
function initWrapper(wrapper) {
return {
...wrapper,
valueEntities: {},
};
}
function processEntity(entity, wrapper) {
const value = getPropsData(entity.node).value;
entity.value = value;
// This should be empty, or will get error message.
const currentEntity = wrapper.valueEntities[value];
if (currentEntity) {
warning(
false,
`Conflict! value of node '${entity.key}' (${value}) has already used by node '${currentEntity.key}'.`,
);
}
wrapper.valueEntities[value] = entity;
}
export function convertTreeToEntities(treeNodes) {
return vcConvertTreeToEntities(treeNodes, {
initWrapper,
processEntity,
});
}
/**
* https://github.com/ant-design/ant-design/issues/13328
* We need calculate the half check key when searchValue is set.
*/
// TODO: This logic may better move to rc-tree
export function getHalfCheckedKeys(valueList, valueEntities) {
const values = {};
// Fill checked keys
valueList.forEach(({ value }) => {
values[value] = false;
});
// Fill half checked keys
valueList.forEach(({ value }) => {
let current = valueEntities[value];
while (current && current.parent) {
const parentValue = current.parent.value;
if (parentValue in values) break;
values[parentValue] = true;
current = current.parent;
}
});
// Get half keys
return Object.keys(values)
.filter(value => values[value])
.map(value => valueEntities[value].key);
}
export const conductCheck = rcConductCheck;

View File

@ -0,0 +1,181 @@
import { filterEmpty } from '../../_util/props-util';
import type { VNodeChild } from 'vue';
import { camelize } from 'vue';
import { warning } from '../../vc-util/warning';
import type {
DataNode,
LegacyDataNode,
ChangeEventExtra,
InternalDataEntity,
RawValueType,
LegacyCheckedNode,
} from '../interface';
import TreeNode from '../TreeNode';
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 filterEmpty(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,
isLeaf,
...newProps,
};
const parsedChildren = dig(children);
if (parsedChildren.length) {
dataNode.children = parsedChildren;
}
return dataNode;
});
}
return dig(rootNodes as any[]);
}
export function fillLegacyProps(dataNode: DataNode): LegacyDataNode {
// Skip if not dataNode exist
if (!dataNode) {
return dataNode as LegacyDataNode;
}
const cloneNode = { ...dataNode };
if (!('props' in cloneNode)) {
Object.defineProperty(cloneNode, 'props', {
get() {
warning(
false,
'New `rc-tree-select` not support return node instance as argument anymore. Please consider to remove `props` access.',
);
return cloneNode;
},
});
}
return cloneNode as LegacyDataNode;
}
export function fillAdditionalInfo(
extra: ChangeEventExtra,
triggerValue: RawValueType,
checkedValues: RawValueType[],
treeData: InternalDataEntity[],
showPosition: boolean,
) {
let triggerNode = null;
let nodeList: LegacyCheckedNode[] = null;
function generateMap() {
function dig(list: InternalDataEntity[], level = '0', parentIncluded = false) {
return list
.map((dataNode, index) => {
const pos = `${level}-${index}`;
const included = checkedValues.includes(dataNode.value);
const children = dig(dataNode.children || [], pos, included);
const node = <TreeNode {...dataNode}>{children.map(child => child.node)}</TreeNode>;
// Link with trigger node
if (triggerValue === dataNode.value) {
triggerNode = node;
}
if (included) {
const checkedNode: LegacyCheckedNode = {
pos,
node,
children,
};
if (!parentIncluded) {
nodeList.push(checkedNode);
}
return checkedNode;
}
return null;
})
.filter(node => node);
}
if (!nodeList) {
nodeList = [];
dig(treeData);
// Sort to keep the checked node length
nodeList.sort(
(
{
node: {
props: { value: val1 },
},
},
{
node: {
props: { value: val2 },
},
},
) => {
const index1 = checkedValues.indexOf(val1);
const index2 = checkedValues.indexOf(val2);
return index1 - index2;
},
);
}
}
Object.defineProperty(extra, 'triggerNode', {
get() {
warning(false, '`triggerNode` is deprecated. Please consider decoupling data with node.');
generateMap();
return triggerNode;
},
});
Object.defineProperty(extra, 'allCheckedNodes', {
get() {
warning(false, '`allCheckedNodes` is deprecated. Please consider decoupling data with node.');
generateMap();
if (showPosition) {
return nodeList;
}
return nodeList.map(({ node }) => node);
},
});
}

View File

@ -0,0 +1,46 @@
import type { DataEntity } from '../../vc-tree/interface';
import type { RawValueType, Key, DataNode } from '../interface';
import { isCheckDisabled } from './valueUtil';
export const SHOW_ALL = 'SHOW_ALL';
export const SHOW_PARENT = 'SHOW_PARENT';
export const SHOW_CHILD = 'SHOW_CHILD';
export type CheckedStrategy = typeof SHOW_ALL | typeof SHOW_PARENT | typeof SHOW_CHILD;
export function formatStrategyKeys(
keys: Key[],
strategy: CheckedStrategy,
keyEntities: Record<Key, DataEntity>,
): RawValueType[] {
const keySet = new Set(keys);
if (strategy === SHOW_CHILD) {
return keys.filter(key => {
const entity = keyEntities[key];
if (
entity &&
entity.children &&
entity.children.every(
({ node }) => isCheckDisabled(node) || keySet.has((node as DataNode).key),
)
) {
return false;
}
return true;
});
}
if (strategy === SHOW_PARENT) {
return keys.filter(key => {
const entity = keyEntities[key];
const parent = entity ? entity.parent : null;
if (parent && !isCheckDisabled(parent.node) && keySet.has((parent.node as DataNode).key)) {
return false;
}
return true;
});
}
return keys;
}

View File

@ -0,0 +1,244 @@
import type {
FlattenDataNode,
Key,
RawValueType,
DataNode,
DefaultValueType,
LabelValueType,
LegacyDataNode,
FieldNames,
InternalDataEntity,
} from '../interface';
import { fillLegacyProps } from './legacyUtil';
import type { SkipType } from '../hooks/useKeyValueMapping';
import type { FlattenNode } from '../../vc-tree/interface';
import { flattenTreeData } from '../../vc-tree/utils/treeUtil';
import type { FilterFunc } from '../../vc-select/interface/generator';
type CompatibleDataNode = Omit<FlattenDataNode, 'level'>;
export function toArray<T>(value: T | T[]): T[] {
if (Array.isArray(value)) {
return value;
}
return value !== undefined ? [value] : [];
}
/**
* Fill `fieldNames` with default field names.
*
* @param fieldNames passed props
* @param skipTitle Skip if no need fill `title`. This is useful since we have 2 name as same title level
* @returns
*/
export function fillFieldNames(fieldNames?: FieldNames, skipTitle = false) {
const { label, value, children } = fieldNames || {};
const filledNames: FieldNames = {
value: value || 'value',
children: children || 'children',
};
if (!skipTitle || label) {
filledNames.label = label || 'label';
}
return filledNames;
}
export function findValueOption(values: RawValueType[], options: CompatibleDataNode[]): DataNode[] {
const optionMap: Map<RawValueType, DataNode> = new Map();
options.forEach(flattenItem => {
const { data, value } = flattenItem;
optionMap.set(value, data.node);
});
return values.map(val => fillLegacyProps(optionMap.get(val)));
}
export function isValueDisabled(value: RawValueType, options: CompatibleDataNode[]): boolean {
const option = findValueOption([value], options)[0];
if (option) {
return option.disabled;
}
return false;
}
export function isCheckDisabled(node: DataNode) {
return node.disabled || node.disableCheckbox || node.checkable === false;
}
interface TreeDataNode extends InternalDataEntity {
key: Key;
children?: TreeDataNode[];
}
function getLevel({ parent }: FlattenNode): number {
let level = 0;
let current = parent;
while (current) {
current = current.parent;
level += 1;
}
return level;
}
/**
* Before reuse `rc-tree` logic, we need to add key since TreeSelect use `value` instead of `key`.
*/
export function flattenOptions(options: any): FlattenDataNode[] {
const typedOptions = options as InternalDataEntity[];
// Add missing key
function fillKey(list: InternalDataEntity[]): TreeDataNode[] {
return (list || []).map(node => {
const { value, key, children } = node;
const clone: TreeDataNode = {
...node,
key: 'key' in node ? key : value,
};
if (children) {
clone.children = fillKey(children);
}
return clone;
});
}
const flattenList = flattenTreeData(fillKey(typedOptions), true, null);
const cacheMap = new Map<Key, FlattenDataNode>();
const flattenDateNodeList: (FlattenDataNode & { parentKey?: Key })[] = flattenList.map(option => {
const { data, key, value } = option as any as Omit<FlattenNode, 'data'> & {
value: RawValueType;
data: InternalDataEntity;
};
const flattenNode = {
key,
value,
data,
level: getLevel(option),
parentKey: option.parent?.data.key,
};
cacheMap.set(key, flattenNode);
return flattenNode;
});
// Fill parent
flattenDateNodeList.forEach(flattenNode => {
// eslint-disable-next-line no-param-reassign
flattenNode.parent = cacheMap.get(flattenNode.parentKey);
});
return flattenDateNodeList;
}
function getDefaultFilterOption(optionFilterProp: string) {
return (searchValue: string, dataNode: LegacyDataNode) => {
const value = dataNode[optionFilterProp];
return String(value).toLowerCase().includes(String(searchValue).toLowerCase());
};
}
/** Filter options and return a new options by the search text */
export function filterOptions(
searchValue: string,
options: DataNode[],
{
optionFilterProp,
filterOption,
}: {
optionFilterProp: string;
filterOption: boolean | FilterFunc<LegacyDataNode>;
},
): DataNode[] {
if (filterOption === false) {
return options;
}
let filterOptionFunc: FilterFunc<LegacyDataNode>;
if (typeof filterOption === 'function') {
filterOptionFunc = filterOption;
} else {
filterOptionFunc = getDefaultFilterOption(optionFilterProp);
}
function dig(list: DataNode[], keepAll = false) {
return list
.map(dataNode => {
const { children } = dataNode;
const match = keepAll || filterOptionFunc(searchValue, fillLegacyProps(dataNode));
const childList = dig(children || [], match);
if (match || childList.length) {
return {
...dataNode,
children: childList,
};
}
return null;
})
.filter(node => node);
}
return dig(options);
}
export function getRawValueLabeled(
values: RawValueType[],
prevValue: DefaultValueType,
getEntityByValue: (
value: RawValueType,
skipType?: SkipType,
ignoreDisabledCheck?: boolean,
) => FlattenDataNode,
getLabelProp: (entity: FlattenDataNode) => any,
): LabelValueType[] {
const valueMap = new Map<RawValueType, LabelValueType>();
toArray(prevValue).forEach(item => {
if (item && typeof item === 'object' && 'value' in item) {
valueMap.set(item.value, item);
}
});
return values.map(val => {
const item: LabelValueType = { value: val };
const entity = getEntityByValue(val, 'select', true);
const label = entity ? getLabelProp(entity) : val;
if (valueMap.has(val)) {
const labeledValue = valueMap.get(val);
item.label = 'label' in labeledValue ? labeledValue.label : label;
if ('halfChecked' in labeledValue) {
item.halfChecked = labeledValue.halfChecked;
}
} else {
item.label = label;
}
return item;
});
}
export function addValue(rawValues: RawValueType[], value: RawValueType) {
const values = new Set(rawValues);
values.add(value);
return Array.from(values);
}
export function removeValue(rawValues: RawValueType[], value: RawValueType) {
const values = new Set(rawValues);
values.delete(value);
return Array.from(values);
}

View File

@ -0,0 +1,34 @@
import { warning } from '../../vc-util/warning';
import { toArray } from './valueUtil';
function warningProps(props: any) {
const { searchPlaceholder, treeCheckStrictly, treeCheckable, labelInValue, value, multiple } =
props;
warning(
!searchPlaceholder,
'`searchPlaceholder` has been removed, please use `placeholder` instead',
);
if (treeCheckStrictly && labelInValue === false) {
warning(false, '`treeCheckStrictly` will force set `labelInValue` to `true`.');
}
if (labelInValue || treeCheckStrictly) {
warning(
toArray(value).every(val => val && typeof val === 'object' && 'value' in val),
'Invalid prop `value` supplied to `TreeSelect`. You should use { label: string, value: string | number } or [{ label: string, value: string | number }] instead.',
);
}
if (treeCheckStrictly || multiple || treeCheckable) {
warning(
!value || Array.isArray(value),
'`value` should be an array when `TreeSelect` is checkable or multiple.',
);
} else {
warning(!Array.isArray(value), '`value` should not be array when `TreeSelect` is single mode.');
}
}
export default warningProps;

View File

@ -0,0 +1,34 @@
import type { CSSProperties } from 'vue';
export default function DropIndicator({
dropPosition,
dropLevelOffset,
indent,
}: {
dropPosition: -1 | 0 | 1;
dropLevelOffset: number;
indent: number;
}) {
const style: CSSProperties = {
pointerEvents: 'none',
position: 'absolute',
right: 0,
backgroundColor: 'red',
height: `${2}px`,
};
switch (dropPosition) {
case -1:
style.top = 0;
style.left = `${-dropLevelOffset * indent}px`;
break;
case 1:
style.bottom = 0;
style.left = `${-dropLevelOffset * indent}px`;
break;
case 0:
style.bottom = 0;
style.left = `${indent}`;
break;
}
return <div style={style} />;
}

View File

@ -0,0 +1,31 @@
interface IndentProps {
prefixCls: string;
level: number;
isStart: boolean[];
isEnd: boolean[];
}
const Indent = ({ prefixCls, level, isStart, isEnd }: IndentProps) => {
const baseClassName = `${prefixCls}-indent-unit`;
const list = [];
for (let i = 0; i < level; i += 1) {
list.push(
<span
key={i}
class={{
[baseClassName]: true,
[`${baseClassName}-start`]: isStart[i],
[`${baseClassName}-end`]: isEnd[i],
}}
/>,
);
}
return (
<span aria-hidden="true" class={`${prefixCls}-indent`}>
{list}
</span>
);
};
export default Indent;

View File

@ -0,0 +1,127 @@
import TreeNode from './TreeNode';
import type { FlattenNode } from './interface';
import type { TreeNodeRequiredProps } from './utils/treeUtil';
import { getTreeNodeProps } from './utils/treeUtil';
import { useInjectTreeContext } from './contextTypes';
import type { PropType } from 'vue';
import { computed, nextTick } from 'vue';
import { defineComponent, onBeforeUnmount, onMounted, ref, Transition, watch } from 'vue';
import { treeNodeProps } from './props';
import { collapseMotion } from '../_util/transition';
export default defineComponent({
name: 'MotionTreeNode',
inheritAttrs: false,
props: {
...treeNodeProps,
active: Boolean,
motion: Object,
motionNodes: { type: Array as PropType<FlattenNode[]> },
onMotionStart: Function,
onMotionEnd: Function,
motionType: String,
treeNodeRequiredProps: { type: Object as PropType<TreeNodeRequiredProps> },
},
slots: ['title', 'icon', 'switcherIcon', 'checkable'],
setup(props, { attrs, slots }) {
const visible = ref(true);
const context = useInjectTreeContext();
const motionedRef = ref(false);
const transitionClass = ref('');
const transitionStyle = ref({});
const transitionProps = computed(() => {
if (props.motion) {
return props.motion;
} else {
return collapseMotion(transitionStyle, transitionClass);
}
});
const onMotionEnd = (type?: 'appear' | 'leave') => {
if (type === 'appear') {
transitionProps.value?.onAfterAppear?.();
} else if (type === 'leave') {
transitionProps.value?.onAfterLeave?.();
}
if (!motionedRef.value) {
props.onMotionEnd();
}
motionedRef.value = true;
};
watch(
() => props.motionNodes,
() => {
if (props.motionNodes && props.motionType === 'hide' && visible.value) {
nextTick(() => {
visible.value = false;
});
}
},
{ immediate: true, flush: 'post' },
);
onMounted(() => {
props.motionNodes && props.onMotionStart();
});
onBeforeUnmount(() => {
props.motionNodes && onMotionEnd();
});
return () => {
const { motion, motionNodes, motionType, active, treeNodeRequiredProps, ...otherProps } =
props;
if (motionNodes) {
return (
<Transition
{...transitionProps.value}
appear={motionType === 'show'}
onAfterAppear={() => onMotionEnd('appear')}
onAfterLeave={() => onMotionEnd('leave')}
>
<div
v-show={visible.value}
class={[`${context.value.prefixCls}-treenode-motion`, transitionClass.value]}
style={transitionStyle.value}
>
{motionNodes.map((treeNode: FlattenNode) => {
const {
data: { ...restProps },
title,
key,
isStart,
isEnd,
} = treeNode;
delete restProps.children;
const treeNodeProps = getTreeNodeProps(key, treeNodeRequiredProps);
return (
<TreeNode
v-slots={slots}
{...restProps}
{...treeNodeProps}
title={title}
active={active}
data={treeNode.data}
key={key}
isStart={isStart}
isEnd={isEnd}
/>
);
})}
</div>
</Transition>
);
}
return (
<TreeNode
v-slots={slots}
domRef={ref}
class={attrs.class}
style={attrs.style}
{...otherProps}
active={active}
/>
);
};
},
});

View File

@ -0,0 +1,320 @@
/**
* Handle virtual list of the TreeNodes.
*/
import { computed, defineComponent, ref, watch } from 'vue';
import VirtualList from '../vc-virtual-list';
import type { FlattenNode, DataEntity, DataNode, ScrollTo } from './interface';
import MotionTreeNode from './MotionTreeNode';
import { nodeListProps } from './props';
import { findExpandedKeys, getExpandRange } from './utils/diffUtil';
import { getTreeNodeProps, getKey } from './utils/treeUtil';
const HIDDEN_STYLE = {
width: 0,
height: 0,
display: 'flex',
overflow: 'hidden',
opacity: 0,
border: 0,
padding: 0,
margin: 0,
};
const noop = () => {};
export const MOTION_KEY = `RC_TREE_MOTION_${Math.random()}`;
const MotionNode: DataNode = {
key: MOTION_KEY,
};
export const MotionEntity: DataEntity = {
key: MOTION_KEY,
level: 0,
index: 0,
pos: '0',
node: MotionNode,
};
const MotionFlattenData: FlattenNode = {
parent: null,
children: [],
pos: MotionEntity.pos,
data: MotionNode,
title: null,
key: MOTION_KEY,
/** Hold empty list here since we do not use it */
isStart: [],
isEnd: [],
};
export interface NodeListRef {
scrollTo: ScrollTo;
getIndentWidth: () => number;
}
/**
* We only need get visible content items to play the animation.
*/
export function getMinimumRangeTransitionRange(
list: FlattenNode[],
virtual: boolean,
height: number,
itemHeight: number,
) {
if (virtual === false || !height) {
return list;
}
return list.slice(0, Math.ceil(height / itemHeight) + 1);
}
function itemKey(item: FlattenNode) {
const {
data: { key },
pos,
} = item;
return getKey(key, pos);
}
function getAccessibilityPath(item: FlattenNode): string {
let path = String(item.data.key);
let current = item;
while (current.parent) {
current = current.parent;
path = `${current.data.key} > ${path}`;
}
return path;
}
export default defineComponent({
name: 'NodeList',
inheritAttrs: false,
props: nodeListProps,
setup(props, { expose, attrs }) {
// =============================== Ref ================================
const listRef = ref();
const indentMeasurerRef = ref();
expose({
scrollTo: scroll => {
listRef.value.scrollTo(scroll);
},
getIndentWidth: () => indentMeasurerRef.value.offsetWidth,
});
// ============================== Motion ==============================
const transitionData = ref<FlattenNode[]>(props.data);
const transitionRange = ref([]);
const motionType = ref<'show' | 'hide' | null>(null);
function onMotionEnd() {
transitionData.value = props.data;
transitionRange.value = [];
motionType.value = null;
props.onListChangeEnd();
}
watch(
[() => [...props.expandedKeys], () => props.data],
([expandedKeys, data], [prevExpandedKeys, prevData]) => {
const diffExpanded = findExpandedKeys(prevExpandedKeys, expandedKeys);
if (diffExpanded.key !== null) {
const { virtual, height, itemHeight } = props;
if (diffExpanded.add) {
const keyIndex = prevData.findIndex(({ data: { key } }) => key === diffExpanded.key);
const rangeNodes = getMinimumRangeTransitionRange(
getExpandRange(prevData, data, diffExpanded.key),
virtual,
height,
itemHeight,
);
const newTransitionData: FlattenNode[] = prevData.slice();
newTransitionData.splice(keyIndex + 1, 0, MotionFlattenData);
transitionData.value = newTransitionData;
transitionRange.value = rangeNodes;
motionType.value = 'show';
} else {
const keyIndex = data.findIndex(({ data: { key } }) => key === diffExpanded.key);
const rangeNodes = getMinimumRangeTransitionRange(
getExpandRange(data, prevData, diffExpanded.key),
virtual,
height,
itemHeight,
);
const newTransitionData: FlattenNode[] = data.slice();
newTransitionData.splice(keyIndex + 1, 0, MotionFlattenData);
transitionData.value = newTransitionData;
transitionRange.value = rangeNodes;
motionType.value = 'hide';
}
} else if (prevData !== data) {
transitionData.value = data;
}
},
);
// We should clean up motion if is changed by dragging
watch(
() => props.dragging,
dragging => {
if (!dragging) {
onMotionEnd();
}
},
);
const mergedData = computed(() =>
props.motion === undefined ? transitionData.value : props.data,
);
return () => {
const {
prefixCls,
data,
selectable,
checkable,
expandedKeys,
selectedKeys,
checkedKeys,
loadedKeys,
loadingKeys,
halfCheckedKeys,
keyEntities,
disabled,
dragging,
dragOverNodeKey,
dropPosition,
motion,
height,
itemHeight,
virtual,
focusable,
activeItem,
focused,
tabindex,
onKeydown,
onFocus,
onBlur,
onActiveChange,
onListChangeStart,
onListChangeEnd,
...domProps
} = { ...props, ...attrs };
const treeNodeRequiredProps = {
expandedKeys,
selectedKeys,
loadedKeys,
loadingKeys,
checkedKeys,
halfCheckedKeys,
dragOverNodeKey,
dropPosition,
keyEntities,
};
return (
<>
{focused && activeItem && (
<span style={HIDDEN_STYLE} aria-live="assertive">
{getAccessibilityPath(activeItem)}
</span>
)}
<div>
<input
style={HIDDEN_STYLE}
disabled={focusable === false || disabled}
tabindex={focusable !== false ? tabindex : null}
onKeydown={onKeydown}
onFocus={onFocus}
onBlur={onBlur}
value=""
onChange={noop}
aria-label="for screen reader"
/>
</div>
<div
class={`${prefixCls}-treenode`}
aria-hidden
style={{
position: 'absolute',
pointerEvents: 'none',
visibility: 'hidden',
height: 0,
overflow: 'hidden',
}}
>
<div class={`${prefixCls}-indent`}>
<div ref={indentMeasurerRef} class={`${prefixCls}-indent-unit`} />
</div>
</div>
<VirtualList
{...domProps}
data={mergedData.value}
itemKey={itemKey as any}
height={height}
fullHeight={false}
virtual={virtual}
itemHeight={itemHeight}
prefixCls={`${prefixCls}-list`}
ref={listRef}
children={(treeNode: FlattenNode) => {
const {
pos,
data: { ...restProps },
title,
key,
isStart,
isEnd,
} = treeNode;
const mergedKey = getKey(key, pos);
delete restProps.key;
delete restProps.children;
const treeNodeProps = getTreeNodeProps(mergedKey, treeNodeRequiredProps);
return (
<MotionTreeNode
{...restProps}
{...treeNodeProps}
title={title}
active={!!activeItem && key === activeItem.data.key}
pos={pos}
data={treeNode.data}
isStart={isStart}
isEnd={isEnd}
motion={motion}
motionNodes={key === MOTION_KEY ? transitionRange.value : null}
motionType={motionType.value}
onMotionStart={onListChangeStart}
onMotionEnd={onMotionEnd}
treeNodeRequiredProps={treeNodeRequiredProps}
onMousemove={() => {
onActiveChange(null);
}}
/>
);
}}
></VirtualList>
</>
);
};
},
});

1079
components/vc-tree/Tree.tsx Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,521 @@
import { useInjectTreeContext } from './contextTypes';
import { getDataAndAria } from './util';
import Indent from './Indent';
import { convertNodePropsToEventData } from './utils/treeUtil';
import {
computed,
defineComponent,
getCurrentInstance,
onMounted,
onUpdated,
reactive,
ref,
} from 'vue';
import { treeNodeProps } from './props';
import classNames from '../_util/classNames';
import { warning } from '../vc-util/warning';
import type { DragNodeEvent, Key } from './interface';
import pick from 'lodash-es/pick';
const ICON_OPEN = 'open';
const ICON_CLOSE = 'close';
const defaultTitle = '---';
export default defineComponent({
name: 'TreeNode',
inheritAttrs: false,
props: treeNodeProps,
isTreeNode: 1,
slots: ['title', 'icon', 'switcherIcon'],
setup(props, { attrs, slots, expose }) {
warning(
!('slots' in props.data),
`treeData slots is deprecated, please use ${Object.keys(props.data.slots || {}).map(
key => '`v-slot:' + key + '` ',
)}instead`,
);
const dragNodeHighlight = ref(false);
const context = useInjectTreeContext();
const selectHandle = ref();
const hasChildren = computed(() => {
const { eventKey } = props;
const { keyEntities } = context.value;
const { children } = keyEntities[eventKey] || {};
return !!(children || []).length;
});
const isLeaf = computed(() => {
const { isLeaf, loaded } = props;
const { loadData } = context.value;
const has = hasChildren.value;
if (isLeaf === false) {
return false;
}
return isLeaf || (!loadData && !has) || (loadData && loaded && !has);
});
const nodeState = computed(() => {
const { expanded } = props;
if (isLeaf.value) {
return null;
}
return expanded ? ICON_OPEN : ICON_CLOSE;
});
const isDisabled = computed(() => {
const { disabled } = props;
const { disabled: treeDisabled } = context.value;
return !!(treeDisabled || disabled);
});
const isCheckable = computed(() => {
const { checkable } = props;
const { checkable: treeCheckable } = context.value;
// Return false if tree or treeNode is not checkable
if (!treeCheckable || checkable === false) return false;
return treeCheckable;
});
const isSelectable = computed(() => {
const { selectable } = props;
const { selectable: treeSelectable } = context.value;
// Ignore when selectable is undefined or null
if (typeof selectable === 'boolean') {
return selectable;
}
return treeSelectable;
});
const renderArgsData = computed(() => {
return {
...pick(props, [
'active',
'checkable',
'checked',
'disableCheckbox',
'disabled',
'expanded',
'isLeaf',
'loading',
'selectable',
'selected',
'halfChecked',
]),
...props.data,
dataRef: props.data,
isLeaf: isLeaf.value,
};
});
const eventData = computed(() => {
return convertNodePropsToEventData(props);
});
const dragNodeEvent: DragNodeEvent = reactive({
eventData,
eventKey: computed(() => props.eventKey),
selectHandle,
pos: computed(() => props.pos),
key: getCurrentInstance().vnode.key as Key,
});
expose(dragNodeEvent);
const onSelectorDoubleClick = (e: MouseEvent) => {
const { onNodeDoubleClick } = context.value;
onNodeDoubleClick(e, eventData.value);
};
const onSelect = (e: MouseEvent) => {
if (isDisabled.value) return;
const { onNodeSelect } = context.value;
e.preventDefault();
onNodeSelect(e, eventData.value);
};
const onCheck = (e: MouseEvent) => {
if (isDisabled.value) return;
const { disableCheckbox, checked } = props;
const { onNodeCheck } = context.value;
if (!isCheckable.value || disableCheckbox) return;
e.preventDefault();
const targetChecked = !checked;
onNodeCheck(e, eventData.value, targetChecked);
};
const onSelectorClick = (e: MouseEvent) => {
// Click trigger before select/check operation
const { onNodeClick } = context.value;
onNodeClick(e, eventData.value);
if (isSelectable.value) {
onSelect(e);
} else {
onCheck(e);
}
};
const onMouseEnter = (e: MouseEvent) => {
const { onNodeMouseEnter } = context.value;
onNodeMouseEnter(e, eventData.value);
};
const onMouseLeave = (e: MouseEvent) => {
const { onNodeMouseLeave } = context.value;
onNodeMouseLeave(e, eventData.value);
};
const onContextmenu = (e: MouseEvent) => {
const { onNodeContextMenu } = context.value;
onNodeContextMenu(e, eventData.value);
};
const onDragStart = (e: DragEvent) => {
const { onNodeDragStart } = context.value;
e.stopPropagation();
dragNodeHighlight.value = true;
onNodeDragStart(e, dragNodeEvent);
try {
// ie throw error
// firefox-need-it
e.dataTransfer.setData('text/plain', '');
} catch (error) {
// empty
}
};
const onDragEnter = (e: DragEvent) => {
const { onNodeDragEnter } = context.value;
e.preventDefault();
e.stopPropagation();
onNodeDragEnter(e, dragNodeEvent);
};
const onDragOver = (e: DragEvent) => {
const { onNodeDragOver } = context.value;
e.preventDefault();
e.stopPropagation();
onNodeDragOver(e, dragNodeEvent);
};
const onDragLeave = (e: DragEvent) => {
const { onNodeDragLeave } = context.value;
e.stopPropagation();
onNodeDragLeave(e, dragNodeEvent);
};
const onDragEnd = (e: DragEvent) => {
const { onNodeDragEnd } = context.value;
e.stopPropagation();
dragNodeHighlight.value = false;
onNodeDragEnd(e, dragNodeEvent);
};
const onDrop = (e: DragEvent) => {
const { onNodeDrop } = context.value;
e.preventDefault();
e.stopPropagation();
dragNodeHighlight.value = false;
onNodeDrop(e, dragNodeEvent);
};
// Disabled item still can be switch
const onExpand = e => {
const { onNodeExpand } = context.value;
if (props.loading) return;
onNodeExpand(e, eventData.value);
};
const renderSwitcherIconDom = () => {
const {
switcherIcon: switcherIconFromProps = slots.switcherIcon ||
context.value.slots?.[props.data?.slots?.switcherIcon],
} = props;
const { switcherIcon: switcherIconFromCtx } = context.value;
const switcherIcon = switcherIconFromProps || switcherIconFromCtx;
// if switcherIconDom is null, no render switcher span
if (typeof switcherIcon === 'function') {
return switcherIcon(renderArgsData.value);
}
return switcherIcon;
};
// Load data to avoid default expanded tree without data
const syncLoadData = () => {
const { expanded, loading, loaded } = props;
const { loadData, onNodeLoad } = context.value;
if (loading) {
return;
}
// read from state to avoid loadData at same time
if (loadData && expanded && !isLeaf.value) {
// We needn't reload data when has children in sync logic
// It's only needed in node expanded
if (!hasChildren.value && !loaded) {
onNodeLoad(eventData.value);
}
}
};
onMounted(() => {
syncLoadData();
});
onUpdated(() => {
//syncLoadData();
});
// Switcher
const renderSwitcher = () => {
const { expanded } = props;
const { prefixCls } = context.value;
// if switcherIconDom is null, no render switcher span
const switcherIconDom = renderSwitcherIconDom();
if (isLeaf.value) {
return switcherIconDom !== false ? (
<span class={classNames(`${prefixCls}-switcher`, `${prefixCls}-switcher-noop`)}>
{switcherIconDom}
</span>
) : null;
}
const switcherCls = classNames(
`${prefixCls}-switcher`,
`${prefixCls}-switcher_${expanded ? ICON_OPEN : ICON_CLOSE}`,
);
return switcherIconDom !== false ? (
<span onClick={onExpand} class={switcherCls}>
{switcherIconDom}
</span>
) : null;
};
// Checkbox
const renderCheckbox = () => {
const { checked, halfChecked, disableCheckbox } = props;
const { prefixCls } = context.value;
const disabled = isDisabled.value;
const checkable = isCheckable.value;
if (!checkable) return null;
return (
<span
class={classNames(
`${prefixCls}-checkbox`,
checked && `${prefixCls}-checkbox-checked`,
!checked && halfChecked && `${prefixCls}-checkbox-indeterminate`,
(disabled || disableCheckbox) && `${prefixCls}-checkbox-disabled`,
)}
onClick={onCheck}
>
{context.value.customCheckable?.()}
</span>
);
};
const renderIcon = () => {
const { loading } = props;
const { prefixCls } = context.value;
return (
<span
class={classNames(
`${prefixCls}-iconEle`,
`${prefixCls}-icon__${nodeState.value || 'docu'}`,
loading && `${prefixCls}-icon_loading`,
)}
/>
);
};
const renderDropIndicator = () => {
const { disabled, eventKey } = props;
const {
draggable,
dropLevelOffset,
dropPosition,
prefixCls,
indent,
dropIndicatorRender,
dragOverNodeKey,
direction,
} = context.value;
const mergedDraggable = draggable !== false;
// allowDrop is calculated in Tree.tsx, there is no need for calc it here
const showIndicator = !disabled && mergedDraggable && dragOverNodeKey === eventKey;
return showIndicator
? dropIndicatorRender({ dropPosition, dropLevelOffset, indent, prefixCls, direction })
: null;
};
// Icon + Title
const renderSelector = () => {
const {
// title = slots.title ||
// context.value.slots?.[props.data?.slots?.title] ||
// context.value.slots?.title,
selected,
icon = slots.icon,
loading,
data,
} = props;
const title =
slots.title ||
context.value.slots?.[props.data?.slots?.title] ||
context.value.slots?.title ||
props.title;
const {
prefixCls,
showIcon,
icon: treeIcon,
draggable,
loadData,
slots: contextSlots,
} = context.value;
const disabled = isDisabled.value;
const mergedDraggable = typeof draggable === 'function' ? draggable(data) : draggable;
const wrapClass = `${prefixCls}-node-content-wrapper`;
// Icon - Still show loading icon when loading without showIcon
let $icon;
if (showIcon) {
const currentIcon = icon || context.value.slots?.[data?.slots?.icon] || treeIcon;
$icon = currentIcon ? (
<span class={classNames(`${prefixCls}-iconEle`, `${prefixCls}-icon__customize`)}>
{typeof currentIcon === 'function' ? currentIcon(renderArgsData.value) : currentIcon}
</span>
) : (
renderIcon()
);
} else if (loadData && loading) {
$icon = renderIcon();
}
// Title
let titleNode: any;
if (typeof title === 'function') {
titleNode = title(renderArgsData.value);
} else if (contextSlots.titleRender) {
titleNode = contextSlots.titleRender(renderArgsData.value);
} else {
titleNode = title;
}
titleNode = titleNode === undefined ? defaultTitle : titleNode;
const $title = <span class={`${prefixCls}-title`}>{titleNode}</span>;
return (
<span
ref={selectHandle}
title={typeof title === 'string' ? title : ''}
class={classNames(
`${wrapClass}`,
`${wrapClass}-${nodeState.value || 'normal'}`,
!disabled && (selected || dragNodeHighlight.value) && `${prefixCls}-node-selected`,
!disabled && mergedDraggable && 'draggable',
)}
draggable={(!disabled && mergedDraggable) || undefined}
aria-grabbed={(!disabled && mergedDraggable) || undefined}
onMouseenter={onMouseEnter}
onMouseleave={onMouseLeave}
onContextmenu={onContextmenu}
onClick={onSelectorClick}
onDblclick={onSelectorDoubleClick}
onDragstart={mergedDraggable ? onDragStart : undefined}
>
{$icon}
{$title}
{renderDropIndicator()}
</span>
);
};
return () => {
const {
eventKey,
dragOver,
dragOverGapTop,
dragOverGapBottom,
isLeaf,
isStart,
isEnd,
expanded,
selected,
checked,
halfChecked,
loading,
domRef,
active,
data,
onMousemove,
...otherProps
} = { ...props, ...attrs };
const { prefixCls, filterTreeNode, draggable, keyEntities, dropContainerKey, dropTargetKey } =
context.value;
const disabled = isDisabled.value;
const dataOrAriaAttributeProps = getDataAndAria(otherProps);
const { level } = keyEntities[eventKey] || {};
const isEndNode = isEnd[isEnd.length - 1];
const mergedDraggable = typeof draggable === 'function' ? draggable(data) : draggable;
return (
<div
ref={domRef}
class={classNames(attrs.class, `${prefixCls}-treenode`, {
[`${prefixCls}-treenode-disabled`]: disabled,
[`${prefixCls}-treenode-switcher-${expanded ? 'open' : 'close'}`]: !isLeaf,
[`${prefixCls}-treenode-checkbox-checked`]: checked,
[`${prefixCls}-treenode-checkbox-indeterminate`]: halfChecked,
[`${prefixCls}-treenode-selected`]: selected,
[`${prefixCls}-treenode-loading`]: loading,
[`${prefixCls}-treenode-active`]: active,
[`${prefixCls}-treenode-leaf-last`]: isEndNode,
'drop-target': dropTargetKey === eventKey,
'drop-container': dropContainerKey === eventKey,
'drag-over': !disabled && dragOver,
'drag-over-gap-top': !disabled && dragOverGapTop,
'drag-over-gap-bottom': !disabled && dragOverGapBottom,
'filter-node': filterTreeNode && filterTreeNode(eventData.value),
})}
style={attrs.style}
onDragenter={mergedDraggable ? onDragEnter : undefined}
onDragover={mergedDraggable ? onDragOver : undefined}
onDragleave={mergedDraggable ? onDragLeave : undefined}
onDrop={mergedDraggable ? onDrop : undefined}
onDragend={mergedDraggable ? onDragEnd : undefined}
onMousemove={onMousemove}
{...dataOrAriaAttributeProps}
>
<Indent prefixCls={prefixCls} level={level} isStart={isStart} isEnd={isEnd} />
{renderSwitcher()}
{renderCheckbox()}
{renderSelector()}
</div>
);
};
},
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 B

View File

@ -0,0 +1,110 @@
/**
* Webpack has bug for import loop, which is not the same behavior as ES module.
* When util.js imports the TreeNode for tree generate will cause treeContextTypes be empty.
*/
import type { ComputedRef, InjectionKey, PropType } from 'vue';
import { inject } from 'vue';
import { computed } from 'vue';
import { defineComponent, provide } from 'vue';
import type { VueNode } from '../_util/type';
import type {
IconType,
Key,
DataEntity,
EventDataNode,
DragNodeEvent,
DataNode,
Direction,
} from './interface';
export type NodeMouseEventParams = {
event: MouseEvent;
node: EventDataNode;
};
export type NodeDragEventParams = {
event: MouseEvent;
node: EventDataNode;
};
export type NodeMouseEventHandler = (e: MouseEvent, node: EventDataNode) => void;
export type NodeDragEventHandler = (
e: MouseEvent,
node: DragNodeEvent,
outsideTree?: boolean,
) => void;
export interface TreeContextProps {
prefixCls: string;
selectable: boolean;
showIcon: boolean;
icon: IconType;
switcherIcon: IconType;
draggable: ((node: DataNode) => boolean) | boolean;
checkable: boolean;
customCheckable: () => any;
checkStrictly: boolean;
disabled: boolean;
keyEntities: Record<Key, DataEntity>;
// for details see comment in Tree.state (Tree.tsx)
dropLevelOffset?: number;
dropContainerKey: Key | null;
dropTargetKey: Key | null;
dropPosition: -1 | 0 | 1 | null;
indent: number | null;
dropIndicatorRender: (props: {
dropPosition: -1 | 0 | 1;
dropLevelOffset: number;
indent: number | null;
prefixCls: string;
direction: Direction;
}) => VueNode;
dragOverNodeKey: Key | null;
direction: Direction;
loadData: (treeNode: EventDataNode) => Promise<void>;
filterTreeNode: (treeNode: EventDataNode) => boolean;
onNodeClick: NodeMouseEventHandler;
onNodeDoubleClick: NodeMouseEventHandler;
onNodeExpand: NodeMouseEventHandler;
onNodeSelect: NodeMouseEventHandler;
onNodeCheck: (e: MouseEvent, treeNode: EventDataNode, checked: boolean) => void;
onNodeLoad: (treeNode: EventDataNode) => void;
onNodeMouseEnter: NodeMouseEventHandler;
onNodeMouseLeave: NodeMouseEventHandler;
onNodeContextMenu: NodeMouseEventHandler;
onNodeDragStart: NodeDragEventHandler;
onNodeDragEnter: NodeDragEventHandler;
onNodeDragOver: NodeDragEventHandler;
onNodeDragLeave: NodeDragEventHandler;
onNodeDragEnd: NodeDragEventHandler;
onNodeDrop: NodeDragEventHandler;
slots: {
title?: (data: DataNode) => any;
titleRender?: (data: DataNode) => any;
[key: string]: (d: any) => any | undefined;
};
}
const TreeContextKey: InjectionKey<ComputedRef<TreeContextProps>> = Symbol('TreeContextKey');
export const TreeContext = defineComponent({
name: 'TreeContext',
props: {
value: { type: Object as PropType<TreeContextProps> },
},
setup(props, { slots }) {
provide(
TreeContextKey,
computed(() => props.value),
);
return () => slots.default?.();
},
});
export const useInjectTreeContext = () => {
return inject(
TreeContextKey,
computed(() => ({} as TreeContextProps)),
);
};

View File

@ -1,4 +0,0 @@
// based on rc-tree 2.1.3
import Tree from './src';
export default Tree;

View File

@ -0,0 +1,6 @@
import type { TreeProps, TreeNodeProps } from './props';
import Tree from './Tree';
import TreeNode from './TreeNode';
export { TreeNode };
export type { TreeProps, TreeNodeProps };
export default Tree;

View File

@ -0,0 +1,89 @@
import type { CSSProperties, VNode } from 'vue';
import type { TreeNodeProps } from './props';
export type { ScrollTo } from '../vc-virtual-list/List';
export interface DataNode {
checkable?: boolean;
children?: DataNode[];
disabled?: boolean;
disableCheckbox?: boolean;
icon?: IconType;
isLeaf?: boolean;
key: string | number;
title?: any;
selectable?: boolean;
switcherIcon?: IconType;
/** Set style of TreeNode. This is not recommend if you don't have any force requirement */
class?: string;
style?: CSSProperties;
slots?: Record<string, string>;
}
export interface EventDataNode extends DataNode {
expanded?: boolean;
selected?: boolean;
checked: boolean;
loaded?: boolean;
loading?: boolean;
halfChecked?: boolean;
dragOver?: boolean;
dragOverGapTop?: boolean;
dragOverGapBottom?: boolean;
pos?: string;
active?: boolean;
dataRef?: DataNode;
eventKey?: Key; // v2 key
}
export type IconType = any;
export type Key = string | number;
export type NodeElement = VNode<TreeNodeProps>;
export type DragNodeEvent = {
key: Key;
eventData: EventDataNode;
eventKey: Key;
selectHandle: HTMLSpanElement;
pos: string;
};
export interface Entity {
node: NodeElement;
index: number;
key: Key;
pos: string;
parent?: Entity;
children?: Entity[];
}
export interface DataEntity extends Omit<Entity, 'node' | 'parent' | 'children'> {
node: DataNode;
parent?: DataEntity;
children?: DataEntity[];
level: number;
}
export interface FlattenNode {
parent: FlattenNode | null;
children: FlattenNode[];
pos: string;
data: DataNode;
title: any;
key: Key;
isStart: boolean[];
isEnd: boolean[];
}
export type GetKey<RecordType> = (record: RecordType, index?: number) => Key;
export type GetCheckDisabled<RecordType> = (record: RecordType) => boolean;
export type Direction = 'ltr' | 'rtl' | undefined;
export interface FieldNames {
title?: string;
key?: string;
children?: string;
}

240
components/vc-tree/props.ts Normal file
View File

@ -0,0 +1,240 @@
import type { ExtractPropTypes, PropType } from 'vue';
import PropTypes from '../_util/vue-types';
import type {
NodeDragEventParams,
NodeMouseEventHandler,
NodeMouseEventParams,
} from './contextTypes';
import type {
DataNode,
Key,
FlattenNode,
DataEntity,
EventDataNode,
Direction,
FieldNames,
} from './interface';
export interface CheckInfo {
event: 'check';
node: EventDataNode;
checked: boolean;
nativeEvent: MouseEvent;
checkedNodes: DataNode[];
checkedNodesPositions?: { node: DataNode; pos: string }[];
halfCheckedKeys?: Key[];
}
export const treeNodeProps = {
eventKey: [String, Number], // Pass by parent `cloneElement`
prefixCls: String,
// By parent
expanded: { type: Boolean, default: undefined },
selected: { type: Boolean, default: undefined },
checked: { type: Boolean, default: undefined },
loaded: { type: Boolean, default: undefined },
loading: { type: Boolean, default: undefined },
halfChecked: { type: Boolean, default: undefined },
title: PropTypes.any,
dragOver: { type: Boolean, default: undefined },
dragOverGapTop: { type: Boolean, default: undefined },
dragOverGapBottom: { type: Boolean, default: undefined },
pos: String,
/** New added in Tree for easy data access */
data: { type: Object as PropType<DataNode>, default: undefined as DataNode },
isStart: { type: Array as PropType<boolean[]> },
isEnd: { type: Array as PropType<boolean[]> },
active: { type: Boolean, default: undefined },
onMousemove: { type: Function as PropType<EventHandlerNonNull> },
// By user
isLeaf: { type: Boolean, default: undefined },
checkable: { type: Boolean, default: undefined },
selectable: { type: Boolean, default: undefined },
disabled: { type: Boolean, default: undefined },
disableCheckbox: { type: Boolean, default: undefined },
icon: PropTypes.any,
switcherIcon: PropTypes.any,
domRef: { type: Function as PropType<(arg: any) => void> },
};
export type TreeNodeProps = Partial<ExtractPropTypes<typeof treeNodeProps>>;
export const nodeListProps = {
prefixCls: { type: String as PropType<string> },
data: { type: Array as PropType<FlattenNode[]> },
motion: { type: Object as PropType<any> },
focusable: { type: Boolean as PropType<boolean> },
activeItem: { type: Object as PropType<FlattenNode> },
focused: { type: Boolean as PropType<boolean> },
tabindex: { type: Number as PropType<number> },
checkable: { type: Boolean as PropType<boolean> },
selectable: { type: Boolean as PropType<boolean> },
disabled: { type: Boolean as PropType<boolean> },
expandedKeys: { type: Array as PropType<Key[]> },
selectedKeys: { type: Array as PropType<Key[]> },
checkedKeys: { type: Array as PropType<Key[]> },
loadedKeys: { type: Array as PropType<Key[]> },
loadingKeys: { type: Array as PropType<Key[]> },
halfCheckedKeys: { type: Array as PropType<Key[]> },
keyEntities: { type: Object as PropType<Record<Key, DataEntity>> },
dragging: { type: Boolean as PropType<boolean> },
dragOverNodeKey: { type: [String, Number] as PropType<Key> },
dropPosition: { type: Number as PropType<number> },
// Virtual list
height: { type: Number as PropType<number> },
itemHeight: { type: Number as PropType<number> },
virtual: { type: Boolean as PropType<boolean> },
onKeydown: { type: Function as PropType<EventHandlerNonNull> },
onFocus: { type: Function as PropType<(e: FocusEvent) => void> },
onBlur: { type: Function as PropType<(e: FocusEvent) => void> },
onActiveChange: { type: Function as PropType<(key: Key) => void> },
onContextmenu: { type: Function as PropType<EventHandlerNonNull> },
onListChangeStart: { type: Function as PropType<() => void> },
onListChangeEnd: { type: Function as PropType<() => void> },
};
export type NodeListProps = Partial<ExtractPropTypes<typeof nodeListProps>>;
export type AllowDrop = (options: { dropNode: DataNode; dropPosition: -1 | 0 | 1 }) => boolean;
export const treeProps = () => ({
prefixCls: String,
focusable: { type: Boolean, default: undefined },
tabindex: Number,
children: PropTypes.VNodeChild,
treeData: { type: Array as PropType<DataNode[]> }, // Generate treeNode by children
fieldNames: { type: Object as PropType<FieldNames> },
showLine: { type: Boolean, default: undefined },
showIcon: { type: Boolean, default: undefined },
icon: PropTypes.any,
selectable: { type: Boolean, default: undefined },
disabled: { type: Boolean, default: undefined },
multiple: { type: Boolean, default: undefined },
checkable: { type: Boolean, default: undefined },
checkStrictly: { type: Boolean, default: undefined },
draggable: { type: [Function, Boolean] as PropType<((node: DataNode) => boolean) | boolean> },
defaultExpandParent: { type: Boolean, default: undefined },
autoExpandParent: { type: Boolean, default: undefined },
defaultExpandAll: { type: Boolean, default: undefined },
defaultExpandedKeys: { type: Array as PropType<Key[]> },
expandedKeys: { type: Array as PropType<Key[]> },
defaultCheckedKeys: { type: Array as PropType<Key[]> },
checkedKeys: {
type: [Object, Array] as PropType<Key[] | { checked: Key[]; halfChecked: Key[] }>,
},
defaultSelectedKeys: { type: Array as PropType<Key[]> },
selectedKeys: { type: Array as PropType<Key[]> },
allowDrop: { type: Function as PropType<AllowDrop> },
dropIndicatorRender: {
type: Function as PropType<
(props: {
dropPosition: -1 | 0 | 1;
dropLevelOffset: number;
indent: number;
prefixCls: string;
direction: Direction;
}) => any
>,
},
onFocus: { type: Function as PropType<(e: FocusEvent) => void> },
onBlur: { type: Function as PropType<(e: FocusEvent) => void> },
onKeydown: { type: Function as PropType<EventHandlerNonNull> },
onContextmenu: { type: Function as PropType<EventHandlerNonNull> },
onClick: { type: Function as PropType<NodeMouseEventHandler> },
onDblclick: { type: Function as PropType<NodeMouseEventHandler> },
onScroll: { type: Function as PropType<EventHandlerNonNull> },
onExpand: {
type: Function as PropType<
(
expandedKeys: Key[],
info: {
node: EventDataNode;
expanded: boolean;
nativeEvent: MouseEvent;
},
) => void
>,
},
onCheck: {
type: Function as PropType<
(checked: { checked: Key[]; halfChecked: Key[] } | Key[], info: CheckInfo) => void
>,
},
onSelect: {
type: Function as PropType<
(
selectedKeys: Key[],
info: {
event: 'select';
selected: boolean;
node: EventDataNode;
selectedNodes: DataNode[];
nativeEvent: MouseEvent;
},
) => void
>,
},
onLoad: {
type: Function as PropType<
(
loadedKeys: Key[],
info: {
event: 'load';
node: EventDataNode;
},
) => void
>,
},
loadData: { type: Function as PropType<(treeNode: EventDataNode) => Promise<void>> },
loadedKeys: { type: Array as PropType<Key[]> },
onMouseenter: { type: Function as PropType<(info: NodeMouseEventParams) => void> },
onMouseleave: { type: Function as PropType<(info: NodeMouseEventParams) => void> },
onRightClick: {
type: Function as PropType<(info: { event: MouseEvent; node: EventDataNode }) => void>,
},
onDragstart: { type: Function as PropType<(info: NodeDragEventParams) => void> },
onDragenter: {
type: Function as PropType<(info: NodeDragEventParams & { expandedKeys: Key[] }) => void>,
},
onDragover: { type: Function as PropType<(info: NodeDragEventParams) => void> },
onDragleave: { type: Function as PropType<(info: NodeDragEventParams) => void> },
onDragend: { type: Function as PropType<(info: NodeDragEventParams) => void> },
onDrop: {
type: Function as PropType<
(
info: NodeDragEventParams & {
dragNode: EventDataNode;
dragNodesKeys: Key[];
dropPosition: number;
dropToGap: boolean;
},
) => void
>,
},
/**
* Used for `rc-tree-select` only.
* Do not use in your production code directly since this will be refactor.
*/
onActiveChange: { type: Function as PropType<(key: Key) => void> },
filterTreeNode: { type: Function as PropType<(treeNode: EventDataNode) => boolean> },
motion: PropTypes.any,
switcherIcon: PropTypes.any,
// Virtual List
height: Number,
itemHeight: Number,
virtual: { type: Boolean, default: undefined },
// direction for drag logic
direction: { type: String as PropType<Direction> },
});
export type TreeProps = Partial<ExtractPropTypes<ReturnType<typeof treeProps>>>;

View File

@ -1,686 +0,0 @@
import PropTypes, { withUndefined } from '../../_util/vue-types';
import classNames from '../../_util/classNames';
import warning from 'warning';
import { hasProp, initDefaultProps, getOptionProps, getSlot } from '../../_util/props-util';
import { cloneElement } from '../../_util/vnode';
import BaseMixin from '../../_util/BaseMixin';
import {
convertTreeToEntities,
convertDataToTree,
getPosition,
getDragNodesKeys,
parseCheckedKeys,
conductExpandParent,
calcSelectedKeys,
calcDropPosition,
arrAdd,
arrDel,
posToArr,
mapChildren,
conductCheck,
warnOnlyTreeNode,
getDataAndAria,
} from './util';
import { defineComponent } from 'vue';
/**
* Thought we still use `cloneElement` to pass `key`,
* other props can pass with context for future refactor.
*/
function getWatch(keys = []) {
const watch = {};
keys.forEach(k => {
watch[k] = {
handler() {
this.needSyncKeys[k] = true;
},
flush: 'sync',
};
});
return watch;
}
const Tree = defineComponent({
name: 'Tree',
mixins: [BaseMixin],
provide() {
return {
vcTree: this,
};
},
inheritAttrs: false,
props: initDefaultProps(
{
prefixCls: PropTypes.string,
tabindex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
children: PropTypes.any,
treeData: PropTypes.array, // Generate treeNode by children
showLine: PropTypes.looseBool,
showIcon: PropTypes.looseBool,
icon: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
focusable: PropTypes.looseBool,
selectable: PropTypes.looseBool,
disabled: PropTypes.looseBool,
multiple: PropTypes.looseBool,
checkable: withUndefined(PropTypes.oneOfType([PropTypes.object, PropTypes.looseBool])),
checkStrictly: PropTypes.looseBool,
draggable: PropTypes.looseBool,
defaultExpandParent: PropTypes.looseBool,
autoExpandParent: PropTypes.looseBool,
defaultExpandAll: PropTypes.looseBool,
defaultExpandedKeys: PropTypes.array,
expandedKeys: PropTypes.array,
defaultCheckedKeys: PropTypes.array,
checkedKeys: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
defaultSelectedKeys: PropTypes.array,
selectedKeys: PropTypes.array,
// onClick: PropTypes.func,
// onDoubleClick: PropTypes.func,
// onExpand: PropTypes.func,
// onCheck: PropTypes.func,
// onSelect: PropTypes.func,
loadData: PropTypes.func,
loadedKeys: PropTypes.array,
// onMouseEnter: PropTypes.func,
// onMouseLeave: PropTypes.func,
// onRightClick: PropTypes.func,
// onDragStart: PropTypes.func,
// onDragEnter: PropTypes.func,
// onDragOver: PropTypes.func,
// onDragLeave: PropTypes.func,
// onDragEnd: PropTypes.func,
// onDrop: PropTypes.func,
filterTreeNode: PropTypes.func,
openTransitionName: PropTypes.string,
openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
switcherIcon: PropTypes.any,
__propsSymbol__: PropTypes.any,
},
{
prefixCls: 'rc-tree',
showLine: false,
showIcon: true,
selectable: true,
multiple: false,
checkable: false,
disabled: false,
checkStrictly: false,
draggable: false,
defaultExpandParent: true,
autoExpandParent: false,
defaultExpandAll: false,
defaultExpandedKeys: [],
defaultCheckedKeys: [],
defaultSelectedKeys: [],
},
),
data() {
warning(this.$props.__propsSymbol__, 'must pass __propsSymbol__');
warning(this.$props.children, 'please use children prop replace slots.default');
this.needSyncKeys = {};
this.domTreeNodes = {};
const state = {
_posEntities: new Map(),
_keyEntities: new Map(),
_expandedKeys: [],
_selectedKeys: [],
_checkedKeys: [],
_halfCheckedKeys: [],
_loadedKeys: [],
_loadingKeys: [],
_treeNode: [],
_prevProps: null,
_dragOverNodeKey: '',
_dropPosition: null,
_dragNodesKeys: [],
};
return {
...state,
...this.getDerivedState(getOptionProps(this), state),
};
},
watch: {
// watch
...getWatch([
'treeData',
'children',
'expandedKeys',
'autoExpandParent',
'selectedKeys',
'checkedKeys',
'loadedKeys',
]),
__propsSymbol__() {
this.setState(this.getDerivedState(getOptionProps(this), this.$data));
this.needSyncKeys = {};
},
},
methods: {
getDerivedState(props, prevState) {
const { _prevProps } = prevState;
const newState = {
_prevProps: { ...props },
};
const self = this;
function needSync(name) {
return (!_prevProps && name in props) || (_prevProps && self.needSyncKeys[name]);
}
// ================== Tree Node ==================
let treeNode = null;
// Check if `treeData` or `children` changed and save into the state.
if (needSync('treeData')) {
treeNode = convertDataToTree(props.treeData);
} else if (needSync('children')) {
treeNode = props.children;
}
// Tree support filter function which will break the tree structure in the vdm.
// We cache the treeNodes in state so that we can return the treeNode in event trigger.
if (treeNode) {
newState._treeNode = treeNode;
// Calculate the entities data for quick match
const entitiesMap = convertTreeToEntities(treeNode);
newState._keyEntities = entitiesMap.keyEntities;
}
const keyEntities = newState._keyEntities || prevState._keyEntities;
// ================ expandedKeys =================
if (needSync('expandedKeys') || (_prevProps && needSync('autoExpandParent'))) {
newState._expandedKeys =
props.autoExpandParent || (!_prevProps && props.defaultExpandParent)
? conductExpandParent(props.expandedKeys, keyEntities)
: props.expandedKeys;
} else if (!_prevProps && props.defaultExpandAll) {
newState._expandedKeys = [...keyEntities.keys()];
} else if (!_prevProps && props.defaultExpandedKeys) {
newState._expandedKeys =
props.autoExpandParent || props.defaultExpandParent
? conductExpandParent(props.defaultExpandedKeys, keyEntities)
: props.defaultExpandedKeys;
}
// ================ selectedKeys =================
if (props.selectable) {
if (needSync('selectedKeys')) {
newState._selectedKeys = calcSelectedKeys(props.selectedKeys, props);
} else if (!_prevProps && props.defaultSelectedKeys) {
newState._selectedKeys = calcSelectedKeys(props.defaultSelectedKeys, props);
}
}
// ================= checkedKeys =================
if (props.checkable) {
let checkedKeyEntity;
if (needSync('checkedKeys')) {
checkedKeyEntity = parseCheckedKeys(props.checkedKeys) || {};
} else if (!_prevProps && props.defaultCheckedKeys) {
checkedKeyEntity = parseCheckedKeys(props.defaultCheckedKeys) || {};
} else if (treeNode) {
// If treeNode changed, we also need check it
checkedKeyEntity = parseCheckedKeys(props.checkedKeys) || {
checkedKeys: prevState._checkedKeys,
halfCheckedKeys: prevState._halfCheckedKeys,
};
}
if (checkedKeyEntity) {
let { checkedKeys = [], halfCheckedKeys = [] } = checkedKeyEntity;
if (!props.checkStrictly) {
const conductKeys = conductCheck(checkedKeys, true, keyEntities);
({ checkedKeys, halfCheckedKeys } = conductKeys);
}
newState._checkedKeys = checkedKeys;
newState._halfCheckedKeys = halfCheckedKeys;
}
}
// ================= loadedKeys ==================
if (needSync('loadedKeys')) {
newState._loadedKeys = props.loadedKeys;
}
return newState;
},
onNodeDragStart(event, node) {
const { _expandedKeys } = this.$data;
const { eventKey } = node;
const children = getSlot(node);
this.dragNode = node;
this.setState({
_dragNodesKeys: getDragNodesKeys(
typeof children === 'function' ? children() : children,
node,
),
_expandedKeys: arrDel(_expandedKeys, eventKey),
});
this.__emit('dragstart', { event, node });
},
/**
* [Legacy] Select handler is less small than node,
* so that this will trigger when drag enter node or select handler.
* This is a little tricky if customize css without padding.
* Better for use mouse move event to refresh drag state.
* But let's just keep it to avoid event trigger logic change.
*/
onNodeDragEnter(event, node) {
const { _expandedKeys: expandedKeys } = this.$data;
const { pos, eventKey } = node;
if (!this.dragNode || !node.selectHandle) return;
const dropPosition = calcDropPosition(event, node);
// Skip if drag node is self
if (this.dragNode.eventKey === eventKey && dropPosition === 0) {
this.setState({
_dragOverNodeKey: '',
_dropPosition: null,
});
return;
}
// Ref: https://github.com/react-component/tree/issues/132
// Add timeout to let onDragLevel fire before onDragEnter,
// so that we can clean drag props for onDragLeave node.
// Macro task for this:
// https://html.spec.whatwg.org/multipage/webappapis.html#clean-up-after-running-script
setTimeout(() => {
// Update drag over node
this.setState({
_dragOverNodeKey: eventKey,
_dropPosition: dropPosition,
});
// Side effect for delay drag
if (!this.delayedDragEnterLogic) {
this.delayedDragEnterLogic = {};
}
Object.keys(this.delayedDragEnterLogic).forEach(key => {
clearTimeout(this.delayedDragEnterLogic[key]);
});
this.delayedDragEnterLogic[pos] = setTimeout(() => {
const newExpandedKeys = arrAdd(expandedKeys, eventKey);
if (!hasProp(this, 'expandedKeys')) {
this.setState({
_expandedKeys: newExpandedKeys,
});
}
this.__emit('dragenter', { event, node, expandedKeys: newExpandedKeys });
}, 400);
}, 0);
},
onNodeDragOver(event, node) {
const { eventKey } = node;
const { _dragOverNodeKey, _dropPosition } = this.$data;
// Update drag position
if (this.dragNode && eventKey === _dragOverNodeKey && node.selectHandle) {
const dropPosition = calcDropPosition(event, node);
if (dropPosition === _dropPosition) return;
this.setState({
_dropPosition: dropPosition,
});
}
this.__emit('dragover', { event, node });
},
onNodeDragLeave(event, node) {
this.setState({
_dragOverNodeKey: '',
});
this.__emit('dragleave', { event, node });
},
onNodeDragEnd(event, node) {
this.setState({
_dragOverNodeKey: '',
});
this.__emit('dragend', { event, node });
this.dragNode = null;
},
onNodeDrop(event, node) {
const { _dragNodesKeys = [], _dropPosition } = this.$data;
const { eventKey, pos } = node;
this.setState({
_dragOverNodeKey: '',
});
if (_dragNodesKeys.indexOf(eventKey) !== -1) {
warning(false, "Can not drop to dragNode(include it's children node)");
return;
}
const posArr = posToArr(pos);
const dropResult = {
event,
node,
dragNode: this.dragNode,
dragNodesKeys: _dragNodesKeys.slice(),
dropPosition: _dropPosition + Number(posArr[posArr.length - 1]),
dropToGap: false,
};
if (_dropPosition !== 0) {
dropResult.dropToGap = true;
}
this.__emit('drop', dropResult);
this.dragNode = null;
},
onNodeClick(e, treeNode) {
this.__emit('click', e, treeNode);
},
onNodeDoubleClick(e, treeNode) {
this.__emit('dblclick', e, treeNode);
},
onNodeSelect(e, treeNode) {
let { _selectedKeys: selectedKeys } = this.$data;
const { _keyEntities: keyEntities } = this.$data;
const { multiple } = this.$props;
const { selected, eventKey } = getOptionProps(treeNode);
const targetSelected = !selected;
// Update selected keys
if (!targetSelected) {
selectedKeys = arrDel(selectedKeys, eventKey);
} else if (!multiple) {
selectedKeys = [eventKey];
} else {
selectedKeys = arrAdd(selectedKeys, eventKey);
}
// [Legacy] Not found related usage in doc or upper libs
const selectedNodes = selectedKeys
.map(key => {
const entity = keyEntities.get(key);
if (!entity) return null;
return entity.node;
})
.filter(node => node);
this.setUncontrolledState({ _selectedKeys: selectedKeys });
const eventObj = {
event: 'select',
selected: targetSelected,
node: treeNode,
selectedNodes,
nativeEvent: e,
};
this.__emit('select', selectedKeys, eventObj);
},
onNodeCheck(e, treeNode, checked) {
const {
_keyEntities: keyEntities,
_checkedKeys: oriCheckedKeys,
_halfCheckedKeys: oriHalfCheckedKeys,
} = this.$data;
const { checkStrictly } = this.$props;
const { eventKey } = getOptionProps(treeNode);
// Prepare trigger arguments
let checkedObj;
const eventObj = {
event: 'check',
node: treeNode,
checked,
nativeEvent: e,
};
if (checkStrictly) {
const checkedKeys = checked
? arrAdd(oriCheckedKeys, eventKey)
: arrDel(oriCheckedKeys, eventKey);
const halfCheckedKeys = arrDel(oriHalfCheckedKeys, eventKey);
checkedObj = { checked: checkedKeys, halfChecked: halfCheckedKeys };
eventObj.checkedNodes = checkedKeys
.map(key => keyEntities.get(key))
.filter(entity => entity)
.map(entity => entity.node);
this.setUncontrolledState({ _checkedKeys: checkedKeys });
} else {
const { checkedKeys, halfCheckedKeys } = conductCheck([eventKey], checked, keyEntities, {
checkedKeys: oriCheckedKeys,
halfCheckedKeys: oriHalfCheckedKeys,
});
checkedObj = checkedKeys;
// [Legacy] This is used for `rc-tree-select`
eventObj.checkedNodes = [];
eventObj.checkedNodesPositions = [];
eventObj.halfCheckedKeys = halfCheckedKeys;
checkedKeys.forEach(key => {
const entity = keyEntities.get(key);
if (!entity) return;
const { node, pos } = entity;
eventObj.checkedNodes.push(node);
eventObj.checkedNodesPositions.push({ node, pos });
});
this.setUncontrolledState({
_checkedKeys: checkedKeys,
_halfCheckedKeys: halfCheckedKeys,
});
}
this.__emit('check', checkedObj, eventObj);
},
onNodeLoad(treeNode) {
return new Promise(resolve => {
// We need to get the latest state of loading/loaded keys
this.setState(({ _loadedKeys: loadedKeys = [], _loadingKeys: loadingKeys = [] }) => {
const { loadData } = this.$props;
const { eventKey } = getOptionProps(treeNode);
if (
!loadData ||
loadedKeys.indexOf(eventKey) !== -1 ||
loadingKeys.indexOf(eventKey) !== -1
) {
return {};
}
// Process load data
const promise = loadData(treeNode);
promise.then(() => {
const { _loadedKeys: currentLoadedKeys, _loadingKeys: currentLoadingKeys } = this.$data;
const newLoadedKeys = arrAdd(currentLoadedKeys, eventKey);
const newLoadingKeys = arrDel(currentLoadingKeys, eventKey);
// onLoad should trigger before internal setState to avoid `loadData` trigger twice.
// https://github.com/ant-design/ant-design/issues/12464
this.__emit('load', newLoadedKeys, {
event: 'load',
node: treeNode,
});
this.setUncontrolledState({
_loadedKeys: newLoadedKeys,
});
this.setState({
_loadingKeys: newLoadingKeys,
});
resolve();
});
return {
_loadingKeys: arrAdd(loadingKeys, eventKey),
};
});
});
},
onNodeExpand(e, treeNode) {
let { _expandedKeys: expandedKeys } = this.$data;
const { loadData } = this.$props;
const { eventKey, expanded } = getOptionProps(treeNode);
// Update selected keys
const index = expandedKeys.indexOf(eventKey);
const targetExpanded = !expanded;
warning(
(expanded && index !== -1) || (!expanded && index === -1),
'Expand state not sync with index check',
);
if (targetExpanded) {
expandedKeys = arrAdd(expandedKeys, eventKey);
} else {
expandedKeys = arrDel(expandedKeys, eventKey);
}
this.setUncontrolledState({ _expandedKeys: expandedKeys });
this.__emit('expand', expandedKeys, {
node: treeNode,
expanded: targetExpanded,
nativeEvent: e,
});
// Async Load data
if (targetExpanded && loadData) {
const loadPromise = this.onNodeLoad(treeNode);
return loadPromise
? loadPromise.then(() => {
// [Legacy] Refresh logic
this.setUncontrolledState({ _expandedKeys: expandedKeys });
})
: null;
}
return null;
},
onNodeMouseEnter(event, node) {
this.__emit('mouseenter', { event, node });
},
onNodeMouseLeave(event, node) {
this.__emit('mouseleave', { event, node });
},
onNodeContextMenu(event, node) {
event.preventDefault();
this.__emit('rightClick', { event, node });
},
/**
* Only update the value which is not in props
*/
setUncontrolledState(state) {
let needSync = false;
const newState = {};
const props = getOptionProps(this);
Object.keys(state).forEach(name => {
if (name.replace('_', '') in props) return;
needSync = true;
newState[name] = state[name];
});
if (needSync) {
this.setState(newState);
}
},
registerTreeNode(key, node) {
if (node) {
this.domTreeNodes[key] = node;
} else {
delete this.domTreeNodes[key];
}
},
isKeyChecked(key) {
const { _checkedKeys: checkedKeys = [] } = this.$data;
return checkedKeys.indexOf(key) !== -1;
},
/**
* [Legacy] Original logic use `key` as tracking clue.
* We have to use `cloneElement` to pass `key`.
*/
renderTreeNode(child, index, level = 0) {
const {
_keyEntities: keyEntities,
_expandedKeys: expandedKeys = [],
_selectedKeys: selectedKeys = [],
_halfCheckedKeys: halfCheckedKeys = [],
_loadedKeys: loadedKeys = [],
_loadingKeys: loadingKeys = [],
_dragOverNodeKey: dragOverNodeKey,
_dropPosition: dropPosition,
} = this.$data;
const pos = getPosition(level, index);
let key = child.key;
if (!key && (key === undefined || key === null)) {
key = pos;
}
if (!keyEntities.get(key)) {
warnOnlyTreeNode();
return null;
}
return cloneElement(child, {
eventKey: key,
expanded: expandedKeys.indexOf(key) !== -1,
selected: selectedKeys.indexOf(key) !== -1,
loaded: loadedKeys.indexOf(key) !== -1,
loading: loadingKeys.indexOf(key) !== -1,
checked: this.isKeyChecked(key),
halfChecked: halfCheckedKeys.indexOf(key) !== -1,
pos,
// [Legacy] Drag props
dragOver: dragOverNodeKey === key && dropPosition === 0,
dragOverGapTop: dragOverNodeKey === key && dropPosition === -1,
dragOverGapBottom: dragOverNodeKey === key && dropPosition === 1,
key,
});
},
},
render() {
const { _treeNode: treeNode } = this.$data;
const { prefixCls, focusable, showLine, tabindex = 0 } = this.$props;
const domProps = getDataAndAria({ ...this.$props, ...this.$attrs });
const { class: className, style } = this.$attrs;
return (
<ul
{...domProps}
class={classNames(prefixCls, className, {
[`${prefixCls}-show-line`]: showLine,
})}
style={style}
role="tree"
unselectable="on"
tabindex={focusable ? tabindex : null}
>
{mapChildren(treeNode, (node, index) => this.renderTreeNode(node, index))}
</ul>
);
},
});
export { Tree };
export default Tree;

View File

@ -1,578 +0,0 @@
import { defineComponent, inject, provide } from 'vue';
import PropTypes from '../../_util/vue-types';
import classNames from '../../_util/classNames';
import { getNodeChildren, mapChildren, warnOnlyTreeNode, getDataAndAria } from './util';
import { initDefaultProps, getComponent, getSlot } from '../../_util/props-util';
import BaseMixin from '../../_util/BaseMixin';
import { getTransitionProps, Transition } from '../../_util/transition';
function noop() {}
const ICON_OPEN = 'open';
const ICON_CLOSE = 'close';
const defaultTitle = '---';
const TreeNode = defineComponent({
name: 'TreeNode',
mixins: [BaseMixin],
inheritAttrs: false,
__ANT_TREE_NODE: true,
props: initDefaultProps(
{
eventKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), // Pass by parent `cloneElement`
prefixCls: PropTypes.string,
// className: PropTypes.string,
root: PropTypes.object,
// onSelect: PropTypes.func,
// By parent
expanded: PropTypes.looseBool,
selected: PropTypes.looseBool,
checked: PropTypes.looseBool,
loaded: PropTypes.looseBool,
loading: PropTypes.looseBool,
halfChecked: PropTypes.looseBool,
title: PropTypes.any,
pos: PropTypes.string,
dragOver: PropTypes.looseBool,
dragOverGapTop: PropTypes.looseBool,
dragOverGapBottom: PropTypes.looseBool,
// By user
isLeaf: PropTypes.looseBool,
checkable: PropTypes.looseBool,
selectable: PropTypes.looseBool,
disabled: PropTypes.looseBool,
disableCheckbox: PropTypes.looseBool,
icon: PropTypes.any,
dataRef: PropTypes.object,
switcherIcon: PropTypes.any,
label: PropTypes.any,
value: PropTypes.any,
},
{},
),
setup() {
return {
vcTree: inject('vcTree', {}),
vcTreeNode: inject('vcTreeNode', {}),
};
},
data() {
this.children = null;
return {
dragNodeHighlight: false,
};
},
created() {
provide('vcTreeNode', this);
},
// Isomorphic needn't load data in server side
mounted() {
const {
eventKey,
vcTree: { registerTreeNode },
} = this;
this.syncLoadData(this.$props);
registerTreeNode && registerTreeNode(eventKey, this);
},
updated() {
this.syncLoadData(this.$props);
},
beforeUnmount() {
const {
eventKey,
vcTree: { registerTreeNode },
} = this;
registerTreeNode && registerTreeNode(eventKey, null);
},
methods: {
onSelectorClick(e) {
// Click trigger before select/check operation
const {
vcTree: { onNodeClick },
} = this;
onNodeClick(e, this);
if (this.isSelectable()) {
this.onSelect(e);
} else {
this.onCheck(e);
}
},
onSelectorDoubleClick(e) {
const {
vcTree: { onNodeDoubleClick },
} = this;
onNodeDoubleClick(e, this);
},
onSelect(e) {
if (this.isDisabled()) return;
const {
vcTree: { onNodeSelect },
} = this;
e.preventDefault();
onNodeSelect(e, this);
},
onCheck(e) {
if (this.isDisabled()) return;
const { disableCheckbox, checked } = this;
const {
vcTree: { onNodeCheck },
} = this;
if (!this.isCheckable() || disableCheckbox) return;
e.preventDefault();
const targetChecked = !checked;
onNodeCheck(e, this, targetChecked);
},
onMouseEnter(e) {
const {
vcTree: { onNodeMouseEnter },
} = this;
onNodeMouseEnter(e, this);
},
onMouseLeave(e) {
const {
vcTree: { onNodeMouseLeave },
} = this;
onNodeMouseLeave(e, this);
},
onContextMenu(e) {
const {
vcTree: { onNodeContextMenu },
} = this;
onNodeContextMenu(e, this);
},
onDragStart(e) {
const {
vcTree: { onNodeDragStart },
} = this;
e.stopPropagation();
this.setState({
dragNodeHighlight: true,
});
onNodeDragStart(e, this);
try {
// ie throw error
// firefox-need-it
e.dataTransfer.setData('text/plain', '');
} catch (error) {
// empty
}
},
onDragEnter(e) {
const {
vcTree: { onNodeDragEnter },
} = this;
e.preventDefault();
e.stopPropagation();
onNodeDragEnter(e, this);
},
onDragOver(e) {
const {
vcTree: { onNodeDragOver },
} = this;
e.preventDefault();
e.stopPropagation();
onNodeDragOver(e, this);
},
onDragLeave(e) {
const {
vcTree: { onNodeDragLeave },
} = this;
e.stopPropagation();
onNodeDragLeave(e, this);
},
onDragEnd(e) {
const {
vcTree: { onNodeDragEnd },
} = this;
e.stopPropagation();
this.setState({
dragNodeHighlight: false,
});
onNodeDragEnd(e, this);
},
onDrop(e) {
const {
vcTree: { onNodeDrop },
} = this;
e.preventDefault();
e.stopPropagation();
this.setState({
dragNodeHighlight: false,
});
onNodeDrop(e, this);
},
// Disabled item still can be switch
onExpand(e) {
const {
vcTree: { onNodeExpand },
} = this;
onNodeExpand(e, this);
},
// Drag usage
setSelectHandle(node) {
this.selectHandle = node;
},
getNodeChildren() {
const originList = this.children;
const targetList = getNodeChildren(originList);
if (originList.length !== targetList.length) {
warnOnlyTreeNode();
}
return targetList;
},
getNodeState() {
const { expanded } = this;
if (this.isLeaf2()) {
return null;
}
return expanded ? ICON_OPEN : ICON_CLOSE;
},
isLeaf2() {
const { isLeaf, loaded } = this;
const {
vcTree: { loadData },
} = this;
const hasChildren = this.getNodeChildren().length !== 0;
if (isLeaf === false) {
return false;
}
return isLeaf || (!loadData && !hasChildren) || (loadData && loaded && !hasChildren);
},
isDisabled() {
const { disabled } = this;
const {
vcTree: { disabled: treeDisabled },
} = this;
// Follow the logic of Selectable
if (disabled === false) {
return false;
}
return !!(treeDisabled || disabled);
},
isCheckable() {
const { checkable } = this.$props;
const {
vcTree: { checkable: treeCheckable },
} = this;
// Return false if tree or treeNode is not checkable
if (!treeCheckable || checkable === false) return false;
return treeCheckable;
},
// Load data to avoid default expanded tree without data
syncLoadData(props) {
const { expanded, loading, loaded } = props;
const {
vcTree: { loadData, onNodeLoad },
} = this;
if (loading) return;
// read from state to avoid loadData at same time
if (loadData && expanded && !this.isLeaf2()) {
// We needn't reload data when has children in sync logic
// It's only needed in node expanded
const hasChildren = this.getNodeChildren().length !== 0;
if (!hasChildren && !loaded) {
onNodeLoad(this);
}
}
},
isSelectable() {
const { selectable } = this;
const {
vcTree: { selectable: treeSelectable },
} = this;
// Ignore when selectable is undefined or null
if (typeof selectable === 'boolean') {
return selectable;
}
return treeSelectable;
},
// Switcher
renderSwitcher() {
const { expanded } = this;
const {
vcTree: { prefixCls },
} = this;
const switcherIcon =
getComponent(this, 'switcherIcon', {}, false) ||
getComponent(this.vcTree, 'switcherIcon', {}, false);
if (this.isLeaf2()) {
return (
<span
key="switcher"
class={classNames(`${prefixCls}-switcher`, `${prefixCls}-switcher-noop`)}
>
{typeof switcherIcon === 'function'
? switcherIcon({ ...this.$props, ...this.$props.dataRef, isLeaf: true })
: switcherIcon}
</span>
);
}
const switcherCls = classNames(
`${prefixCls}-switcher`,
`${prefixCls}-switcher_${expanded ? ICON_OPEN : ICON_CLOSE}`,
);
return (
<span key="switcher" onClick={this.onExpand} class={switcherCls}>
{typeof switcherIcon === 'function'
? switcherIcon({ ...this.$props, ...this.$props.dataRef, isLeaf: false })
: switcherIcon}
</span>
);
},
// Checkbox
renderCheckbox() {
const { checked, halfChecked, disableCheckbox } = this;
const {
vcTree: { prefixCls },
} = this;
const disabled = this.isDisabled();
const checkable = this.isCheckable();
if (!checkable) return null;
// [Legacy] Custom element should be separate with `checkable` in future
const $custom = typeof checkable !== 'boolean' ? checkable : null;
return (
<span
key="checkbox"
class={classNames(
`${prefixCls}-checkbox`,
checked && `${prefixCls}-checkbox-checked`,
!checked && halfChecked && `${prefixCls}-checkbox-indeterminate`,
(disabled || disableCheckbox) && `${prefixCls}-checkbox-disabled`,
)}
onClick={this.onCheck}
>
{$custom}
</span>
);
},
renderIcon() {
const { loading } = this;
const {
vcTree: { prefixCls },
} = this;
return (
<span
key="icon"
class={classNames(
`${prefixCls}-iconEle`,
`${prefixCls}-icon__${this.getNodeState() || 'docu'}`,
loading && `${prefixCls}-icon_loading`,
)}
/>
);
},
// Icon + Title
renderSelector() {
const { selected, loading, dragNodeHighlight } = this;
const icon = getComponent(this, 'icon', {}, false);
const {
vcTree: { prefixCls, showIcon, icon: treeIcon, draggable, loadData },
} = this;
const disabled = this.isDisabled();
const title = getComponent(this, 'title', {}, false);
const wrapClass = `${prefixCls}-node-content-wrapper`;
// Icon - Still show loading icon when loading without showIcon
let $icon;
if (showIcon) {
const currentIcon = icon || treeIcon;
$icon = currentIcon ? (
<span class={classNames(`${prefixCls}-iconEle`, `${prefixCls}-icon__customize`)}>
{typeof currentIcon === 'function'
? currentIcon({ ...this.$props, ...this.$props.dataRef })
: currentIcon}
</span>
) : (
this.renderIcon()
);
} else if (loadData && loading) {
$icon = this.renderIcon();
}
const currentTitle = title;
let $title = currentTitle ? (
<span class={`${prefixCls}-title`}>
{typeof currentTitle === 'function'
? currentTitle({ ...this.$props, ...this.$props.dataRef })
: currentTitle}
</span>
) : (
<span class={`${prefixCls}-title`}>{defaultTitle}</span>
);
return (
<span
key="selector"
ref={this.setSelectHandle}
title={typeof title === 'string' ? title : ''}
class={classNames(
`${wrapClass}`,
`${wrapClass}-${this.getNodeState() || 'normal'}`,
!disabled && (selected || dragNodeHighlight) && `${prefixCls}-node-selected`,
!disabled && draggable && 'draggable',
)}
draggable={(!disabled && draggable) || undefined}
aria-grabbed={(!disabled && draggable) || undefined}
onMouseenter={this.onMouseEnter}
onMouseleave={this.onMouseLeave}
onContextmenu={this.onContextMenu}
onClick={this.onSelectorClick}
onDblclick={this.onSelectorDoubleClick}
onDragstart={draggable ? this.onDragStart : noop}
>
{$icon}
{$title}
</span>
);
},
// Children list wrapped with `Animation`
renderChildren() {
const { expanded, pos } = this;
const {
vcTree: { prefixCls, openTransitionName, openAnimation, renderTreeNode },
} = this;
let animProps = {};
if (openTransitionName) {
animProps = getTransitionProps(openTransitionName);
} else if (typeof openAnimation === 'object') {
animProps = { ...openAnimation, css: false, ...animProps };
}
// Children TreeNode
const nodeList = this.getNodeChildren();
if (nodeList.length === 0) {
return null;
}
let $children;
if (expanded) {
$children = (
<ul
class={classNames(
`${prefixCls}-child-tree`,
expanded && `${prefixCls}-child-tree-open`,
)}
data-expanded={expanded}
role="group"
>
{mapChildren(nodeList, (node, index) => renderTreeNode(node, index, pos))}
</ul>
);
}
return <Transition {...animProps}>{$children}</Transition>;
},
},
render() {
this.children = getSlot(this);
const {
dragOver,
dragOverGapTop,
dragOverGapBottom,
isLeaf,
expanded,
selected,
checked,
halfChecked,
loading,
} = this.$props;
const {
vcTree: { prefixCls, filterTreeNode, draggable },
} = this;
const disabled = this.isDisabled();
const dataOrAriaAttributeProps = getDataAndAria({ ...this.$props, ...this.$attrs });
const { class: className, style } = this.$attrs;
return (
<li
class={{
[className]: className,
[`${prefixCls}-treenode-disabled`]: disabled,
[`${prefixCls}-treenode-switcher-${expanded ? 'open' : 'close'}`]: !isLeaf,
[`${prefixCls}-treenode-checkbox-checked`]: checked,
[`${prefixCls}-treenode-checkbox-indeterminate`]: halfChecked,
[`${prefixCls}-treenode-selected`]: selected,
[`${prefixCls}-treenode-loading`]: loading,
'drag-over': !disabled && dragOver,
'drag-over-gap-top': !disabled && dragOverGapTop,
'drag-over-gap-bottom': !disabled && dragOverGapBottom,
'filter-node': filterTreeNode && filterTreeNode(this),
}}
style={style}
role="treeitem"
onDragenter={draggable ? this.onDragEnter : noop}
onDragover={draggable ? this.onDragOver : noop}
onDragleave={draggable ? this.onDragLeave : noop}
onDrop={draggable ? this.onDrop : noop}
onDragend={draggable ? this.onDragEnd : noop}
{...dataOrAriaAttributeProps}
>
{this.renderSwitcher()}
{this.renderCheckbox()}
{this.renderSelector()}
{this.renderChildren()}
</li>
);
},
});
TreeNode.isTreeNode = 1;
export default TreeNode;

View File

@ -1,5 +0,0 @@
import Tree from './Tree';
import TreeNode from './TreeNode';
Tree.TreeNode = TreeNode;
export default Tree;

Some files were not shown because too many files have changed in this diff Show More