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
parent
16ee0dd2f1
commit
8198cab549
|
@ -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) {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
// ---
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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(() => ({})),
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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,不方便传递,保留notFoundContent及searchPlaceholder
|
||||
// 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}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export type PaginationType =
|
||||
| boolean
|
||||
| {
|
||||
pageSize?: number;
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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} />;
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
};
|
|
@ -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>;
|
||||
},
|
||||
});
|
|
@ -1,4 +0,0 @@
|
|||
// based on rc-tooltip 3.7.3
|
||||
import Tooltip from './Tooltip';
|
||||
|
||||
export default Tooltip;
|
|
@ -0,0 +1,4 @@
|
|||
// base rc-tooltip 5.1.1
|
||||
import Tooltip from './src/Tooltip';
|
||||
|
||||
export default Tooltip;
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -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>;
|
||||
};
|
||||
},
|
||||
});
|
Loading…
Reference in New Issue