Merge branch 'refactor-tree' into v2.3
commit
d72798bc08
|
|
@ -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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// mixins for clearfix
|
// mixins for clearfix
|
||||||
// ------------------------
|
// ------------------------
|
||||||
.clearfix() {
|
.clearfix() {
|
||||||
zoom: 1;
|
|
||||||
&::before,
|
&::before,
|
||||||
&::after {
|
&::after {
|
||||||
display: table;
|
display: table;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
});
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
@ -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-node移除后,删除相关代码,不要再render中调用 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>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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__={[]} />;
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
title 的渲染逻辑
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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`} />;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 ==========================
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}[];
|
}[];
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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];
|
||||||
|
}
|
||||||
|
|
@ -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];
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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';
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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']>
|
||||||
|
>;
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
import BasePopup from '../Base/BasePopup';
|
|
||||||
|
|
||||||
export default BasePopup;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</span>
|
|
||||||
{renderPlaceholder && !mirrorSearchValue ? renderPlaceholder() : null}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SearchInput;
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export const SHOW_ALL = 'SHOW_ALL';
|
|
||||||
export const SHOW_PARENT = 'SHOW_PARENT';
|
|
||||||
export const SHOW_CHILD = 'SHOW_CHILD';
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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} />;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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 |
|
|
@ -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)),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
// based on rc-tree 2.1.3
|
|
||||||
import Tree from './src';
|
|
||||||
|
|
||||||
export default Tree;
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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>>>;
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -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
Loading…
Reference in New Issue