refactor: tree-select
parent
ae36627763
commit
415c2888c4
|
@ -16,6 +16,12 @@ 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 RefOptionListProps {
|
||||||
|
onKeydown: KeyboardEvent;
|
||||||
|
onKeyup: KeyboardEvent;
|
||||||
|
scrollTo?: (index: number) => void;
|
||||||
|
}
|
||||||
export interface OptionListProps {
|
export interface OptionListProps {
|
||||||
prefixCls: string;
|
prefixCls: string;
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
import type { FlattenDataNode, Key, LegacyDataNode, RawValueType } from './interface';
|
||||||
|
import type { SkipType } from './hooks/useKeyValueMapping';
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
ComputedRef,
|
||||||
|
defineComponent,
|
||||||
|
inject,
|
||||||
|
InjectionKey,
|
||||||
|
PropType,
|
||||||
|
provide,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
interface ContextProps {
|
||||||
|
checkable: boolean;
|
||||||
|
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: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 useInjectSelectContext = () => {
|
||||||
|
return inject(
|
||||||
|
SelectContextKey,
|
||||||
|
computed(() => ({} as ContextProps)),
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,57 @@
|
||||||
|
import type { FlattenDataNode, RawValueType, DataNode, TreeDataNode, Key } from './interface';
|
||||||
|
import { SelectContext } from './Context';
|
||||||
|
import { RefOptionListProps } from '../vc-select/OptionList';
|
||||||
|
import { ScrollTo } from '../vc-virtual-list/List';
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import { optionListProps } from './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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OptionListProps<OptionsType extends object[]> {
|
||||||
|
prefixCls: string;
|
||||||
|
id: string;
|
||||||
|
options: OptionsType;
|
||||||
|
flattenOptions: FlattenDataNode[];
|
||||||
|
height: number;
|
||||||
|
itemHeight: number;
|
||||||
|
virtual?: boolean;
|
||||||
|
values: Set<RawValueType>;
|
||||||
|
multiple: boolean;
|
||||||
|
open: boolean;
|
||||||
|
defaultActiveFirstOption?: boolean;
|
||||||
|
notFoundContent?: any;
|
||||||
|
menuItemSelectedIcon?: any;
|
||||||
|
childrenAsData: boolean;
|
||||||
|
searchValue: string;
|
||||||
|
|
||||||
|
onSelect: (value: RawValueType, option: { selected: boolean }) => void;
|
||||||
|
onToggleOpen: (open?: boolean) => void;
|
||||||
|
/** Tell Select that some value is now active to make accessibility work */
|
||||||
|
onActiveValue: (value: RawValueType, index: number) => void;
|
||||||
|
onScroll: UIEvent;
|
||||||
|
|
||||||
|
onMouseenter: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReviseRefOptionListProps = Omit<RefOptionListProps, 'scrollTo'> & { scrollTo: ScrollTo };
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'OptionList',
|
||||||
|
props: optionListProps(),
|
||||||
|
slots: ['notFoundContent', 'menuItemSelectedIcon'],
|
||||||
|
});
|
|
@ -0,0 +1,14 @@
|
||||||
|
/* istanbul ignore file */
|
||||||
|
|
||||||
|
import { 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> = () => null;
|
||||||
|
TreeNode.inheritAttrs = false;
|
||||||
|
TreeNode.displayName = 'ATreeSelectNode';
|
||||||
|
export default TreeNode;
|
|
@ -0,0 +1,8 @@
|
||||||
|
import generate, { TreeSelectProps } from './generate';
|
||||||
|
import OptionList from './OptionList';
|
||||||
|
|
||||||
|
const TreeSelect = generate({ prefixCls: 'vc-tree-select', optionList: OptionList as any });
|
||||||
|
|
||||||
|
export { TreeSelectProps };
|
||||||
|
|
||||||
|
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,6 @@
|
||||||
|
import { GenerateConfig } from '../vc-select/generate';
|
||||||
|
|
||||||
|
export default function generate(config: {
|
||||||
|
prefixCls: string;
|
||||||
|
optionList: GenerateConfig<any>['components']['optionList'];
|
||||||
|
}) {}
|
|
@ -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,147 @@
|
||||||
|
import { warning } from 'ant-design-vue/es/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,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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,7 @@
|
||||||
|
import TreeSelect, { TreeSelectProps } from './TreeSelect';
|
||||||
|
import TreeNode from './TreeNode';
|
||||||
|
import { SHOW_ALL, SHOW_CHILD, SHOW_PARENT } from './utils/strategyUtil';
|
||||||
|
|
||||||
|
export { TreeNode, SHOW_ALL, SHOW_CHILD, SHOW_PARENT, TreeSelectProps };
|
||||||
|
|
||||||
|
export default TreeSelect;
|
|
@ -0,0 +1,93 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,41 @@
|
||||||
|
import type { ExtractPropTypes, PropType } from 'vue';
|
||||||
|
import type { DataNode } from '../tree';
|
||||||
|
import PropTypes from '../_util/vue-types';
|
||||||
|
import type { FlattenDataNode, RawValueType } from './interface';
|
||||||
|
|
||||||
|
export function optionListProps<OptionsType extends object[]>() {
|
||||||
|
return {
|
||||||
|
prefixCls: String,
|
||||||
|
id: String,
|
||||||
|
options: { type: Array as PropType<unknown> 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> },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class Helper<T extends object[]> {
|
||||||
|
Return = optionListProps<T>();
|
||||||
|
}
|
||||||
|
type FuncReturnType<T extends object[]> = Helper<T>['Return'];
|
||||||
|
|
||||||
|
export type OptionListProps = Partial<ExtractPropTypes<FuncReturnType<DataNode[]>>>;
|
|
@ -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/utils/treeUtil';
|
|
||||||
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,153 @@
|
||||||
|
import { warning } from '../../vc-util/warning';
|
||||||
|
import { isValidElement } from '../../_util/props-util';
|
||||||
|
import type {
|
||||||
|
DataNode,
|
||||||
|
LegacyDataNode,
|
||||||
|
ChangeEventExtra,
|
||||||
|
InternalDataEntity,
|
||||||
|
RawValueType,
|
||||||
|
LegacyCheckedNode,
|
||||||
|
} from '../interface';
|
||||||
|
import TreeNode from '../TreeNode';
|
||||||
|
|
||||||
|
export function convertChildrenToData(nodes): DataNode[] {
|
||||||
|
return nodes
|
||||||
|
.map(node => {
|
||||||
|
if (!isValidElement(node) || !node.type) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
key,
|
||||||
|
props: { children, value, ...restProps },
|
||||||
|
} = node;
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
...restProps,
|
||||||
|
};
|
||||||
|
|
||||||
|
const childData = convertChildrenToData(children);
|
||||||
|
if (childData.length) {
|
||||||
|
data.children = childData;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
.filter(data => data);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 'ant-design-vue/es/vc-tree/interface';
|
||||||
|
import { flattenTreeData } from 'ant-design-vue/es/vc-tree/utils/treeUtil';
|
||||||
|
import type { FilterFunc } from 'ant-design-vue/es/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,32 @@
|
||||||
|
import { warning } from '../../vc-util/warning';
|
||||||
|
import type { TreeSelectProps } from '../TreeSelect';
|
||||||
|
import { toArray } from './valueUtil';
|
||||||
|
|
||||||
|
function warningProps(props: TreeSelectProps) {
|
||||||
|
const { searchPlaceholder, treeCheckStrictly, treeCheckable, labelInValue, value, multiple } =
|
||||||
|
props;
|
||||||
|
|
||||||
|
warning(!searchPlaceholder, '`searchPlaceholder` has been removed.');
|
||||||
|
|
||||||
|
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;
|
Loading…
Reference in New Issue