refactor: transfer、tooltip (#4306)

* refactor(transfer): use composition api (#4135)

* refactor(transfer): use composition api

* fix: remove console

* refactor(tooltip): use composition api (#4059)

* refactor(tooltip): use composition api

* chore: useConfigInject

* fix: remove useless

* style: format code

* refactor: transfer

* refactor: tooltip

Co-authored-by: ajuner <106791576@qq.com>
refactor-progress
tangjinzhou 2021-07-06 16:16:57 +08:00 committed by GitHub
parent 16ee0dd2f1
commit 8198cab549
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 2049 additions and 1522 deletions

View File

@ -92,10 +92,15 @@ const Dropdown = defineComponent({
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('dropdown', customizePrefixCls);
const child = getSlot(this)[0];
const dropdownTrigger = cloneElement(child, {
class: classNames(child?.props?.class, `${prefixCls}-trigger`),
disabled,
});
const dropdownTrigger = cloneElement(
child,
Object.assign(
{
class: classNames(child?.props?.class, `${prefixCls}-trigger`),
},
disabled ? { disabled } : {},
),
);
const triggerActions = disabled ? [] : typeof trigger === 'string' ? [trigger] : trigger;
let alignPoint;
if (triggerActions && triggerActions.indexOf('contextmenu') !== -1) {

View File

@ -19,9 +19,9 @@ describe('Slider', () => {
await asyncExpect(() => {
expect(document.body.innerHTML).toMatchSnapshot();
wrapper.findAll('.ant-slider-handle')[0].trigger('mouseleave');
}, 0);
}, 100);
await asyncExpect(() => {
expect(document.body.innerHTML).toMatchSnapshot();
}, 0);
}, 100);
});
});

View File

@ -775,9 +775,12 @@
// Transfer
// ---
@transfer-header-height: 40px;
@transfer-item-height: @height-base;
@transfer-disabled-bg: @disabled-bg;
@transfer-list-height: 200px;
@transfer-item-hover-bg: @item-hover-bg;
@transfer-item-padding-vertical: 6px;
@transfer-list-search-icon-top: 12px;
// Message
// ---

View File

@ -1,21 +1,33 @@
import type { ExtractPropTypes, CSSProperties } from 'vue';
import { defineComponent, inject } from 'vue';
import { computed, watch } from 'vue';
import { defineComponent, onMounted, ref } from 'vue';
import VcTooltip from '../vc-tooltip';
import classNames from '../_util/classNames';
import getPlacements from './placements';
import PropTypes from '../_util/vue-types';
import { PresetColorTypes } from '../_util/colors';
import {
hasProp,
getComponent,
getStyle,
filterEmpty,
getSlot,
isValidElement,
} from '../_util/props-util';
import warning from '../_util/warning';
import { getPropsSlot, getStyle, filterEmpty, isValidElement } from '../_util/props-util';
import { cloneElement } from '../_util/vnode';
import { defaultConfigProvider } from '../config-provider';
import type { triggerTypes, placementTypes } from './abstractTooltipProps';
import abstractTooltipProps from './abstractTooltipProps';
import useConfigInject from '../_util/hooks/useConfigInject';
import getPlacements, { AdjustOverflow, PlacementsConfig } from './placements';
export { AdjustOverflow, PlacementsConfig };
export type TooltipPlacement = typeof placementTypes;
// https://github.com/react-component/tooltip
// https://github.com/yiminghe/dom-align
export interface TooltipAlignConfig {
points?: [string, string];
offset?: [number | string, number | string];
targetOffset?: [number | string, number | string];
overflow?: { adjustX: boolean; adjustY: boolean };
useCssRight?: boolean;
useCssBottom?: boolean;
useCssTransform?: boolean;
}
const splitObject = (obj: any, keys: string[]) => {
const picked = {};
@ -37,6 +49,10 @@ const tooltipProps = {
title: PropTypes.VNodeChild,
};
export type TriggerTypes = typeof triggerTypes[number];
export type PlacementTypes = typeof placementTypes[number];
export type TooltipProps = Partial<ExtractPropTypes<typeof tooltipProps>>;
export default defineComponent({
@ -44,52 +60,59 @@ export default defineComponent({
inheritAttrs: false,
props: tooltipProps,
emits: ['update:visible', 'visibleChange'],
setup() {
return {
configProvider: inject('configProvider', defaultConfigProvider),
};
},
data() {
return {
sVisible: !!this.$props.visible || !!this.$props.defaultVisible,
};
},
watch: {
visible(val) {
this.sVisible = val;
},
},
methods: {
handleVisibleChange(visible: boolean) {
if (!hasProp(this, 'visible')) {
this.sVisible = this.isNoTitle() ? false : visible;
}
if (!this.isNoTitle()) {
this.$emit('update:visible', visible);
this.$emit('visibleChange', visible);
}
},
setup(props, { slots, emit, attrs, expose }) {
const { prefixCls, getTargetContainer } = useConfigInject('tooltip', props);
getPopupDomNode() {
return (this.$refs.tooltip as any).getPopupDomNode();
},
const visible = ref(false);
getPlacements() {
const { builtinPlacements, arrowPointAtCenter, autoAdjustOverflow } = this.$props;
const tooltip = ref();
onMounted(() => {
warning(
!('default-visible' in attrs) || !('defaultVisible' in attrs),
'Tooltip',
`'defaultVisible' is deprecated, please use 'v-model:visible'`,
);
});
watch(
() => props.visible,
val => {
visible.value = !!val;
},
{ immediate: true },
);
const isNoTitle = () => {
const title = getPropsSlot(slots, props, 'title');
return !title && title !== 0;
};
const handleVisibleChange = (val: boolean) => {
visible.value = isNoTitle() ? false : val;
if (!isNoTitle()) {
emit('update:visible', val);
emit('visibleChange', val);
}
};
const getPopupDomNode = () => {
return tooltip.value.getPopupDomNode();
};
expose({ getPopupDomNode, visible });
const tooltipPlacements = computed(() => {
const { builtinPlacements, arrowPointAtCenter, autoAdjustOverflow } = props;
return (
builtinPlacements ||
getPlacements({
arrowPointAtCenter,
verticalArrowShift: 8,
autoAdjustOverflow,
})
);
},
});
// Fix Tooltip won't hide at disabled button
// mouse events don't trigger at disabled button in Chrome
// https://github.com/react-component/tooltip/issues/18
getDisabledCompatibleChildren(ele: any) {
const getDisabledCompatibleChildren = (ele: any) => {
if (
((typeof ele.type === 'object' &&
(ele.type.__ANT_BUTTON === true ||
@ -128,27 +151,22 @@ export default defineComponent({
},
true,
);
return <span style={spanStyle}>{child}</span>;
return (
<span style={spanStyle} class={`${prefixCls}-disabled-compatible-wrapper`}>
{child}
</span>
);
}
return ele;
},
};
isNoTitle() {
const title = getComponent(this, 'title');
return !title && title !== 0;
},
const getOverlay = () => {
const title = getPropsSlot(slots, props, 'title');
return title ?? '';
};
getOverlay() {
const title = getComponent(this, 'title');
if (title === 0) {
return title;
}
return title || '';
},
//
onPopupAlign(domNode: HTMLElement, align: any) {
const placements = this.getPlacements();
const onPopupAlign = (domNode: HTMLElement, align: any) => {
const placements = tooltipPlacements.value;
//
const placement = Object.keys(placements).filter(
key =>
@ -175,67 +193,64 @@ export default defineComponent({
transformOrigin.left = `${-align.offset[0]}px`;
}
domNode.style.transformOrigin = `${transformOrigin.left} ${transformOrigin.top}`;
},
},
render() {
const { $props, $data, $attrs } = this;
const {
prefixCls: customizePrefixCls,
openClassName,
getPopupContainer,
color,
overlayClassName,
} = $props;
const { getPopupContainer: getContextPopupContainer } = this.configProvider;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('tooltip', customizePrefixCls);
let children = this.children || filterEmpty(getSlot(this));
children = children.length === 1 ? children[0] : children;
let sVisible = $data.sVisible;
// Hide tooltip when there is no title
if (!hasProp(this, 'visible') && this.isNoTitle()) {
sVisible = false;
}
if (!children) {
return null;
}
const child = this.getDisabledCompatibleChildren(
isValidElement(children) ? children : <span>{children}</span>,
);
const childCls = classNames({
[openClassName || `${prefixCls}-open`]: sVisible,
[child.props && child.props.class]: child.props && child.props.class,
});
const customOverlayClassName = classNames(overlayClassName, {
[`${prefixCls}-${color}`]: color && PresetColorRegex.test(color),
});
let formattedOverlayInnerStyle: CSSProperties;
let arrowContentStyle: CSSProperties;
if (color && !PresetColorRegex.test(color)) {
formattedOverlayInnerStyle = { backgroundColor: color };
arrowContentStyle = { backgroundColor: color };
}
const vcTooltipProps = {
...$attrs,
...$props,
prefixCls,
getTooltipContainer: getPopupContainer || getContextPopupContainer,
builtinPlacements: this.getPlacements(),
overlay: this.getOverlay(),
visible: sVisible,
ref: 'tooltip',
overlayClassName: customOverlayClassName,
overlayInnerStyle: formattedOverlayInnerStyle,
arrowContent: <span class={`${prefixCls}-arrow-content`} style={arrowContentStyle}></span>,
onVisibleChange: this.handleVisibleChange,
onPopupAlign: this.onPopupAlign,
};
return (
<VcTooltip {...vcTooltipProps}>
{sVisible ? cloneElement(child, { class: childCls }) : child}
</VcTooltip>
);
return () => {
const { openClassName, getPopupContainer, color, overlayClassName } = props;
let children = filterEmpty(slots.default?.()) ?? null;
children = children.length === 1 ? children[0] : children;
let tempVisible = visible.value;
// Hide tooltip when there is no title
if (props.visible === undefined && isNoTitle()) {
tempVisible = false;
}
if (!children) {
return null;
}
const child = getDisabledCompatibleChildren(
isValidElement(children) ? children : <span>{children}</span>,
);
const childCls = classNames({
[openClassName || `${prefixCls.value}-open`]: true,
[child.props && child.props.class]: child.props && child.props.class,
});
const customOverlayClassName = classNames(overlayClassName, {
[`${prefixCls.value}-${color}`]: color && PresetColorRegex.test(color),
});
let formattedOverlayInnerStyle: CSSProperties;
let arrowContentStyle: CSSProperties;
if (color && !PresetColorRegex.test(color)) {
formattedOverlayInnerStyle = { backgroundColor: color };
arrowContentStyle = { backgroundColor: color };
}
const vcTooltipProps = {
...attrs,
...props,
prefixCls: prefixCls.value,
getTooltipContainer: getPopupContainer || getTargetContainer.value,
builtinPlacements: tooltipPlacements.value,
overlay: getOverlay(),
visible: tempVisible,
ref: tooltip,
overlayClassName: customOverlayClassName,
overlayInnerStyle: formattedOverlayInnerStyle,
onVisibleChange: handleVisibleChange,
onPopupAlign,
};
return (
<VcTooltip
{...vcTooltipProps}
v-slots={{
arrowContent: () => (
<span class={`${prefixCls.value}-arrow-content`} style={arrowContentStyle}></span>
),
}}
>
{visible.value ? cloneElement(child, { class: childCls }) : child}
</VcTooltip>
);
};
},
});

View File

@ -5,7 +5,7 @@ import mountTest from '../../../tests/shared/mountTest';
describe('Tooltip', () => {
mountTest(Tooltip);
it('check `onVisibleChange` arguments', async () => {
fit('check `onVisibleChange` arguments', async () => {
const onVisibleChange = jest.fn();
const wrapper = mount(
{
@ -44,14 +44,14 @@ describe('Tooltip', () => {
});
await asyncExpect(() => {
expect(onVisibleChange).not.toHaveBeenCalled();
expect(wrapper.vm.$refs.tooltip.$refs.tooltip.visible).toBe(false);
expect(wrapper.vm.$refs.tooltip.visible).toBe(false);
});
await asyncExpect(() => {
div.dispatchEvent(new MouseEvent('mouseleave'));
});
await asyncExpect(() => {
expect(onVisibleChange).not.toHaveBeenCalled();
expect(wrapper.vm.$refs.tooltip.$refs.tooltip.visible).toBe(false);
expect(wrapper.vm.$refs.tooltip.visible).toBe(false);
});
await asyncExpect(() => {
// update `title` value.
@ -62,14 +62,14 @@ describe('Tooltip', () => {
});
await asyncExpect(() => {
expect(onVisibleChange).toHaveBeenLastCalledWith(true);
expect(wrapper.vm.$refs.tooltip.$refs.tooltip.visible).toBe(true);
expect(wrapper.vm.$refs.tooltip.visible).toBe(true);
}, 0);
await asyncExpect(() => {
wrapper.findAll('#hello')[0].element.dispatchEvent(new MouseEvent('mouseleave'));
});
await asyncExpect(() => {
expect(onVisibleChange).toHaveBeenLastCalledWith(false);
expect(wrapper.vm.$refs.tooltip.$refs.tooltip.visible).toBe(false);
expect(wrapper.vm.$refs.tooltip.visible).toBe(false);
});
await asyncExpect(() => {
// add `visible` props.
@ -80,16 +80,16 @@ describe('Tooltip', () => {
});
await asyncExpect(() => {
expect(onVisibleChange).toHaveBeenLastCalledWith(true);
lastCount = onVisibleChange.mock.calls.length;
expect(wrapper.vm.$refs.tooltip.$refs.tooltip.visible).toBe(false);
expect(wrapper.vm.$refs.tooltip.visible).toBe(true);
});
await asyncExpect(() => {
// always trigger onVisibleChange
wrapper.findAll('#hello')[0].element.dispatchEvent(new MouseEvent('mouseleave'));
lastCount = onVisibleChange.mock.calls.length;
});
await asyncExpect(() => {
expect(onVisibleChange.mock.calls.length).toBe(lastCount); // no change with lastCount
expect(wrapper.vm.$refs.tooltip.$refs.tooltip.visible).toBe(false);
expect(wrapper.vm.$refs.tooltip.visible).toBe(false);
});
});
});

View File

@ -1,27 +1,30 @@
import PropTypes from '../_util/vue-types';
import { tuple } from '../_util/type';
const triggerType = PropTypes.oneOf(tuple('hover', 'focus', 'click', 'contextmenu'));
export const triggerTypes = tuple('hover', 'focus', 'click', 'contextmenu');
export const placementTypes = tuple(
'top',
'left',
'right',
'bottom',
'topLeft',
'topRight',
'bottomLeft',
'bottomRight',
'leftTop',
'leftBottom',
'rightTop',
'rightBottom',
);
export default () => ({
trigger: PropTypes.oneOfType([triggerType, PropTypes.arrayOf(triggerType)]).def('hover'),
trigger: PropTypes.oneOfType([
PropTypes.oneOf(triggerTypes),
PropTypes.arrayOf(PropTypes.oneOf(triggerTypes)),
]).def('hover'),
visible: PropTypes.looseBool,
defaultVisible: PropTypes.looseBool,
placement: PropTypes.oneOf(
tuple(
'top',
'left',
'right',
'bottom',
'topLeft',
'topRight',
'bottomLeft',
'bottomRight',
'leftTop',
'leftBottom',
'rightTop',
'rightBottom',
),
).def('top'),
// defaultVisible: PropTypes.looseBool,
placement: PropTypes.oneOf(placementTypes).def('top'),
color: PropTypes.string,
transitionName: PropTypes.string.def('zoom-big-fast'),
overlayStyle: PropTypes.object.def(() => ({})),

View File

@ -1,6 +1,12 @@
import { withInstall } from '../_util/type';
import ToolTip from './Tooltip';
export { TooltipProps } from './Tooltip';
export type {
TooltipProps,
AdjustOverflow,
PlacementsConfig,
TooltipAlignConfig,
PlacementTypes,
} from './Tooltip';
export default withInstall(ToolTip);

View File

@ -1,4 +1,4 @@
import { placements as rcPlacements } from '../vc-tooltip/placements';
import { placements } from '../vc-tooltip/src/placements';
const autoAdjustOverflowEnabled = {
adjustX: 1,
@ -12,15 +12,20 @@ const autoAdjustOverflowDisabled = {
const targetOffset = [0, 0];
interface PlacementsConfig {
arrowPointAtCenter: boolean;
arrowWidth?: number;
verticalArrowShift?: number;
horizontalArrowShift?: number;
autoAdjustOverflow?: boolean | Object;
export interface AdjustOverflow {
adjustX?: 0 | 1;
adjustY?: 0 | 1;
}
export function getOverflowOptions(autoAdjustOverflow: boolean | Object) {
export interface PlacementsConfig {
arrowWidth?: number;
horizontalArrowShift?: number;
verticalArrowShift?: number;
arrowPointAtCenter?: boolean;
autoAdjustOverflow?: boolean | AdjustOverflow;
}
export function getOverflowOptions(autoAdjustOverflow?: boolean | AdjustOverflow) {
if (typeof autoAdjustOverflow === 'boolean') {
return autoAdjustOverflow ? autoAdjustOverflowEnabled : autoAdjustOverflowDisabled;
}
@ -34,8 +39,8 @@ export default function getPlacements(config: PlacementsConfig) {
const {
arrowWidth = 5,
horizontalArrowShift = 16,
verticalArrowShift = 12,
autoAdjustOverflow = true,
verticalArrowShift = 8,
autoAdjustOverflow,
} = config;
const placementMap = {
left: {
@ -95,9 +100,10 @@ export default function getPlacements(config: PlacementsConfig) {
targetOffset,
}
: {
...rcPlacements[key],
...placements[key],
overflow: getOverflowOptions(autoAdjustOverflow),
};
placementMap[key].ignoreShake = true;
});
return placementMap;

View File

@ -18,6 +18,7 @@
position: absolute;
z-index: @zindex-tooltip;
display: block;
width: max-content;
max-width: @tooltip-max-width;
visibility: visible;
@ -202,3 +203,5 @@
}
}
.generator-tooltip-preset-color();
@import './rtl';

View File

@ -0,0 +1,14 @@
@tooltip-prefix-cls: ~'@{ant-prefix}-tooltip';
// Base class
.@{tooltip-prefix-cls} {
&-rtl {
direction: rtl;
}
// Wrapper for the tooltip content
&-inner {
.@{tooltip-prefix-cls}-rtl & {
text-align: right;
}
}
}

View File

@ -0,0 +1,165 @@
import type { ExtractPropTypes } from 'vue';
import { defineComponent, computed, ref, watch } from 'vue';
import classNames from '../_util/classNames';
import ListItem from './ListItem';
import Pagination from '../pagination';
import PropTypes from '../_util/vue-types';
import type { TransferItem } from '.';
export const transferListBodyProps = {
prefixCls: PropTypes.string,
filteredRenderItems: PropTypes.array.def([]),
selectedKeys: PropTypes.array,
disabled: PropTypes.looseBool,
showRemove: PropTypes.looseBool,
pagination: PropTypes.any,
onItemSelect: PropTypes.func,
onScroll: PropTypes.func,
onItemRemove: PropTypes.func,
};
export type TransferListBodyProps = Partial<ExtractPropTypes<typeof transferListBodyProps>>;
function parsePagination(pagination) {
if (!pagination) {
return null;
}
const defaultPagination = {
pageSize: 10,
};
if (typeof pagination === 'object') {
return {
...defaultPagination,
...pagination,
};
}
return defaultPagination;
}
const ListBody = defineComponent({
name: 'ListBody',
inheritAttrs: false,
props: transferListBodyProps,
emits: ['itemSelect', 'itemRemove', 'scroll'],
setup(props, { emit, expose }) {
const current = ref(1);
const handleItemSelect = (item: TransferItem) => {
const { selectedKeys } = props;
const checked = selectedKeys.indexOf(item.key) >= 0;
emit('itemSelect', item.key, !checked);
};
const handleItemRemove = (item: TransferItem) => {
emit('itemRemove', item.key);
};
const handleScroll = (e: Event) => {
emit('scroll', e);
};
const mergedPagination = computed(() => parsePagination(props.pagination));
watch(
[mergedPagination, () => props.filteredRenderItems],
() => {
if (mergedPagination.value) {
// Calculate the page number
const maxPageCount = Math.ceil(
props.filteredRenderItems.length / mergedPagination.value.pageSize,
);
if (current.value > maxPageCount) {
current.value = maxPageCount;
}
}
},
{ immediate: true },
);
const items = computed(() => {
const { filteredRenderItems } = props;
let displayItems = filteredRenderItems;
if (mergedPagination.value) {
displayItems = filteredRenderItems.slice(
(current.value - 1) * mergedPagination.value.pageSize,
current.value * mergedPagination.value.pageSize,
);
}
return displayItems;
});
const onPageChange = (cur: number) => {
current.value = cur;
};
expose({ items });
return () => {
const {
prefixCls,
filteredRenderItems,
selectedKeys,
disabled: globalDisabled,
showRemove,
} = props;
let paginationNode = null;
if (mergedPagination.value) {
paginationNode = (
<Pagination
simple
size="small"
disabled={globalDisabled}
class={`${prefixCls}-pagination`}
total={filteredRenderItems.length}
pageSize={mergedPagination.value.pageSize}
current={current.value}
onChange={onPageChange}
/>
);
}
const itemsList = items.value.map(({ renderedEl, renderedText, item }: any) => {
const { disabled } = item;
const checked = selectedKeys.indexOf(item.key) >= 0;
return (
<ListItem
disabled={globalDisabled || disabled}
key={item.key}
item={item}
renderedText={renderedText}
renderedEl={renderedEl}
checked={checked}
prefixCls={prefixCls}
onClick={handleItemSelect}
onRemove={handleItemRemove}
showRemove={showRemove}
/>
);
});
return (
<>
<ul
class={classNames(`${prefixCls}-content`, {
[`${prefixCls}-content-show-remove`]: showRemove,
})}
onScroll={handleScroll}
>
{itemsList}
</ul>
{paginationNode}
</>
);
};
},
});
export default ListBody;

View File

@ -1,66 +1,93 @@
import PropTypes, { withUndefined } from '../_util/vue-types';
import PropTypes from '../_util/vue-types';
import classNames from '../_util/classNames';
import Lazyload from '../vc-lazy-load';
import type { TransferLocale } from '.';
import DeleteOutlined from '@ant-design/icons-vue/DeleteOutlined';
import defaultLocale from '../locale/default';
import Checkbox from '../checkbox';
import TransButton from '../_util/transButton';
import LocaleReceiver from '../locale-provider/LocaleReceiver';
import type { ExtractPropTypes } from 'vue';
import { defineComponent } from 'vue';
function noop() {}
export const transferListItemProps = {
renderedText: PropTypes.any,
renderedEl: PropTypes.any,
item: PropTypes.any,
checked: PropTypes.looseBool,
prefixCls: PropTypes.string,
disabled: PropTypes.looseBool,
showRemove: PropTypes.looseBool,
onClick: PropTypes.func,
onRemove: PropTypes.func,
};
export type TransferListItemProps = Partial<ExtractPropTypes<typeof transferListItemProps>>;
export default defineComponent({
name: 'ListItem',
inheritAttrs: false,
props: {
renderedText: PropTypes.any,
renderedEl: PropTypes.any,
item: PropTypes.any,
lazy: withUndefined(PropTypes.oneOfType([PropTypes.looseBool, PropTypes.object])),
checked: PropTypes.looseBool,
prefixCls: PropTypes.string,
disabled: PropTypes.looseBool,
onClick: PropTypes.func,
},
render() {
const { renderedText, renderedEl, item, lazy, checked, disabled, prefixCls } = this.$props;
props: transferListItemProps,
emits: ['click', 'remove'],
setup(props, { emit }) {
return () => {
const { renderedText, renderedEl, item, checked, disabled, prefixCls, showRemove } = props;
const className = classNames({
[`${prefixCls}-content-item`]: true,
[`${prefixCls}-content-item-disabled`]: disabled || item.disabled,
});
const className = classNames({
[`${prefixCls}-content-item`]: true,
[`${prefixCls}-content-item-disabled`]: disabled || item.disabled,
});
let title: string;
if (typeof renderedText === 'string' || typeof renderedText === 'number') {
title = String(renderedText);
}
let title;
if (typeof renderedText === 'string' || typeof renderedText === 'number') {
title = String(renderedText);
}
return (
<LocaleReceiver componentName="Transfer" defaultLocale={defaultLocale.Transfer}>
{(transferLocale: TransferLocale) => {
const labelNode = <span class={`${prefixCls}-content-item-text`}>{renderedEl}</span>;
if (showRemove) {
return (
<li class={className} title={title}>
{labelNode}
<TransButton
disabled={disabled || item.disabled}
class={`${prefixCls}-content-item-remove`}
aria-label={transferLocale.remove}
onClick={() => {
emit('remove', item);
}}
>
<DeleteOutlined />
</TransButton>
</li>
);
}
const listItem = (
<li
class={className}
title={title}
onClick={
disabled || item.disabled
? noop
: () => {
this.$emit('click', item);
}
}
>
<Checkbox checked={checked} disabled={disabled || item.disabled} />
<span class={`${prefixCls}-content-item-text`}>{renderedEl}</span>
</li>
);
let children = null;
if (lazy) {
const lazyProps = {
height: 32,
offset: 500,
throttle: 0,
debounce: false,
...(lazy as any),
};
children = <Lazyload {...lazyProps}>{listItem}</Lazyload>;
} else {
children = listItem;
}
return children;
return (
<li
class={className}
title={title}
onClick={
disabled || item.disabled
? noop
: () => {
emit('click', item);
}
}
>
<Checkbox
class={`${prefixCls}-checkbox`}
checked={checked}
disabled={disabled || item.disabled}
/>
{labelNode}
</li>
);
}}
</LocaleReceiver>
);
};
},
});

View File

@ -3,34 +3,40 @@
exports[`Transfer should render correctly 1`] = `
<div class="ant-transfer">
<div class="ant-transfer-list">
<div class="ant-transfer-list-header"><label class="ant-checkbox-wrapper ant-checkbox-wrapper-checked"><span class="ant-checkbox ant-checkbox-checked"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<div class="ant-transfer-list-header"><label class="ant-checkbox-wrapper ant-checkbox-wrapper-checked ant-transfer-list-checkbox"><span class="ant-checkbox ant-checkbox-checked"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label><span class="ant-transfer-list-header-selected"><span>1/2 items</span><span class="ant-transfer-list-header-title"></span></span></div>
</label>
<!----><span tabindex="-1" role="img" aria-label="down" class="anticon anticon-down ant-dropdown-trigger ant-transfer-list-header-dropdown"><svg focusable="false" class="" data-icon="down" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"></path></svg></span><span class="ant-transfer-list-header-selected"><span>1/2 items</span><span class="ant-transfer-list-header-title"></span></span>
</div>
<div class="ant-transfer-list-body">
<!---->
<ul class="ant-transfer-list-content">
<li class="ant-transfer-list-content-item"><label class="ant-checkbox-wrapper ant-checkbox-wrapper-checked"><span class="ant-checkbox ant-checkbox-checked"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<li class="ant-transfer-list-content-item"><label class="ant-checkbox-wrapper ant-checkbox-wrapper-checked ant-transfer-list-checkbox"><span class="ant-checkbox ant-checkbox-checked"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label><span class="ant-transfer-list-content-item-text"><!----></span></li>
<li class="ant-transfer-list-content-item ant-transfer-list-content-item-disabled"><label class="ant-checkbox-wrapper ant-checkbox-wrapper-disabled"><span class="ant-checkbox ant-checkbox-disabled"><input type="checkbox" disabled="" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<li class="ant-transfer-list-content-item ant-transfer-list-content-item-disabled"><label class="ant-checkbox-wrapper ant-checkbox-wrapper-disabled ant-transfer-list-checkbox"><span class="ant-checkbox ant-checkbox-disabled"><input type="checkbox" disabled="" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label><span class="ant-transfer-list-content-item-text"><!----></span></li>
</ul>
<!---->
</div>
<!---->
</div>
<div class="ant-transfer-operation"><button class="ant-btn ant-btn-primary ant-btn-sm ant-btn-icon-only" type="button"><span role="img" aria-label="right" class="anticon anticon-right"><svg focusable="false" class="" data-icon="right" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"></path></svg></span></button><button disabled="" class="ant-btn ant-btn-primary ant-btn-sm ant-btn-icon-only" type="button"><span role="img" aria-label="left" class="anticon anticon-left"><svg focusable="false" class="" data-icon="left" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"></path></svg></span></button></div>
<div class="ant-transfer-list">
<div class="ant-transfer-list-header"><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<div class="ant-transfer-list-header"><label class="ant-checkbox-wrapper ant-transfer-list-checkbox"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label><span class="ant-transfer-list-header-selected"><span>1 item</span><span class="ant-transfer-list-header-title"></span></span></div>
</label>
<!----><span tabindex="-1" role="img" aria-label="down" class="anticon anticon-down ant-dropdown-trigger ant-transfer-list-header-dropdown"><svg focusable="false" class="" data-icon="down" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"></path></svg></span><span class="ant-transfer-list-header-selected"><span>1 item</span><span class="ant-transfer-list-header-title"></span></span>
</div>
<div class="ant-transfer-list-body">
<!---->
<ul class="ant-transfer-list-content">
<li class="ant-transfer-list-content-item"><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<li class="ant-transfer-list-content-item"><label class="ant-checkbox-wrapper ant-transfer-list-checkbox"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label><span class="ant-transfer-list-content-item-text"><!----></span></li>
</ul>
<!---->
</div>
<!---->
</div>
@ -40,34 +46,40 @@ exports[`Transfer should render correctly 1`] = `
exports[`Transfer should show sorted targetkey 1`] = `
<div class="ant-transfer">
<div class="ant-transfer-list">
<div class="ant-transfer-list-header"><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<div class="ant-transfer-list-header"><label class="ant-checkbox-wrapper ant-transfer-list-checkbox"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label><span class="ant-transfer-list-header-selected"><span>1 item</span><span class="ant-transfer-list-header-title"></span></span></div>
</label>
<!----><span tabindex="-1" role="img" aria-label="down" class="anticon anticon-down ant-dropdown-trigger ant-transfer-list-header-dropdown"><svg focusable="false" class="" data-icon="down" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"></path></svg></span><span class="ant-transfer-list-header-selected"><span>1 item</span><span class="ant-transfer-list-header-title"></span></span>
</div>
<div class="ant-transfer-list-body">
<!---->
<ul class="ant-transfer-list-content">
<li class="ant-transfer-list-content-item" title="a"><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<li class="ant-transfer-list-content-item" title="a"><label class="ant-checkbox-wrapper ant-transfer-list-checkbox"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label><span class="ant-transfer-list-content-item-text">a</span></li>
</ul>
<!---->
</div>
<!---->
</div>
<div class="ant-transfer-operation"><button disabled="" class="ant-btn ant-btn-primary ant-btn-sm ant-btn-icon-only" type="button"><span role="img" aria-label="right" class="anticon anticon-right"><svg focusable="false" class="" data-icon="right" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"></path></svg></span></button><button disabled="" class="ant-btn ant-btn-primary ant-btn-sm ant-btn-icon-only" type="button"><span role="img" aria-label="left" class="anticon anticon-left"><svg focusable="false" class="" data-icon="left" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"></path></svg></span></button></div>
<div class="ant-transfer-list">
<div class="ant-transfer-list-header"><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<div class="ant-transfer-list-header"><label class="ant-checkbox-wrapper ant-transfer-list-checkbox"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label><span class="ant-transfer-list-header-selected"><span>2 items</span><span class="ant-transfer-list-header-title"></span></span></div>
</label>
<!----><span tabindex="-1" role="img" aria-label="down" class="anticon anticon-down ant-dropdown-trigger ant-transfer-list-header-dropdown"><svg focusable="false" class="" data-icon="down" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"></path></svg></span><span class="ant-transfer-list-header-selected"><span>2 items</span><span class="ant-transfer-list-header-title"></span></span>
</div>
<div class="ant-transfer-list-body">
<!---->
<ul class="ant-transfer-list-content">
<li class="ant-transfer-list-content-item" title="c"><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<li class="ant-transfer-list-content-item" title="c"><label class="ant-checkbox-wrapper ant-transfer-list-checkbox"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label><span class="ant-transfer-list-content-item-text">c</span></li>
<li class="ant-transfer-list-content-item" title="b"><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<li class="ant-transfer-list-content-item" title="b"><label class="ant-checkbox-wrapper ant-transfer-list-checkbox"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label><span class="ant-transfer-list-content-item-text">b</span></li>
</ul>
<!---->
</div>
<!---->
</div>

View File

@ -2,22 +2,25 @@
exports[`List should render correctly 1`] = `
<div class="ant-transfer-list">
<div class="ant-transfer-list-header"><label class="ant-checkbox-wrapper"><span class="ant-checkbox ant-checkbox-indeterminate"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<div class="ant-transfer-list-header"><label class="ant-checkbox-wrapper ant-transfer-list-checkbox"><span class="ant-checkbox ant-checkbox-indeterminate"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label><span class="ant-transfer-list-header-selected"><span>1/3 <!----></span><span class="ant-transfer-list-header-title"></span></span></div>
</label>
<!----><span tabindex="-1" role="img" aria-label="down" class="anticon anticon-down ant-dropdown-trigger ant-transfer-list-header-dropdown"><svg focusable="false" class="" data-icon="down" width="1em" height="1em" fill="currentColor" aria-hidden="true" viewBox="64 64 896 896"><path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"></path></svg></span><span class="ant-transfer-list-header-selected"><span>1/3 <!----></span><span class="ant-transfer-list-header-title"><!----></span></span>
</div>
<div class="ant-transfer-list-body">
<!---->
<ul class="ant-transfer-list-content">
<li class="ant-transfer-list-content-item"><label class="ant-checkbox-wrapper ant-checkbox-wrapper-checked"><span class="ant-checkbox ant-checkbox-checked"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<li class="ant-transfer-list-content-item"><label class="ant-checkbox-wrapper ant-checkbox-wrapper-checked ant-transfer-list-checkbox"><span class="ant-checkbox ant-checkbox-checked"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label><span class="ant-transfer-list-content-item-text"><!----></span></li>
<li class="ant-transfer-list-content-item"><label class="ant-checkbox-wrapper"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<li class="ant-transfer-list-content-item"><label class="ant-checkbox-wrapper ant-transfer-list-checkbox"><span class="ant-checkbox"><input type="checkbox" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label><span class="ant-transfer-list-content-item-text"><!----></span></li>
<li class="ant-transfer-list-content-item ant-transfer-list-content-item-disabled"><label class="ant-checkbox-wrapper ant-checkbox-wrapper-disabled"><span class="ant-checkbox ant-checkbox-disabled"><input type="checkbox" disabled="" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<li class="ant-transfer-list-content-item ant-transfer-list-content-item-disabled"><label class="ant-checkbox-wrapper ant-checkbox-wrapper-disabled ant-transfer-list-checkbox"><span class="ant-checkbox ant-checkbox-disabled"><input type="checkbox" disabled="" class="ant-checkbox-input"><span class="ant-checkbox-inner"></span></span>
<!---->
</label><span class="ant-transfer-list-content-item-text"><!----></span></li>
</ul>
<!---->
</div>
<!---->
</div>

View File

@ -90,19 +90,28 @@ const searchTransferProps = {
describe('Transfer', () => {
mountTest(Transfer);
it('should render correctly', () => {
const props = {
props: listCommonProps,
};
const wrapper = mount(Transfer, props);
const wrapper = mount({
setup() {
return () => <Transfer {...{ ...listCommonProps }} />;
},
});
expect(wrapper.html()).toMatchSnapshot();
});
it('should move selected keys to corresponding list', done => {
const handleChange = jest.fn();
const wrapper = mount(Transfer, {
props: { ...listCommonProps, onChange: handleChange },
sync: false,
});
const wrapper = mount(
{
setup() {
return () => <Transfer {...{ ...listCommonProps, onChange: handleChange }} />;
},
},
{
sync: false,
},
);
Vue.nextTick(() => {
wrapper.findAll('.ant-btn')[0].trigger('click'); // move selected keys to right list
expect(handleChange).toHaveBeenCalledWith(['a', 'b'], 'right', ['a']);
@ -111,10 +120,16 @@ describe('Transfer', () => {
});
it('should move selected keys expect disabled to corresponding list', done => {
const handleChange = jest.fn();
const wrapper = mount(Transfer, {
props: { ...listDisabledProps, onChange: handleChange },
sync: false,
});
const wrapper = mount(
{
setup() {
return () => <Transfer {...{ ...listDisabledProps, onChange: handleChange }} />;
},
},
{
sync: false,
},
);
Vue.nextTick(() => {
wrapper.findAll('.ant-btn')[0].trigger('click');
expect(handleChange).toHaveBeenCalledWith(['b'], 'right', ['b']);
@ -124,10 +139,18 @@ describe('Transfer', () => {
it('should uncheck checkbox when click on checked item', async () => {
const handleSelectChange = jest.fn();
const wrapper = mount(Transfer, {
props: { ...listCommonProps, onSelectChange: handleSelectChange },
sync: false,
});
const wrapper = mount(
{
setup() {
return () => <Transfer {...{ ...listCommonProps, onSelectChange: handleSelectChange }} />;
},
},
{
sync: false,
},
);
await sleep();
wrapper.findAll('.ant-transfer-list-content-item')[0].trigger('click');
expect(handleSelectChange).toHaveBeenLastCalledWith([], []);
@ -135,10 +158,18 @@ describe('Transfer', () => {
it('should check checkbox when click on unchecked item', async () => {
const handleSelectChange = jest.fn();
const wrapper = mount(Transfer, {
props: { ...listCommonProps, onSelectChange: handleSelectChange },
sync: false,
});
const wrapper = mount(
{
setup() {
return () => <Transfer {...{ ...listCommonProps, onSelectChange: handleSelectChange }} />;
},
},
{
sync: false,
},
);
await sleep();
wrapper.findAll('.ant-transfer-list-content-item')[2].trigger('click');
await sleep();
@ -147,10 +178,18 @@ describe('Transfer', () => {
it('should not check checkbox when click on disabled item', async () => {
const handleSelectChange = jest.fn();
const wrapper = mount(Transfer, {
props: { ...listCommonProps, onSelectChange: handleSelectChange },
sync: false,
});
const wrapper = mount(
{
setup() {
return () => <Transfer {...{ ...listCommonProps, onSelectChange: handleSelectChange }} />;
},
},
{
sync: false,
},
);
await sleep();
wrapper.findAll('.ant-transfer-list-content-item')[1].trigger('click');
expect(handleSelectChange).not.toHaveBeenCalled();
@ -200,14 +239,26 @@ describe('Transfer', () => {
it('should call `filterOption` when use input in search box', done => {
const filterOption = (inputValue, option) => inputValue === option.title;
const wrapper = mount(Transfer, {
props: {
...listCommonProps,
showSearch: true,
filterOption,
const wrapper = mount(
{
setup() {
return () => (
<Transfer
{...{
...listCommonProps,
showSearch: true,
filterOption,
}}
/>
);
},
},
sync: false,
});
{
sync: false,
},
);
Vue.nextTick(() => {
const input = wrapper.findAll('.ant-transfer-list-body-search-wrapper input')[0];
input.element.value = 'a';
@ -227,15 +278,26 @@ describe('Transfer', () => {
it('should display the correct count of items when filter by input', done => {
const filterOption = (inputValue, option) => option.description.indexOf(inputValue) > -1;
const renderFunc = item => item.title;
const wrapper = mount(Transfer, {
props: {
...searchTransferProps,
showSearch: true,
filterOption,
render: renderFunc,
const wrapper = mount(
{
setup() {
return () => (
<Transfer
{...{
...searchTransferProps,
showSearch: true,
filterOption,
render: renderFunc,
}}
/>
);
},
},
sync: false,
});
{
sync: false,
},
);
Vue.nextTick(() => {
const input = wrapper.findAll('.ant-transfer-list-body-search-wrapper input')[0];
input.element.value = 'content2';
@ -247,7 +309,7 @@ describe('Transfer', () => {
.findAll('.ant-transfer-list-header-selected > span')[0]
.text()
.trim(),
).toEqual('1 items');
).toEqual('1 item');
done();
});
});
@ -395,14 +457,11 @@ describe('Transfer', () => {
targetKeys: ['c', 'b'],
lazy: false,
};
const props = {
props: {
...sortedTargetKeyProps,
render: item => item.title,
const wrapper = mount({
setup() {
return () => <Transfer {...sortedTargetKeyProps} render={item => item.title} />;
},
};
const wrapper = mount(Transfer, props);
});
expect(wrapper.html()).toMatchSnapshot();
});
it('should add custom styles when their props are provided', async () => {

View File

@ -1,175 +1,154 @@
import { defineComponent, inject } from 'vue';
import type { CSSProperties, ExtractPropTypes, PropType } from 'vue';
import { watchEffect } from 'vue';
import { defineComponent, ref, watch } from 'vue';
import PropTypes from '../_util/vue-types';
import { hasProp, getOptionProps, getComponent } from '../_util/props-util';
import initDefaultProps from '../_util/props-util/initDefaultProps';
import BaseMixin from '../_util/BaseMixin';
import { getPropsSlot } from '../_util/props-util';
import classNames from '../_util/classNames';
import List from './list';
import Operation from './operation';
import LocaleReceiver from '../locale-provider/LocaleReceiver';
import defaultLocale from '../locale-provider/default';
import type { RenderEmptyHandler } from '../config-provider';
import { defaultConfigProvider } from '../config-provider';
import type { VueNode } from '../_util/type';
import { withInstall } from '../_util/type';
import useConfigInject from '../_util/hooks/useConfigInject';
import type { TransferListBodyProps } from './ListBody';
import type { PaginationType } from './interface';
export type { TransferListProps } from './list';
export type { TransferOperationProps } from './operation';
export type { TransferSearchProps } from './search';
export type TransferDirection = 'left' | 'right';
export const TransferItem = {
key: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
description: PropTypes.string,
disabled: PropTypes.looseBool,
};
export interface RenderResultObject {
label: VueNode;
value: string;
}
export const TransferProps = {
prefixCls: PropTypes.string,
dataSource: PropTypes.arrayOf(PropTypes.shape(TransferItem).loose),
disabled: PropTypes.looseBool,
targetKeys: PropTypes.arrayOf(PropTypes.string),
selectedKeys: PropTypes.arrayOf(PropTypes.string),
render: PropTypes.func,
listStyle: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
operationStyle: PropTypes.object,
titles: PropTypes.arrayOf(PropTypes.string),
operations: PropTypes.arrayOf(PropTypes.string),
showSearch: PropTypes.looseBool,
filterOption: PropTypes.func,
searchPlaceholder: PropTypes.string,
export type RenderResult = VueNode | RenderResultObject | string | null;
export interface TransferItem {
key?: string;
title?: string;
description?: string;
disabled?: boolean;
[name: string]: any;
}
export type KeyWise<T> = T & { key: string };
export type KeyWiseTransferItem = KeyWise<TransferItem>;
type TransferRender<RecordType> = (item: RecordType) => RenderResult;
export interface ListStyle {
direction: TransferDirection;
}
export type SelectAllLabel =
| VueNode
| ((info: { selectedCount: number; totalCount: number }) => VueNode);
export interface TransferLocale {
titles: VueNode[];
notFoundContent?: VueNode;
searchPlaceholder: string;
itemUnit: string;
itemsUnit: string;
remove: string;
selectAll: string;
selectCurrent: string;
selectInvert: string;
removeAll: string;
removeCurrent: string;
}
export const transferProps = {
prefixCls: String,
dataSource: { type: Array as PropType<TransferItem[]>, default: [] },
disabled: { type: Boolean, default: undefined },
targetKeys: { type: Array as PropType<string[]>, default: undefined },
selectedKeys: { type: Array as PropType<string[]>, default: undefined },
render: { type: Function as PropType<TransferRender<TransferItem>> },
listStyle: {
type: [Function, Object] as PropType<((style: ListStyle) => CSSProperties) | CSSProperties>,
default: () => ({}),
},
operationStyle: PropTypes.style,
titles: { type: Array as PropType<string[]> },
operations: { type: Array as PropType<string[]> },
showSearch: { type: Boolean, default: false },
filterOption: { type: Function as PropType<(inputValue: string, item: TransferItem) => boolean> },
searchPlaceholder: String,
notFoundContent: PropTypes.any,
locale: PropTypes.object,
rowKey: PropTypes.func,
lazy: PropTypes.oneOfType([PropTypes.object, PropTypes.looseBool]),
showSelectAll: PropTypes.looseBool,
children: PropTypes.any,
locale: { type: Object as PropType<Partial<TransferLocale>>, default: () => ({}) },
rowKey: { type: Function as PropType<(record: TransferItem) => string> },
showSelectAll: { type: Boolean, default: undefined },
selectAllLabels: { type: Array as PropType<SelectAllLabel[]> },
children: { type: Function as PropType<(props: TransferListBodyProps) => VueNode> },
oneWay: { type: Boolean, default: undefined },
pagination: { type: [Object, Boolean] as PropType<PaginationType>, default: undefined },
onChange: PropTypes.func,
onSelectChange: PropTypes.func,
onSearchChange: PropTypes.func,
onSearch: PropTypes.func,
onScroll: PropTypes.func,
['onUpdate:targetKeys']: PropTypes.func,
};
export interface TransferLocale {
titles: string[];
notFoundContent: string;
searchPlaceholder: string;
itemUnit: string;
itemsUnit: string;
}
export type TransferProps = Partial<ExtractPropTypes<typeof transferProps>>;
const Transfer = defineComponent({
name: 'ATransfer',
mixins: [BaseMixin],
inheritAttrs: false,
props: initDefaultProps(TransferProps, {
dataSource: [],
locale: {},
showSearch: false,
listStyle: () => {},
}),
setup() {
return {
separatedDataSource: null,
configProvider: inject('configProvider', defaultConfigProvider),
};
},
data() {
// vue slot便notFoundContentsearchPlaceholder
// warning(
// !(getComponent(this, 'notFoundContent') || hasProp(this, 'searchPlaceholder')),
// 'Transfer[notFoundContent] and Transfer[searchPlaceholder] will be removed, ' +
// 'please use Transfer[locale] instead.',
// )
const { selectedKeys = [], targetKeys = [] } = this;
return {
leftFilter: '',
rightFilter: '',
sourceSelectedKeys: selectedKeys.filter(key => targetKeys.indexOf(key) === -1),
targetSelectedKeys: selectedKeys.filter(key => targetKeys.indexOf(key) > -1),
};
},
watch: {
targetKeys() {
this.updateState();
if (this.selectedKeys) {
const targetKeys = this.targetKeys || [];
this.setState({
sourceSelectedKeys: this.selectedKeys.filter(key => !targetKeys.includes(key)),
targetSelectedKeys: this.selectedKeys.filter(key => targetKeys.includes(key)),
});
}
},
dataSource() {
this.updateState();
},
selectedKeys() {
if (this.selectedKeys) {
const targetKeys = this.targetKeys || [];
this.setState({
sourceSelectedKeys: this.selectedKeys.filter(key => !targetKeys.includes(key)),
targetSelectedKeys: this.selectedKeys.filter(key => targetKeys.includes(key)),
});
}
},
},
mounted() {
// this.currentProps = { ...this.$props }
},
methods: {
getSelectedKeysName(direction) {
return direction === 'left' ? 'sourceSelectedKeys' : 'targetSelectedKeys';
},
props: transferProps,
slots: [
'leftTitle',
'rightTitle',
'children',
'render',
'notFoundContent',
'leftSelectAllLabel',
'rightSelectAllLabel',
'footer',
],
emits: ['update:targetKeys', 'change', 'search', 'scroll', 'selectChange', 'searchChange'],
setup(props, { emit, attrs, slots, expose }) {
const { configProvider, prefixCls, direction } = useConfigInject('transfer', props);
const sourceSelectedKeys = ref([]);
const targetSelectedKeys = ref([]);
getTitles(transferLocale: TransferLocale) {
if (this.titles) {
return this.titles;
}
return transferLocale.titles || ['', ''];
},
watch(
() => props.selectedKeys,
() => {
sourceSelectedKeys.value =
props.selectedKeys?.filter(key => props.targetKeys.indexOf(key) === -1) || [];
targetSelectedKeys.value =
props.selectedKeys?.filter(key => props.targetKeys.indexOf(key) > -1) || [];
},
{ immediate: true },
);
getLocale(transferLocale: TransferLocale, renderEmpty: RenderEmptyHandler) {
const getLocale = (transferLocale: TransferLocale, renderEmpty: RenderEmptyHandler) => {
// Keep old locale props still working.
const oldLocale: { notFoundContent?: any; searchPlaceholder?: string } = {
notFoundContent: renderEmpty('Transfer'),
};
const notFoundContent = getComponent(this, 'notFoundContent');
const notFoundContent = getPropsSlot(slots, props, 'notFoundContent');
if (notFoundContent) {
oldLocale.notFoundContent = notFoundContent;
}
if (hasProp(this, 'searchPlaceholder')) {
oldLocale.searchPlaceholder = this.$props.searchPlaceholder;
if (props.searchPlaceholder !== undefined) {
oldLocale.searchPlaceholder = props.searchPlaceholder;
}
return { ...transferLocale, ...oldLocale, ...this.$props.locale };
},
updateState() {
const { sourceSelectedKeys, targetSelectedKeys } = this;
this.separatedDataSource = null;
if (!this.selectedKeys) {
// clear key nolonger existed
// clear checkedKeys according to targetKeys
const { dataSource, targetKeys = [] } = this;
return { ...transferLocale, ...oldLocale, ...props.locale };
};
const newSourceSelectedKeys = [];
const newTargetSelectedKeys = [];
dataSource.forEach(({ key }) => {
if (sourceSelectedKeys.includes(key) && !targetKeys.includes(key)) {
newSourceSelectedKeys.push(key);
}
if (targetSelectedKeys.includes(key) && targetKeys.includes(key)) {
newTargetSelectedKeys.push(key);
}
});
this.setState({
sourceSelectedKeys: newSourceSelectedKeys,
targetSelectedKeys: newTargetSelectedKeys,
});
}
},
moveTo(direction: TransferDirection) {
const { targetKeys = [], dataSource = [] } = this.$props;
const { sourceSelectedKeys, targetSelectedKeys } = this;
const moveKeys = direction === 'right' ? sourceSelectedKeys : targetSelectedKeys;
const moveTo = (direction: TransferDirection) => {
const { targetKeys = [], dataSource = [] } = props;
const moveKeys = direction === 'right' ? sourceSelectedKeys.value : targetSelectedKeys.value;
// filter the disabled options
const newMoveKeys = moveKeys.filter(
key => !dataSource.some(data => !!(key === data.key && data.disabled)),
@ -182,101 +161,74 @@ const Transfer = defineComponent({
// empty checked keys
const oppositeDirection = direction === 'right' ? 'left' : 'right';
this.setState({
[this.getSelectedKeysName(oppositeDirection)]: [],
});
this.handleSelectChange(oppositeDirection, []);
direction === 'right' ? (sourceSelectedKeys.value = []) : (targetSelectedKeys.value = []);
emit('update:targetKeys', newTargetKeys);
handleSelectChange(oppositeDirection, []);
emit('change', newTargetKeys, direction, newMoveKeys);
};
this.$emit('change', newTargetKeys, direction, newMoveKeys);
},
moveToLeft() {
this.moveTo('left');
},
moveToRight() {
this.moveTo('right');
},
const moveToLeft = () => {
moveTo('left');
};
const moveToRight = () => {
moveTo('right');
};
onItemSelectAll(direction: TransferDirection, selectedKeys: string[], checkAll: boolean) {
const originalSelectedKeys = this.$data[this.getSelectedKeysName(direction)] || [];
const onItemSelectAll = (direction: TransferDirection, selectedKeys: string[]) => {
handleSelectChange(direction, selectedKeys);
};
let mergedCheckedKeys = [];
if (checkAll) {
// Merge current keys with origin key
mergedCheckedKeys = Array.from(new Set([...originalSelectedKeys, ...selectedKeys]));
const onLeftItemSelectAll = (selectedKeys: string[]) => {
return onItemSelectAll('left', selectedKeys);
};
const onRightItemSelectAll = (selectedKeys: string[]) => {
return onItemSelectAll('right', selectedKeys);
};
const handleSelectChange = (direction: TransferDirection, holder: string[]) => {
if (direction === 'left') {
if (!props.selectedKeys) {
sourceSelectedKeys.value = holder;
}
emit('selectChange', holder, targetSelectedKeys.value);
} else {
// Remove current keys from origin keys
mergedCheckedKeys = originalSelectedKeys.filter(key => selectedKeys.indexOf(key) === -1);
if (!props.selectedKeys) {
targetSelectedKeys.value = holder;
}
emit('selectChange', sourceSelectedKeys.value, holder);
}
};
this.handleSelectChange(direction, mergedCheckedKeys);
if (!this.$props.selectedKeys) {
this.setState({
[this.getSelectedKeysName(direction)]: mergedCheckedKeys,
});
}
},
handleSelectAll(direction, filteredDataSource, checkAll) {
this.onItemSelectAll(
direction,
filteredDataSource.map(({ key }) => key),
!checkAll,
);
},
// [Legacy] Old prop `body` pass origin check as arg. It's confusing.
// TODO: Remove this in next version.
handleLeftSelectAll(filteredDataSource, checkAll) {
return this.handleSelectAll('left', filteredDataSource, !checkAll);
},
handleRightSelectAll(filteredDataSource, checkAll) {
return this.handleSelectAll('right', filteredDataSource, !checkAll);
},
onLeftItemSelectAll(selectedKeys, checkAll) {
return this.onItemSelectAll('left', selectedKeys, checkAll);
},
onRightItemSelectAll(selectedKeys, checkAll) {
return this.onItemSelectAll('right', selectedKeys, checkAll);
},
handleFilter(direction, e) {
const handleFilter = (direction: TransferDirection, e) => {
const value = e.target.value;
// if (getListeners(this).searchChange) {
// warning(
// false,
// 'Transfer',
// '`searchChange` in Transfer is deprecated. Please use `search` instead.',
// );
// this.$emit('searchChange', direction, e);
// }
this.$emit('search', direction, value);
},
emit('search', direction, value);
};
handleLeftFilter(e) {
this.handleFilter('left', e);
},
handleRightFilter(e) {
this.handleFilter('right', e);
},
const handleLeftFilter = (e: Event) => {
handleFilter('left', e);
};
const handleRightFilter = (e: Event) => {
handleFilter('right', e);
};
handleClear(direction) {
this.$emit('search', direction, '');
},
const handleClear = (direction: TransferDirection) => {
emit('search', direction, '');
};
handleLeftClear() {
this.handleClear('left');
},
handleRightClear() {
this.handleClear('right');
},
const handleLeftClear = () => {
handleClear('left');
};
onItemSelect(direction, selectedKey, checked) {
const { sourceSelectedKeys, targetSelectedKeys } = this;
const holder = direction === 'left' ? [...sourceSelectedKeys] : [...targetSelectedKeys];
const handleRightClear = () => {
handleClear('right');
};
const onItemSelect = (direction: TransferDirection, selectedKey: string, checked: boolean) => {
const holder =
direction === 'left' ? [...sourceSelectedKeys.value] : [...targetSelectedKeys.value];
const index = holder.indexOf(selectedKey);
if (index > -1) {
holder.splice(index, 1);
@ -284,67 +236,50 @@ const Transfer = defineComponent({
if (checked) {
holder.push(selectedKey);
}
this.handleSelectChange(direction, holder);
handleSelectChange(direction, holder);
};
if (!this.selectedKeys) {
this.setState({
[this.getSelectedKeysName(direction)]: holder,
});
}
},
const onLeftItemSelect = (selectedKey: string, checked: boolean) => {
return onItemSelect('left', selectedKey, checked);
};
const onRightItemSelect = (selectedKey: string, checked: boolean) => {
return onItemSelect('right', selectedKey, checked);
};
const onRightItemRemove = (targetedKeys: string[]) => {
const { targetKeys = [] } = props;
const newTargetKeys = targetKeys.filter(key => !targetedKeys.includes(key));
emit('update:targetKeys', newTargetKeys);
emit('change', newTargetKeys, 'left', [...targetedKeys]);
};
// handleSelect(direction, selectedItem, checked) {
// warning(false, 'Transfer', '`handleSelect` will be removed, please use `onSelect` instead.');
// this.onItemSelect(direction, selectedItem.key, checked);
// },
const handleScroll = (direction: TransferDirection, e: Event) => {
emit('scroll', direction, e);
};
// handleLeftSelect(selectedItem, checked) {
// return this.handleSelect('left', selectedItem, checked);
// },
// handleRightSelect(selectedItem, checked) {
// return this.handleSelect('right', selectedItem, checked);
// },
onLeftItemSelect(selectedKey, checked) {
return this.onItemSelect('left', selectedKey, checked);
},
onRightItemSelect(selectedKey, checked) {
return this.onItemSelect('right', selectedKey, checked);
},
handleScroll(direction, e) {
this.$emit('scroll', direction, e);
},
handleLeftScroll(e) {
this.handleScroll('left', e);
},
handleRightScroll(e) {
this.handleScroll('right', e);
},
handleSelectChange(direction: TransferDirection, holder: string[]) {
const { sourceSelectedKeys, targetSelectedKeys } = this;
if (direction === 'left') {
this.$emit('selectChange', holder, targetSelectedKeys);
} else {
this.$emit('selectChange', sourceSelectedKeys, holder);
}
},
handleListStyle(listStyle, direction) {
const handleLeftScroll = (e: Event) => {
handleScroll('left', e);
};
const handleRightScroll = (e: Event) => {
handleScroll('right', e);
};
const handleListStyle = (
listStyle: ((style: ListStyle) => CSSProperties) | CSSProperties,
direction: TransferDirection,
) => {
if (typeof listStyle === 'function') {
return listStyle({ direction });
}
return listStyle;
},
};
separateDataSource() {
const { dataSource, rowKey, targetKeys = [] } = this.$props;
const leftDataSource = ref([]);
const rightDataSource = ref([]);
const leftDataSource = [];
const rightDataSource = new Array(targetKeys.length);
watchEffect(() => {
const { dataSource, rowKey, targetKeys = [] } = props;
const ld = [];
const rd = new Array(targetKeys.length);
dataSource.forEach(record => {
if (rowKey) {
record.key = rowKey(record);
@ -354,132 +289,124 @@ const Transfer = defineComponent({
// leftDataSource should be ordered by dataSource
const indexOfKey = targetKeys.indexOf(record.key);
if (indexOfKey !== -1) {
rightDataSource[indexOfKey] = record;
rd[indexOfKey] = record;
} else {
leftDataSource.push(record);
ld.push(record);
}
});
return {
leftDataSource,
rightDataSource,
};
},
leftDataSource.value = ld;
rightDataSource.value = rd;
});
renderTransfer(transferLocale: TransferLocale) {
const props = getOptionProps(this);
expose({ handleSelectChange });
const renderTransfer = (transferLocale: TransferLocale) => {
const {
prefixCls: customizePrefixCls,
disabled,
operations = [],
showSearch,
listStyle,
operationStyle,
filterOption,
lazy,
showSelectAll,
selectAllLabels = [],
oneWay,
pagination,
} = props;
const { class: className, style } = this.$attrs;
const children = getComponent(this, 'children', {}, false);
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('transfer', customizePrefixCls);
const { class: className, style } = attrs;
const renderEmpty = this.configProvider.renderEmpty;
const locale = this.getLocale(transferLocale, renderEmpty);
const { sourceSelectedKeys, targetSelectedKeys, $slots } = this;
const { body, footer } = $slots;
const renderItem = props.render || this.$slots.render;
const { leftDataSource, rightDataSource } = this.separateDataSource();
const leftActive = targetSelectedKeys.length > 0;
const rightActive = sourceSelectedKeys.length > 0;
const children = slots.children;
const mergedPagination = !children && pagination;
const cls = classNames(prefixCls, className, {
[`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-customize-list`]: !!children,
const renderEmpty = configProvider.renderEmpty;
const locale = getLocale(transferLocale, renderEmpty);
const { footer } = slots;
const renderItem = props.render || slots.render;
const leftActive = targetSelectedKeys.value.length > 0;
const rightActive = sourceSelectedKeys.value.length > 0;
const cls = classNames(prefixCls.value, className, {
[`${prefixCls.value}-disabled`]: disabled,
[`${prefixCls.value}-customize-list`]: !!children,
});
const titles = this.getTitles(locale);
const titles = props.titles;
const leftTitle =
(titles && titles[0]) ?? slots.leftTitle?.() ?? (locale.titles || ['', ''])[0];
const rightTitle =
(titles && titles[1]) ?? slots.rightTitle?.() ?? (locale.titles || ['', ''])[1];
return (
<div class={cls} style={style}>
<List
key="leftList"
prefixCls={`${prefixCls}-list`}
titleText={titles[0]}
dataSource={leftDataSource}
prefixCls={`${prefixCls.value}-list`}
dataSource={leftDataSource.value}
filterOption={filterOption}
style={this.handleListStyle(listStyle, 'left')}
checkedKeys={sourceSelectedKeys}
handleFilter={this.handleLeftFilter}
handleClear={this.handleLeftClear}
// handleSelect={this.handleLeftSelect}
handleSelectAll={this.handleLeftSelectAll}
onItemSelect={this.onLeftItemSelect}
onItemSelectAll={this.onLeftItemSelectAll}
style={handleListStyle(listStyle, 'left')}
checkedKeys={sourceSelectedKeys.value}
handleFilter={handleLeftFilter}
handleClear={handleLeftClear}
onItemSelect={onLeftItemSelect}
onItemSelectAll={onLeftItemSelectAll}
renderItem={renderItem}
showSearch={showSearch}
body={body}
renderList={children}
footer={footer}
lazy={lazy}
onScroll={this.handleLeftScroll}
onScroll={handleLeftScroll}
disabled={disabled}
direction="left"
showSelectAll={showSelectAll}
itemUnit={locale.itemUnit}
itemsUnit={locale.itemsUnit}
notFoundContent={locale.notFoundContent}
searchPlaceholder={locale.searchPlaceholder}
selectAllLabel={selectAllLabels[0] || slots.leftSelectAllLabel}
pagination={mergedPagination}
{...locale}
v-slots={{ titleText: () => leftTitle, footer }}
/>
<Operation
key="operation"
class={`${prefixCls}-operation`}
class={`${prefixCls.value}-operation`}
rightActive={rightActive}
rightArrowText={operations[0]}
moveToRight={this.moveToRight}
moveToRight={moveToRight}
leftActive={leftActive}
leftArrowText={operations[1]}
moveToLeft={this.moveToLeft}
moveToLeft={moveToLeft}
style={operationStyle}
disabled={disabled}
direction={direction.value}
oneWay={oneWay}
/>
<List
key="rightList"
prefixCls={`${prefixCls}-list`}
titleText={titles[1]}
dataSource={rightDataSource}
prefixCls={`${prefixCls.value}-list`}
dataSource={rightDataSource.value}
filterOption={filterOption}
style={this.handleListStyle(listStyle, 'right')}
checkedKeys={targetSelectedKeys}
handleFilter={this.handleRightFilter}
handleClear={this.handleRightClear}
// handleSelect={this.handleRightSelect}
handleSelectAll={this.handleRightSelectAll}
onItemSelect={this.onRightItemSelect}
onItemSelectAll={this.onRightItemSelectAll}
style={handleListStyle(listStyle, 'right')}
checkedKeys={targetSelectedKeys.value}
handleFilter={handleRightFilter}
handleClear={handleRightClear}
onItemSelect={onRightItemSelect}
onItemSelectAll={onRightItemSelectAll}
onItemRemove={onRightItemRemove}
renderItem={renderItem}
showSearch={showSearch}
body={body}
renderList={children}
footer={footer}
lazy={lazy}
onScroll={this.handleRightScroll}
onScroll={handleRightScroll}
disabled={disabled}
direction="right"
showSelectAll={showSelectAll}
itemUnit={locale.itemUnit}
itemsUnit={locale.itemsUnit}
notFoundContent={locale.notFoundContent}
searchPlaceholder={locale.searchPlaceholder}
selectAllLabel={selectAllLabels[1] || slots.rightSelectAllLabel}
showRemove={oneWay}
pagination={mergedPagination}
{...locale}
v-slots={{ titleText: () => rightTitle, footer }}
/>
</div>
);
},
},
render() {
return (
};
return () => (
<LocaleReceiver
componentName="Transfer"
defaultLocale={defaultLocale.Transfer}
children={this.renderTransfer}
children={renderTransfer}
/>
);
},

View File

@ -0,0 +1,5 @@
export type PaginationType =
| boolean
| {
pageSize?: number;
};

View File

@ -1,32 +1,20 @@
import classNames from '../_util/classNames';
import PropTypes, { withUndefined } from '../_util/vue-types';
import { isValidElement, splitAttrs, findDOMNode, filterEmpty } from '../_util/props-util';
import initDefaultProps from '../_util/props-util/initDefaultProps';
import BaseMixin from '../_util/BaseMixin';
import PropTypes from '../_util/vue-types';
import { isValidElement, splitAttrs, filterEmpty } from '../_util/props-util';
import DownOutlined from '@ant-design/icons-vue/DownOutlined';
import Checkbox from '../checkbox';
import Menu from '../menu';
import Dropdown from '../dropdown';
import Search from './search';
import defaultRenderList from './renderListBody';
import triggerEvent from '../_util/triggerEvent';
import type { VNode, VNodeTypes } from 'vue';
import { defineComponent, nextTick } from 'vue';
import ListBody from './ListBody';
import type { VNode, VNodeTypes, ExtractPropTypes, PropType } from 'vue';
import { watchEffect, computed } from 'vue';
import { defineComponent, ref } from 'vue';
import type { RadioChangeEvent } from '../radio/interface';
import type { TransferItem } from './index';
const defaultRender = () => null;
const TransferItem = {
key: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
description: PropTypes.string,
disabled: PropTypes.looseBool,
};
export interface DataSourceItem {
key: string;
title: string;
description?: string;
disabled?: boolean;
}
function isRenderResultPlainObject(result: VNode) {
return (
result &&
@ -35,241 +23,70 @@ function isRenderResultPlainObject(result: VNode) {
);
}
export const TransferListProps = {
function getEnabledItemKeys<RecordType extends TransferItem>(items: RecordType[]) {
return items.filter(data => !data.disabled).map(data => data.key);
}
export const transferListProps = {
prefixCls: PropTypes.string,
titleText: PropTypes.string,
dataSource: PropTypes.arrayOf(PropTypes.shape(TransferItem).loose),
dataSource: { type: Array as PropType<TransferItem[]>, default: [] },
filter: PropTypes.string,
filterOption: PropTypes.func,
checkedKeys: PropTypes.arrayOf(PropTypes.string),
handleFilter: PropTypes.func,
handleSelect: PropTypes.func,
handleSelectAll: PropTypes.func,
handleClear: PropTypes.func,
renderItem: PropTypes.func,
showSearch: PropTypes.looseBool,
showSearch: PropTypes.looseBool.def(false),
searchPlaceholder: PropTypes.string,
notFoundContent: PropTypes.any,
itemUnit: PropTypes.string,
itemsUnit: PropTypes.string,
body: PropTypes.any,
renderList: PropTypes.any,
footer: PropTypes.any,
lazy: withUndefined(PropTypes.oneOfType([PropTypes.looseBool, PropTypes.object])),
disabled: PropTypes.looseBool,
direction: PropTypes.string,
showSelectAll: PropTypes.looseBool,
remove: PropTypes.string,
selectAll: PropTypes.string,
selectCurrent: PropTypes.string,
selectInvert: PropTypes.string,
removeAll: PropTypes.string,
removeCurrent: PropTypes.string,
selectAllLabel: PropTypes.any,
showRemove: PropTypes.looseBool,
pagination: PropTypes.any,
onItemSelect: PropTypes.func,
onItemSelectAll: PropTypes.func,
onItemRemove: PropTypes.func,
onScroll: PropTypes.func,
};
function renderListNode(renderList: Function, props: any) {
let bodyContent = renderList ? renderList(props) : null;
const customize = !!bodyContent && filterEmpty(bodyContent).length > 0;
if (!customize) {
bodyContent = defaultRenderList(props);
}
return {
customize,
bodyContent,
};
}
export type TransferListProps = Partial<ExtractPropTypes<typeof transferListProps>>;
export default defineComponent({
name: 'TransferList',
mixins: [BaseMixin],
inheritAttrs: false,
props: initDefaultProps(TransferListProps, {
dataSource: [],
titleText: '',
showSearch: false,
lazy: {},
}),
setup() {
return {
timer: null,
triggerScrollTimer: null,
scrollEvent: null,
props: transferListProps,
emits: ['scroll', 'itemSelectAll', 'itemRemove', 'itemSelect'],
slots: ['footer', 'titleText'],
setup(props, { attrs, slots }) {
const filterValue = ref('');
const transferNode = ref();
const defaultListBodyRef = ref();
const renderListBody = (renderList: any, props: any) => {
let bodyContent = renderList ? renderList(props) : null;
const customize = !!bodyContent && filterEmpty(bodyContent).length > 0;
if (!customize) {
bodyContent = <ListBody {...props} ref={defaultListBodyRef} />;
}
return {
customize,
bodyContent,
};
};
},
data() {
return {
filterValue: '',
};
},
beforeUnmount() {
clearTimeout(this.triggerScrollTimer);
// if (this.scrollEvent) {
// this.scrollEvent.remove();
// }
},
updated() {
nextTick(() => {
if (this.scrollEvent) {
this.scrollEvent.remove();
}
});
},
methods: {
handleScroll(e: Event) {
this.$emit('scroll', e);
},
getCheckStatus(filteredItems: DataSourceItem[]) {
const { checkedKeys } = this.$props;
if (checkedKeys.length === 0) {
return 'none';
}
if (filteredItems.every(item => checkedKeys.indexOf(item.key) >= 0 || !!item.disabled)) {
return 'all';
}
return 'part';
},
getFilteredItems(dataSource: DataSourceItem[], filterValue: string) {
const filteredItems = [];
const filteredRenderItems = [];
dataSource.forEach(item => {
const renderedItem = this.renderItemHtml(item);
const { renderedText } = renderedItem;
// Filter skip
if (filterValue && filterValue.trim() && !this.matchFilter(renderedText, item)) {
return null;
}
filteredItems.push(item);
filteredRenderItems.push(renderedItem);
});
return { filteredItems, filteredRenderItems };
},
getListBody(
prefixCls: string,
searchPlaceholder: string,
filterValue: string,
filteredItems: DataSourceItem[],
notFoundContent: unknown,
bodyDom: unknown,
filteredRenderItems: unknown,
checkedKeys: string[],
renderList: Function,
showSearch: boolean,
disabled: boolean,
) {
const search = showSearch ? (
<div class={`${prefixCls}-body-search-wrapper`}>
<Search
prefixCls={`${prefixCls}-search`}
onChange={this._handleFilter}
handleClear={this._handleClear}
placeholder={searchPlaceholder}
value={filterValue}
disabled={disabled}
/>
</div>
) : null;
let listBody = bodyDom;
if (!listBody) {
let bodyNode: VNodeTypes;
const { onEvents } = splitAttrs(this.$attrs);
const { bodyContent, customize } = renderListNode(renderList, {
...this.$props,
filteredItems,
filteredRenderItems,
selectedKeys: checkedKeys,
...onEvents,
});
// We should wrap customize list body in a classNamed div to use flex layout.
if (customize) {
bodyNode = <div class={`${prefixCls}-body-customize-wrapper`}>{bodyContent}</div>;
} else {
bodyNode = filteredItems.length ? (
bodyContent
) : (
<div class={`${prefixCls}-body-not-found`}>{notFoundContent}</div>
);
}
listBody = (
<div
class={classNames(
showSearch ? `${prefixCls}-body ${prefixCls}-body-with-search` : `${prefixCls}-body`,
)}
>
{search}
{bodyNode}
</div>
);
}
return listBody;
},
getCheckBox(filteredItems: DataSourceItem[], showSelectAll: boolean, disabled: boolean) {
const checkStatus = this.getCheckStatus(filteredItems);
const checkedAll = checkStatus === 'all';
const checkAllCheckbox = showSelectAll !== false && (
<Checkbox
disabled={disabled}
checked={checkedAll}
indeterminate={checkStatus === 'part'}
onChange={() => {
// Only select enabled items
this.$emit(
'itemSelectAll',
filteredItems.filter(item => !item.disabled).map(({ key }) => key),
!checkedAll,
);
}}
/>
);
return checkAllCheckbox;
},
_handleSelect(selectedItem: DataSourceItem) {
const { checkedKeys } = this.$props;
const result = checkedKeys.some(key => key === selectedItem.key);
this.handleSelect(selectedItem, !result);
},
_handleFilter(e: RadioChangeEvent) {
const { handleFilter } = this.$props;
const {
target: { value: filterValue },
} = e;
this.setState({ filterValue });
handleFilter(e);
if (!filterValue) {
return;
}
// Manually trigger scroll event for lazy search bug
// https://github.com/ant-design/ant-design/issues/5631
this.triggerScrollTimer = setTimeout(() => {
const transferNode = findDOMNode(this);
const listNode = transferNode.querySelectorAll('.ant-transfer-list-content')[0];
if (listNode) {
triggerEvent(listNode, 'scroll');
}
}, 0);
},
_handleClear(e: Event) {
this.setState({ filterValue: '' });
this.handleClear(e);
},
matchFilter(text: string, item: DataSourceItem) {
const { filterValue } = this.$data;
const { filterOption } = this.$props;
if (filterOption) {
return filterOption(filterValue, item);
}
return text.indexOf(filterValue) >= 0;
},
renderItemHtml(item: DataSourceItem) {
const { renderItem = defaultRender } = this.$props;
const renderItemHtml = (item: TransferItem) => {
const { renderItem = defaultRender } = props;
const renderResult = renderItem(item);
const isRenderResultPlain = isRenderResultPlainObject(renderResult);
return {
@ -277,82 +94,311 @@ export default defineComponent({
renderedEl: isRenderResultPlain ? renderResult.label : renderResult,
item,
};
},
filterNull(arr: unknown[]) {
return arr.filter(item => {
return item !== null;
};
const filteredItems = ref([]);
const filteredRenderItems = ref([]);
watchEffect(() => {
const fItems = [];
const fRenderItems = [];
props.dataSource.forEach(item => {
const renderedItem = renderItemHtml(item);
const { renderedText } = renderedItem;
// Filter skip
if (filterValue.value && filterValue.value.trim() && !matchFilter(renderedText, item)) {
return null;
}
fItems.push(item);
fRenderItems.push(renderedItem);
});
},
},
render() {
const { filterValue } = this.$data;
const {
prefixCls,
dataSource,
titleText,
checkedKeys,
disabled,
body,
footer,
showSearch,
searchPlaceholder,
notFoundContent,
itemUnit,
itemsUnit,
renderList,
showSelectAll,
} = this.$props;
// Custom Layout
const footerDom = footer && footer({ ...this.$props });
const bodyDom = body && body({ ...this.$props });
const listCls = classNames(prefixCls, {
[`${prefixCls}-with-footer`]: !!footerDom,
filteredItems.value = fItems;
filteredRenderItems.value = fRenderItems;
});
// ====================== Get filtered, checked item list ======================
const checkStatus = computed(() => {
const { checkedKeys } = props;
if (checkedKeys.length === 0) {
return 'none';
}
if (
filteredItems.value.every(item => checkedKeys.indexOf(item.key) >= 0 || !!item.disabled)
) {
return 'all';
}
return 'part';
});
const { filteredItems, filteredRenderItems } = this.getFilteredItems(dataSource, filterValue);
const enabledItemKeys = computed(() => {
return getEnabledItemKeys(filteredItems.value);
});
// ================================= List Body =================================
const getNewSelectKeys = (keys, unCheckedKeys) => {
return Array.from(new Set([...keys, ...props.checkedKeys])).filter(
key => unCheckedKeys.indexOf(key) === -1,
);
};
const unit = dataSource.length > 1 ? itemsUnit : itemUnit;
const getCheckBox = (showSelectAll: boolean, disabled?: boolean, prefixCls?: string) => {
const checkedAll = checkStatus.value === 'all';
const checkAllCheckbox = showSelectAll !== false && (
<Checkbox
disabled={disabled}
checked={checkedAll}
indeterminate={checkStatus.value === 'part'}
class={`${prefixCls}-checkbox`}
onChange={() => {
// Only select enabled items
const listBody = this.getListBody(
prefixCls,
searchPlaceholder,
filterValue,
filteredItems,
notFoundContent,
bodyDom,
filteredRenderItems,
checkedKeys,
renderList,
showSearch,
disabled,
);
const keys = enabledItemKeys.value;
props.onItemSelectAll(
getNewSelectKeys(!checkedAll ? keys : [], checkedAll ? props.checkedKeys : []),
);
}}
/>
);
const listFooter = footerDom ? <div class={`${prefixCls}-footer`}>{footerDom}</div> : null;
return checkAllCheckbox;
};
const checkAllCheckbox = this.getCheckBox(filteredItems, showSelectAll, disabled);
const handleFilter = (e: RadioChangeEvent) => {
const {
target: { value: filter },
} = e;
filterValue.value = filter;
props.handleFilter?.(e);
};
const handleClear = (e: Event) => {
filterValue.value = '';
props.handleClear?.(e);
};
const matchFilter = (text: string, item: TransferItem) => {
const { filterOption } = props;
if (filterOption) {
return filterOption(filterValue.value, item);
}
return text.indexOf(filterValue.value) >= 0;
};
return (
<div class={listCls} style={this.$attrs.style}>
<div class={`${prefixCls}-header`}>
{checkAllCheckbox}
<span class={`${prefixCls}-header-selected`}>
<span>
{(checkedKeys.length > 0 ? `${checkedKeys.length}/` : '') + filteredItems.length}{' '}
{unit}
</span>
<span class={`${prefixCls}-header-title`}>{titleText}</span>
</span>
const getSelectAllLabel = (selectedCount: number, totalCount: number) => {
const { itemsUnit, itemUnit, selectAllLabel } = props;
if (selectAllLabel) {
return typeof selectAllLabel === 'function'
? selectAllLabel({ selectedCount, totalCount })
: selectAllLabel;
}
const unit = totalCount > 1 ? itemsUnit : itemUnit;
return (
<>
{(selectedCount > 0 ? `${selectedCount}/` : '') + totalCount} {unit}
</>
);
};
const getListBody = (
prefixCls: string,
searchPlaceholder: string,
checkedKeys: string[],
renderList: Function,
showSearch: boolean,
disabled: boolean,
) => {
const search = showSearch ? (
<div class={`${prefixCls}-body-search-wrapper`}>
<Search
prefixCls={`${prefixCls}-search`}
onChange={handleFilter}
handleClear={handleClear}
placeholder={searchPlaceholder}
value={filterValue.value}
disabled={disabled}
/>
</div>
{listBody}
{listFooter}
</div>
);
) : null;
let bodyNode: VNodeTypes;
const { onEvents } = splitAttrs(attrs);
const { bodyContent, customize } = renderListBody(renderList, {
...props,
filteredItems: filteredItems.value,
filteredRenderItems: filteredRenderItems.value,
selectedKeys: checkedKeys,
...onEvents,
});
// We should wrap customize list body in a classNamed div to use flex layout.
if (customize) {
bodyNode = <div class={`${prefixCls}-body-customize-wrapper`}>{bodyContent}</div>;
} else {
bodyNode = filteredItems.value.length ? (
bodyContent
) : (
<div class={`${prefixCls}-body-not-found`}>{props.notFoundContent}</div>
);
}
return (
<div
class={
showSearch ? `${prefixCls}-body ${prefixCls}-body-with-search` : `${prefixCls}-body`
}
ref={transferNode}
>
{search}
{bodyNode}
</div>
);
};
return () => {
const {
prefixCls,
checkedKeys,
disabled,
showSearch,
searchPlaceholder,
selectAll,
selectCurrent,
selectInvert,
removeAll,
removeCurrent,
renderList,
onItemSelectAll,
onItemRemove,
showSelectAll,
showRemove,
pagination,
} = props;
// Custom Layout
const footerDom = slots.footer?.({ ...props });
const listCls = classNames(prefixCls, {
[`${prefixCls}-with-pagination`]: !!pagination,
[`${prefixCls}-with-footer`]: !!footerDom,
});
// ================================= List Body =================================
const listBody = getListBody(
prefixCls,
searchPlaceholder,
checkedKeys,
renderList,
showSearch,
disabled,
);
const listFooter = footerDom ? <div class={`${prefixCls}-footer`}>{footerDom}</div> : null;
const checkAllCheckbox =
!showRemove && !pagination && getCheckBox(showSelectAll, disabled, prefixCls);
let menu = null;
if (showRemove) {
menu = (
<Menu>
{/* Remove Current Page */}
{pagination && (
<Menu.Item
onClick={() => {
const pageKeys = getEnabledItemKeys(
(defaultListBodyRef.value.items || []).map(entity => entity.item),
);
onItemRemove?.(pageKeys);
}}
>
{removeCurrent}
</Menu.Item>
)}
{/* Remove All */}
<Menu.Item
onClick={() => {
onItemRemove?.(enabledItemKeys.value);
}}
>
{removeAll}
</Menu.Item>
</Menu>
);
} else {
menu = (
<Menu>
<Menu.Item
onClick={() => {
const keys = enabledItemKeys.value;
onItemSelectAll(getNewSelectKeys(keys, []));
}}
>
{selectAll}
</Menu.Item>
{pagination && (
<Menu.Item
onClick={() => {
const pageKeys = getEnabledItemKeys(
(defaultListBodyRef.value.items || []).map(entity => entity.item),
);
onItemSelectAll(getNewSelectKeys(pageKeys, []));
}}
>
{selectCurrent}
</Menu.Item>
)}
<Menu.Item
onClick={() => {
let availableKeys: string[];
if (pagination) {
availableKeys = getEnabledItemKeys(
(defaultListBodyRef.value.items || []).map(entity => entity.item),
);
} else {
availableKeys = enabledItemKeys.value;
}
const checkedKeySet = new Set(checkedKeys);
const newCheckedKeys: string[] = [];
const newUnCheckedKeys: string[] = [];
availableKeys.forEach(key => {
if (checkedKeySet.has(key)) {
newUnCheckedKeys.push(key);
} else {
newCheckedKeys.push(key);
}
});
onItemSelectAll(getNewSelectKeys(newCheckedKeys, newUnCheckedKeys));
}}
>
{selectInvert}
</Menu.Item>
</Menu>
);
}
const dropdown = (
<Dropdown class={`${prefixCls}-header-dropdown`} overlay={menu} disabled={disabled}>
<DownOutlined />
</Dropdown>
);
return (
<div class={listCls} style={attrs.style}>
<div class={`${prefixCls}-header`}>
{checkAllCheckbox}
{dropdown}
<span class={`${prefixCls}-header-selected`}>
<span>{getSelectAllLabel(checkedKeys.length, filteredItems.value.length)}</span>
<span class={`${prefixCls}-header-title`}>{slots.titleText?.()}</span>
</span>
</div>
{listBody}
{listFooter}
</div>
);
};
},
});

View File

@ -2,11 +2,12 @@ import type { CSSProperties, FunctionalComponent } from 'vue';
import LeftOutlined from '@ant-design/icons-vue/LeftOutlined';
import RightOutlined from '@ant-design/icons-vue/RightOutlined';
import Button from '../button';
import type { Direction } from '../config-provider';
function noop() {}
export interface TransferOperationProps {
class?: any;
class?: string;
leftArrowText?: string;
rightArrowText?: string;
moveToLeft?: (e: MouseEvent) => void;
@ -15,6 +16,8 @@ export interface TransferOperationProps {
rightActive?: boolean;
style?: CSSProperties | string;
disabled?: boolean;
direction?: Direction;
oneWay?: boolean;
}
const Operation: FunctionalComponent<TransferOperationProps> = props => {
@ -28,6 +31,8 @@ const Operation: FunctionalComponent<TransferOperationProps> = props => {
rightActive,
class: className,
style,
direction,
oneWay,
} = props;
return (
@ -37,23 +42,25 @@ const Operation: FunctionalComponent<TransferOperationProps> = props => {
size="small"
disabled={disabled || !rightActive}
onClick={moveToRight}
icon={<RightOutlined />}
icon={direction !== 'rtl' ? <RightOutlined /> : <LeftOutlined />}
>
{rightArrowText}
</Button>
<Button
type="primary"
size="small"
disabled={disabled || !leftActive}
onClick={moveToLeft}
icon={<LeftOutlined />}
>
{leftArrowText}
</Button>
{!oneWay && (
<Button
type="primary"
size="small"
disabled={disabled || !leftActive}
onClick={moveToLeft}
icon={direction !== 'rtl' ? <LeftOutlined /> : <RightOutlined />}
>
{leftArrowText}
</Button>
)}
</div>
);
};
Operation.displayName = 'Operation';
Operation.inheritAttrs = false;
export default Operation;

View File

@ -1,113 +0,0 @@
import { defineComponent, nextTick } from 'vue';
import raf from '../_util/raf';
import ListItem from './ListItem';
import PropTypes, { withUndefined } from '../_util/vue-types';
import { findDOMNode } from '../_util/props-util';
import { getTransitionGroupProps, TransitionGroup } from '../_util/transition';
import type { DataSourceItem } from './list';
const ListBody = defineComponent({
name: 'ListBody',
inheritAttrs: false,
props: {
prefixCls: PropTypes.string,
filteredRenderItems: PropTypes.array.def([]),
lazy: withUndefined(PropTypes.oneOfType([PropTypes.looseBool, PropTypes.object])),
selectedKeys: PropTypes.array,
disabled: PropTypes.looseBool,
onItemSelect: PropTypes.func,
onItemSelectAll: PropTypes.func,
onScroll: PropTypes.func,
},
setup() {
return {
mountId: null,
lazyId: null,
};
},
data() {
return {
mounted: false,
};
},
computed: {
itemsLength(): number {
return this.filteredRenderItems ? this.filteredRenderItems.length : 0;
},
},
watch: {
itemsLength() {
nextTick(() => {
const { lazy } = this.$props;
if (lazy !== false) {
const container = findDOMNode(this);
raf.cancel(this.lazyId);
this.lazyId = raf(() => {
if (container) {
const scrollEvent = new Event('scroll', { bubbles: true });
container.dispatchEvent(scrollEvent);
}
});
}
});
},
},
mounted() {
this.mountId = raf(() => {
this.mounted = true;
});
},
beforeUnmount() {
raf.cancel(this.mountId);
raf.cancel(this.lazyId);
},
methods: {
handleItemSelect(item: DataSourceItem) {
const { selectedKeys } = this.$props;
const checked = selectedKeys.indexOf(item.key) >= 0;
this.$emit('itemSelect', item.key, !checked);
},
handleScroll(e: Event) {
this.$emit('scroll', e);
},
},
render() {
const { mounted } = this.$data;
const {
prefixCls,
filteredRenderItems,
lazy,
selectedKeys,
disabled: globalDisabled,
} = this.$props;
const items = filteredRenderItems.map(({ renderedEl, renderedText, item }: any) => {
const { disabled } = item;
const checked = selectedKeys.indexOf(item.key) >= 0;
return (
<ListItem
disabled={globalDisabled || disabled}
key={item.key}
item={item}
lazy={lazy}
renderedText={renderedText}
renderedEl={renderedEl}
checked={checked}
prefixCls={prefixCls}
onClick={this.handleItemSelect}
/>
);
});
const transitionProps = getTransitionGroupProps(
mounted ? `${prefixCls}-content-item-highlight` : '',
{
tag: 'ul',
class: `${prefixCls}-content`,
onScroll: this.handleScroll,
},
);
return <TransitionGroup {...transitionProps}>{items}</TransitionGroup>;
},
});
export default props => <ListBody {...props} />;

View File

@ -1,62 +1,65 @@
import PropTypes from '../_util/vue-types';
import { getOptionProps } from '../_util/props-util';
import initDefaultProps from '../_util/props-util/initDefaultProps';
import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled';
import SearchOutlined from '@ant-design/icons-vue/SearchOutlined';
import Input from '../input';
import type { ExtractPropTypes } from 'vue';
import { defineComponent } from 'vue';
export const TransferSearchProps = {
export const transferSearchProps = {
prefixCls: PropTypes.string,
placeholder: PropTypes.string,
value: PropTypes.any,
value: PropTypes.string,
handleClear: PropTypes.func,
disabled: PropTypes.looseBool,
onChange: PropTypes.func,
};
export type TransferSearchProps = Partial<ExtractPropTypes<typeof transferSearchProps>>;
export default defineComponent({
name: 'Search',
inheritAttrs: false,
props: initDefaultProps(TransferSearchProps, {
props: initDefaultProps(transferSearchProps, {
placeholder: '',
}),
methods: {
handleChange(e: Event) {
this.$emit('change', e);
},
handleClear2(e: Event) {
e.preventDefault();
const { handleClear, disabled } = this.$props;
emits: ['change'],
setup(props, { emit }) {
const handleChange = (e: Event) => {
emit('change', e);
};
const handleClearFn = (e: Event) => {
const { handleClear, disabled } = props;
if (!disabled && handleClear) {
handleClear(e);
}
},
},
render() {
const { placeholder, value, prefixCls, disabled } = getOptionProps(this);
const icon =
value && value.length > 0 ? (
<a href="#" class={`${prefixCls}-action`} onClick={this.handleClear2}>
<CloseCircleFilled />
</a>
) : (
<span class={`${prefixCls}-action`}>
<SearchOutlined />
</span>
);
};
return (
<>
<Input
placeholder={placeholder}
class={prefixCls}
value={value}
onChange={this.handleChange}
disabled={disabled}
/>
{icon}
</>
);
return () => {
const { placeholder, value, prefixCls, disabled } = props;
const icon =
value && value.length > 0 ? (
<a class={`${prefixCls}-action`} onClick={handleClearFn}>
<CloseCircleFilled />
</a>
) : (
<span class={`${prefixCls}-action`}>
<SearchOutlined />
</span>
);
return (
<>
<Input
placeholder={placeholder}
class={prefixCls}
value={value}
onChange={handleChange}
disabled={disabled}
/>
{icon}
</>
);
};
},
});

View File

@ -1,36 +1,14 @@
@import './index.less';
@table-prefix-cls: ~'@{ant-prefix}-table';
@input-prefix-cls: ~'@{ant-prefix}-input';
.@{transfer-prefix-cls}-customize-list {
display: flex;
.@{transfer-prefix-cls}-operation {
flex: none;
align-self: center;
}
.@{transfer-prefix-cls}-list {
flex: auto;
flex: 1 1 50%;
width: auto;
height: auto;
min-height: @transfer-list-height;
&-body {
&-with-search {
padding-top: 0;
}
// Search box in customize mode do not need fix top
&-search-wrapper {
position: relative;
padding-bottom: 0;
}
&-customize-wrapper {
padding: 12px;
}
}
}
// =================== Hook Components ===================
@ -59,4 +37,9 @@
margin: 16px 0 4px;
}
}
.@{input-prefix-cls} {
&[disabled] {
background-color: transparent;
}
}
}

View File

@ -1,18 +1,20 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@import '../../checkbox/style/mixin';
@import './customize.less';
@import './customize';
@transfer-prefix-cls: ~'@{ant-prefix}-transfer';
@transfer-header-vertical-padding: (
(@transfer-header-height - 1px - (@font-size-base * @line-height-base)) / 2
@transfer-header-vertical-padding: ceil(
((@transfer-header-height - 1px - @font-size-base * @line-height-base) / 2)
);
.@{transfer-prefix-cls} {
.reset-component();
position: relative;
display: flex;
align-items: stretch;
&-disabled {
.@{transfer-prefix-cls}-list {
@ -21,17 +23,16 @@
}
&-list {
position: relative;
display: inline-block;
display: flex;
flex-direction: column;
width: 180px;
height: @transfer-list-height;
padding-top: @transfer-header-height;
vertical-align: middle;
border: @border-width-base @border-style-base @border-color-base;
border-radius: @border-radius-base;
&-with-footer {
padding-bottom: 34px;
&-with-pagination {
width: 250px;
height: auto;
}
&-search {
@ -39,13 +40,14 @@
padding-left: @control-padding-horizontal-sm;
&-action {
position: absolute;
top: 12px;
top: @transfer-list-search-icon-top;
right: 12px;
bottom: 12px;
width: 28px;
color: @disabled-color;
line-height: @input-height-base;
text-align: center;
.@{iconfont-css-prefix} {
color: @disabled-color;
transition: all 0.3s;
@ -60,75 +62,128 @@
}
&-header {
position: absolute;
top: 0;
left: 0;
width: 100%;
display: flex;
flex: none;
align-items: center;
height: @transfer-header-height;
// border-top is on the transfer dom. We should minus 1px for this
padding: (@transfer-header-vertical-padding - 1px) @control-padding-horizontal
@transfer-header-vertical-padding;
overflow: hidden;
color: @text-color;
background: @component-background;
border-bottom: @border-width-base @border-style-base @border-color-split;
border-radius: @border-radius-base @border-radius-base 0 0;
&-title {
position: absolute;
right: 12px;
> *:not(:last-child) {
margin-right: 4px;
}
.@{ant-prefix}-checkbox-wrapper + span {
padding-left: 8px;
> * {
flex: none;
}
&-title {
flex: auto;
overflow: hidden;
white-space: nowrap;
text-align: right;
text-overflow: ellipsis;
}
&-dropdown {
font-size: 10px;
transform: translateY(10%);
cursor: pointer;
&[disabled] {
cursor: not-allowed;
}
}
}
&-body {
position: relative;
height: 100%;
display: flex;
flex: auto;
flex-direction: column;
overflow: hidden;
font-size: @font-size-base;
&-search-wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
padding: 12px;
position: relative;
flex: none;
padding: @padding-sm;
}
}
&-body-with-search {
padding-top: @input-height-base + 24px;
}
&-content {
height: 100%;
flex: auto;
margin: 0;
padding: 0;
overflow: auto;
list-style: none;
> .LazyLoad {
animation: transferHighlightIn 1s;
}
&-item {
min-height: 32px;
padding: 6px @control-padding-horizontal;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: flex;
align-items: center;
min-height: @transfer-item-height;
padding: @transfer-item-padding-vertical @control-padding-horizontal;
line-height: @transfer-item-height - 2 * @transfer-item-padding-vertical;
transition: all 0.3s;
> span {
padding-right: 0;
> *:not(:last-child) {
margin-right: 8px;
}
> * {
flex: none;
}
&-text {
padding-left: 8px;
flex: auto;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&-remove {
.operation-unit();
position: relative;
color: @border-color-base;
&::after {
position: absolute;
top: -@transfer-item-padding-vertical;
right: -50%;
bottom: -@transfer-item-padding-vertical;
left: -50%;
content: '';
}
&:hover {
color: @link-hover-color;
}
}
}
&-item:not(&-item-disabled):hover {
background-color: @transfer-item-hover-bg;
cursor: pointer;
&-item:not(&-item-disabled) {
&:hover {
background-color: @transfer-item-hover-bg;
cursor: pointer;
}
&.@{transfer-prefix-cls}-list-content-item-checked:hover {
background-color: darken(@item-active-bg, 2%);
}
}
// Do not change hover style when `oneWay` mode
&-show-remove &-item:not(&-item-disabled):hover {
background: transparent;
cursor: default;
}
&-item-checked {
background-color: @item-active-bg;
}
&-item-disabled {
@ -137,35 +192,31 @@
}
}
&-pagination {
padding: @padding-xs 0;
text-align: right;
border-top: @border-width-base @border-style-base @border-color-split;
}
&-body-not-found {
position: absolute;
top: 50%;
flex: none;
width: 100%;
padding-top: 0;
margin: auto 0;
color: @disabled-color;
text-align: center;
transform: translateY(-50%);
// with filter should offset the search box height
.@{transfer-prefix-cls}-list-body-with-search & {
margin-top: (@input-height-base / 2);
}
}
&-footer {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
border-top: @border-width-base @border-style-base @border-color-split;
border-radius: 0 0 @border-radius-base @border-radius-base;
}
}
&-operation {
display: inline-block;
display: flex;
flex: none;
flex-direction: column;
align-self: center;
margin: 0 8px;
overflow: hidden;
vertical-align: middle;
.@{ant-prefix}-btn {
@ -180,13 +231,10 @@
}
}
}
.@{ant-prefix}-empty-image {
max-height: (@transfer-header-height / 2) - 22;
}
}
@keyframes transferHighlightIn {
0% {
background: @primary-2;
}
100% {
background: transparent;
}
}
@import './rtl';

View File

@ -6,3 +6,6 @@ import '../../empty/style';
import '../../checkbox/style';
import '../../button/style';
import '../../input/style';
import '../../menu/style';
import '../../dropdown/style';
import '../../pagination/style';

View File

@ -0,0 +1,69 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@import '../../checkbox/style/mixin';
@transfer-prefix-cls: ~'@{ant-prefix}-transfer';
.@{transfer-prefix-cls} {
&-rtl {
direction: rtl;
}
&-list {
&-search {
.@{transfer-prefix-cls}-rtl & {
padding-right: @control-padding-horizontal-sm;
padding-left: 24px;
}
&-action {
.@{transfer-prefix-cls}-rtl & {
right: auto;
left: 12px;
}
}
}
&-header {
> *:not(:last-child) {
.@{transfer-prefix-cls}-rtl & {
margin-right: 0;
margin-left: 4px;
}
}
.@{transfer-prefix-cls}-rtl & {
right: 0;
left: auto;
}
&-title {
.@{transfer-prefix-cls}-rtl & {
text-align: left;
}
}
}
&-content {
&-item {
> *:not(:last-child) {
.@{transfer-prefix-cls}-rtl & {
margin-right: 0;
margin-left: 8px;
}
}
}
}
&-pagination {
.@{transfer-prefix-cls}-rtl & {
text-align: left;
}
}
&-footer {
.@{transfer-prefix-cls}-rtl & {
right: 0;
left: auto;
}
}
}
}

View File

@ -1,25 +0,0 @@
import PropTypes from '../_util/vue-types';
export default {
name: 'Content',
props: {
prefixCls: PropTypes.string,
overlay: PropTypes.any,
trigger: PropTypes.any,
overlayInnerStyle: PropTypes.any,
},
updated() {
const { trigger } = this;
if (trigger) {
trigger.forcePopupAlign();
}
},
render() {
const { overlay, prefixCls, overlayInnerStyle } = this;
return (
<div class={`${prefixCls}-inner`} role="tooltip" style={overlayInnerStyle}>
{typeof overlay === 'function' ? overlay() : overlay}
</div>
);
},
};

View File

@ -1,103 +0,0 @@
import PropTypes from '../_util/vue-types';
import Trigger from '../vc-trigger';
import { placements } from './placements';
import Content from './Content';
import { hasProp, getComponent, getOptionProps, getSlot } from '../_util/props-util';
import { defineComponent } from 'vue';
function noop() {}
export default defineComponent({
name: 'Tooltip',
inheritAttrs: false,
props: {
trigger: PropTypes.any.def(['hover']),
defaultVisible: PropTypes.looseBool,
visible: PropTypes.looseBool,
placement: PropTypes.string.def('right'),
transitionName: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
animation: PropTypes.any,
afterVisibleChange: PropTypes.func.def(() => {}),
overlay: PropTypes.any,
overlayStyle: PropTypes.object,
overlayClassName: PropTypes.string,
prefixCls: PropTypes.string.def('rc-tooltip'),
mouseEnterDelay: PropTypes.number.def(0),
mouseLeaveDelay: PropTypes.number.def(0.1),
getTooltipContainer: PropTypes.func,
destroyTooltipOnHide: PropTypes.looseBool.def(false),
align: PropTypes.object.def(() => ({})),
arrowContent: PropTypes.any.def(null),
tipId: PropTypes.string,
builtinPlacements: PropTypes.object,
overlayInnerStyle: PropTypes.style,
},
methods: {
getPopupElement() {
const { prefixCls, tipId, overlayInnerStyle } = this.$props;
return [
<div class={`${prefixCls}-arrow`} key="arrow">
{getComponent(this, 'arrowContent')}
</div>,
<Content
key="content"
trigger={this.$refs.trigger}
prefixCls={prefixCls}
id={tipId}
overlay={getComponent(this, 'overlay')}
overlayInnerStyle={overlayInnerStyle}
/>,
];
},
getPopupDomNode() {
return this.$refs.trigger.getPopupDomNode();
},
},
render(h) {
const {
overlayClassName,
trigger,
mouseEnterDelay,
mouseLeaveDelay,
overlayStyle,
prefixCls,
afterVisibleChange,
transitionName,
animation,
placement,
align,
destroyTooltipOnHide,
defaultVisible,
getTooltipContainer,
...restProps
} = getOptionProps(this);
const extraProps = { ...restProps };
if (hasProp(this, 'visible')) {
extraProps.popupVisible = this.$props.visible;
}
const { $attrs } = this;
const triggerProps = {
popupClassName: overlayClassName,
prefixCls,
action: trigger,
builtinPlacements: placements,
popupPlacement: placement,
popupAlign: align,
getPopupContainer: getTooltipContainer,
afterPopupVisibleChange: afterVisibleChange,
popupTransitionName: transitionName,
popupAnimation: animation,
defaultPopupVisible: defaultVisible,
destroyPopupOnHide: destroyTooltipOnHide,
mouseLeaveDelay,
popupStyle: overlayStyle,
mouseEnterDelay,
...extraProps,
...$attrs,
onPopupVisibleChange: $attrs.onVisibleChange || noop,
onPopupAlign: $attrs.onPopupAlign || noop,
ref: 'trigger',
popup: this.getPopupElement(),
};
return <Trigger {...triggerProps}>{getSlot(this)[0]}</Trigger>;
},
});

View File

@ -1,4 +0,0 @@
// based on rc-tooltip 3.7.3
import Tooltip from './Tooltip';
export default Tooltip;

View File

@ -0,0 +1,4 @@
// base rc-tooltip 5.1.1
import Tooltip from './src/Tooltip';
export default Tooltip;

View File

@ -0,0 +1,29 @@
import type { ExtractPropTypes } from 'vue';
import { defineComponent } from 'vue';
import PropTypes from '../../_util/vue-types';
const tooltipContentProps = {
prefixCls: PropTypes.string,
overlay: PropTypes.any,
id: PropTypes.string,
overlayInnerStyle: PropTypes.any,
};
export type TooltipContentProps = Partial<ExtractPropTypes<typeof tooltipContentProps>>;
export default defineComponent({
name: 'Content',
props: tooltipContentProps,
setup(props: TooltipContentProps) {
return () => (
<div
class={`${props.prefixCls}-inner`}
id={props.id}
role="tooltip"
style={props.overlayInnerStyle}
>
{typeof props.overlay === 'function' ? props.overlay() : props.overlay}
</div>
);
},
});

View File

@ -0,0 +1,123 @@
import PropTypes from '../../_util/vue-types';
import Trigger from '../../vc-trigger';
import { placements } from './placements';
import Content from './Content';
import { getPropsSlot } from '../../_util/props-util';
import { defineComponent, ref, watchEffect } from 'vue';
function noop() {}
export default defineComponent({
name: 'Tooltip',
inheritAttrs: false,
props: {
trigger: PropTypes.any.def(['hover']),
defaultVisible: PropTypes.looseBool,
visible: PropTypes.looseBool,
placement: PropTypes.string.def('right'),
transitionName: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
animation: PropTypes.any,
afterVisibleChange: PropTypes.func.def(() => {}),
overlay: PropTypes.any,
overlayStyle: PropTypes.object,
overlayClassName: PropTypes.string,
prefixCls: PropTypes.string.def('rc-tooltip'),
mouseEnterDelay: PropTypes.number.def(0.1),
mouseLeaveDelay: PropTypes.number.def(0.1),
getTooltipContainer: PropTypes.func,
destroyTooltipOnHide: PropTypes.looseBool.def(false),
align: PropTypes.object.def(() => ({})),
arrowContent: PropTypes.any.def(null),
tipId: PropTypes.string,
builtinPlacements: PropTypes.object,
overlayInnerStyle: PropTypes.style,
popupVisible: PropTypes.looseBool,
},
slots: ['arrowContent', 'overlay'],
setup(props, { slots, attrs, expose }) {
const triggerDOM = ref();
const getPopupElement = () => {
const { prefixCls, tipId, overlayInnerStyle } = props;
return [
<div class={`${prefixCls}-arrow`} key="arrow">
{getPropsSlot(slots, props, 'arrowContent')}
</div>,
<Content
key="content"
prefixCls={prefixCls}
id={tipId}
overlay={getPropsSlot(slots, props, 'overlay')}
overlayInnerStyle={overlayInnerStyle}
/>,
];
};
const getPopupDomNode = () => {
return triggerDOM.value.getPopupDomNode();
};
expose({ getPopupDomNode, triggerDOM });
const destroyTooltip = ref(false);
const autoDestroy = ref(false);
watchEffect(() => {
const { destroyTooltipOnHide } = props;
if (typeof destroyTooltipOnHide === 'boolean') {
destroyTooltip.value = destroyTooltipOnHide;
} else if (destroyTooltipOnHide && typeof destroyTooltipOnHide === 'object') {
const { keepParent } = destroyTooltipOnHide;
destroyTooltip.value = keepParent === true;
autoDestroy.value = keepParent === false;
}
});
return () => {
const {
overlayClassName,
trigger,
mouseEnterDelay,
mouseLeaveDelay,
overlayStyle,
prefixCls,
afterVisibleChange,
transitionName,
animation,
placement,
align,
destroyTooltipOnHide,
defaultVisible,
getTooltipContainer,
...restProps
} = props;
const extraProps = { ...restProps };
if (props.visible !== undefined) {
extraProps.popupVisible = props.visible;
}
const triggerProps = {
popupClassName: overlayClassName,
prefixCls,
action: trigger,
builtinPlacements: placements,
popupPlacement: placement,
popupAlign: align,
getPopupContainer: getTooltipContainer,
afterPopupVisibleChange: afterVisibleChange,
popupTransitionName: transitionName,
popupAnimation: animation,
defaultPopupVisible: defaultVisible,
destroyPopupOnHide: destroyTooltip.value,
autoDestroy: autoDestroy.value,
mouseLeaveDelay,
popupStyle: overlayStyle,
mouseEnterDelay,
...extraProps,
...attrs,
onPopupVisibleChange: (attrs.onVisibleChange as any) || noop,
onPopupAlign: attrs.onPopupAlign || noop,
ref: triggerDOM,
popup: getPopupElement(),
};
return <Trigger {...triggerProps}>{slots.default?.()}</Trigger>;
};
},
});