1
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import type { DirectionType } from '../config-provider';
|
||||
export interface TransferOperationProps {
|
||||
className?: string;
|
||||
actions: React.ReactNode[];
|
||||
moveToLeft?: React.MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>;
|
||||
moveToRight?: React.MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>;
|
||||
leftActive?: boolean;
|
||||
rightActive?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
disabled?: boolean;
|
||||
direction?: DirectionType;
|
||||
oneWay?: boolean;
|
||||
}
|
||||
declare const Actions: React.FC<TransferOperationProps>;
|
||||
export default Actions;
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import LeftOutlined from "@ant-design/icons/es/icons/LeftOutlined";
|
||||
import RightOutlined from "@ant-design/icons/es/icons/RightOutlined";
|
||||
import Button from '../button/Button';
|
||||
function getArrowIcon(type, direction) {
|
||||
const isRight = type === 'right';
|
||||
if (direction !== 'rtl') {
|
||||
return isRight ? /*#__PURE__*/React.createElement(RightOutlined, null) : /*#__PURE__*/React.createElement(LeftOutlined, null);
|
||||
}
|
||||
return isRight ? /*#__PURE__*/React.createElement(LeftOutlined, null) : /*#__PURE__*/React.createElement(RightOutlined, null);
|
||||
}
|
||||
const Action = ({
|
||||
type,
|
||||
actions,
|
||||
moveToLeft,
|
||||
moveToRight,
|
||||
leftActive,
|
||||
rightActive,
|
||||
direction,
|
||||
disabled
|
||||
}) => {
|
||||
const isRight = type === 'right';
|
||||
const button = isRight ? actions[0] : actions[1];
|
||||
const moveHandler = isRight ? moveToRight : moveToLeft;
|
||||
const active = isRight ? rightActive : leftActive;
|
||||
const icon = getArrowIcon(type, direction);
|
||||
if (/*#__PURE__*/React.isValidElement(button)) {
|
||||
const element = button;
|
||||
const onClick = event => {
|
||||
element?.props?.onClick?.(event);
|
||||
moveHandler?.(event);
|
||||
};
|
||||
return /*#__PURE__*/React.cloneElement(element, {
|
||||
disabled: disabled || !active,
|
||||
onClick
|
||||
});
|
||||
}
|
||||
return /*#__PURE__*/React.createElement(Button, {
|
||||
type: "primary",
|
||||
size: "small",
|
||||
disabled: disabled || !active,
|
||||
onClick: event => moveHandler?.(event),
|
||||
icon: icon
|
||||
}, button);
|
||||
};
|
||||
const Actions = props => {
|
||||
const {
|
||||
className,
|
||||
style,
|
||||
oneWay,
|
||||
actions,
|
||||
...restProps
|
||||
} = props;
|
||||
return /*#__PURE__*/React.createElement("div", {
|
||||
className: className,
|
||||
style: style
|
||||
}, /*#__PURE__*/React.createElement(Action, {
|
||||
type: "right",
|
||||
actions: actions,
|
||||
...restProps
|
||||
}), !oneWay && /*#__PURE__*/React.createElement(Action, {
|
||||
type: "left",
|
||||
actions: actions,
|
||||
...restProps
|
||||
}), actions.slice(oneWay ? 1 : 2));
|
||||
};
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
Actions.displayName = 'Actions';
|
||||
}
|
||||
export default Actions;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import type { KeyWiseTransferItem } from '.';
|
||||
import type { TransferKey } from './interface';
|
||||
import type { RenderedItem, TransferListProps } from './Section';
|
||||
export declare const OmitProps: readonly ["handleFilter", "handleClear", "checkedKeys"];
|
||||
export type OmitProp = (typeof OmitProps)[number];
|
||||
type PartialTransferListProps<RecordType> = Omit<TransferListProps<RecordType>, OmitProp>;
|
||||
export interface TransferListBodyProps<RecordType> extends PartialTransferListProps<RecordType> {
|
||||
filteredItems: RecordType[];
|
||||
filteredRenderItems: RenderedItem<RecordType>[];
|
||||
selectedKeys: TransferKey[];
|
||||
}
|
||||
export interface ListBodyRef<RecordType extends KeyWiseTransferItem> {
|
||||
items?: RenderedItem<RecordType>[];
|
||||
}
|
||||
declare const _default: React.ForwardRefExoticComponent<TransferListBodyProps<KeyWiseTransferItem> & React.RefAttributes<ListBodyRef<KeyWiseTransferItem>>>;
|
||||
export default _default;
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
|
||||
import * as React from 'react';
|
||||
import { useControlledState } from '@rc-component/util';
|
||||
import { clsx } from 'clsx';
|
||||
import { isPlainObject } from '../_util/is';
|
||||
import Pagination from '../pagination';
|
||||
import ListItem from './ListItem';
|
||||
export const OmitProps = ['handleFilter', 'handleClear', 'checkedKeys'];
|
||||
const parsePagination = pagination => {
|
||||
const defaultPagination = {
|
||||
simple: true,
|
||||
showSizeChanger: false,
|
||||
showLessItems: false
|
||||
};
|
||||
return {
|
||||
...defaultPagination,
|
||||
...pagination
|
||||
};
|
||||
};
|
||||
const TransferListBody = (props, ref) => {
|
||||
const {
|
||||
prefixCls,
|
||||
classNames,
|
||||
styles,
|
||||
filteredRenderItems,
|
||||
selectedKeys,
|
||||
disabled: globalDisabled,
|
||||
showRemove,
|
||||
pagination,
|
||||
onScroll,
|
||||
onItemSelect,
|
||||
onItemRemove
|
||||
} = props;
|
||||
const [current, setCurrent] = React.useState(1);
|
||||
const mergedPagination = React.useMemo(() => {
|
||||
if (!pagination) {
|
||||
return null;
|
||||
}
|
||||
const convertPagination = isPlainObject(pagination) ? pagination : {};
|
||||
return parsePagination(convertPagination);
|
||||
}, [pagination]);
|
||||
const [pageSize, setPageSize] = useControlledState(10, mergedPagination?.pageSize);
|
||||
React.useEffect(() => {
|
||||
if (mergedPagination) {
|
||||
const maxPageCount = Math.ceil(filteredRenderItems.length / pageSize);
|
||||
setCurrent(Math.min(current, maxPageCount));
|
||||
}
|
||||
}, [filteredRenderItems, mergedPagination, pageSize]);
|
||||
const onInternalClick = (item, e) => {
|
||||
onItemSelect(item.key, !selectedKeys.includes(item.key), e);
|
||||
};
|
||||
const onRemove = item => {
|
||||
onItemRemove?.([item.key]);
|
||||
};
|
||||
const onPageChange = cur => {
|
||||
setCurrent(cur);
|
||||
};
|
||||
const onSizeChange = (cur, size) => {
|
||||
setCurrent(cur);
|
||||
setPageSize(size);
|
||||
};
|
||||
const memoizedItems = React.useMemo(() => {
|
||||
const displayItems = mergedPagination ? filteredRenderItems.slice((current - 1) * pageSize, current * pageSize) : filteredRenderItems;
|
||||
return displayItems;
|
||||
}, [current, filteredRenderItems, mergedPagination, pageSize]);
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
items: memoizedItems
|
||||
}));
|
||||
const paginationNode = mergedPagination ? (/*#__PURE__*/React.createElement(Pagination, {
|
||||
size: "small",
|
||||
disabled: globalDisabled,
|
||||
simple: mergedPagination.simple,
|
||||
pageSize: pageSize,
|
||||
showLessItems: mergedPagination.showLessItems,
|
||||
showSizeChanger: mergedPagination.showSizeChanger,
|
||||
className: `${prefixCls}-pagination`,
|
||||
total: filteredRenderItems.length,
|
||||
current: current,
|
||||
onChange: onPageChange,
|
||||
onShowSizeChange: onSizeChange
|
||||
})) : null;
|
||||
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("ul", {
|
||||
className: clsx(`${prefixCls}-content`, classNames.list, {
|
||||
[`${prefixCls}-content-show-remove`]: showRemove
|
||||
}),
|
||||
style: styles.list,
|
||||
onScroll: onScroll
|
||||
}, (memoizedItems || []).map(({
|
||||
renderedEl,
|
||||
renderedText,
|
||||
item
|
||||
}) => (/*#__PURE__*/React.createElement(ListItem, {
|
||||
key: item.key,
|
||||
prefixCls: prefixCls,
|
||||
classNames: classNames,
|
||||
styles: styles,
|
||||
item: item,
|
||||
renderedText: renderedText,
|
||||
renderedEl: renderedEl,
|
||||
showRemove: showRemove,
|
||||
onClick: onInternalClick,
|
||||
onRemove: onRemove,
|
||||
checked: selectedKeys.includes(item.key),
|
||||
disabled: globalDisabled
|
||||
})))), paginationNode);
|
||||
};
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
TransferListBody.displayName = 'TransferListBody';
|
||||
}
|
||||
export default /*#__PURE__*/React.forwardRef(TransferListBody);
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import type { KeyWiseTransferItem, TransferSemanticClassNames, TransferSemanticStyles } from '.';
|
||||
type ListItemProps<RecordType> = {
|
||||
prefixCls: string;
|
||||
classNames: TransferSemanticClassNames;
|
||||
styles: TransferSemanticStyles;
|
||||
renderedText?: string | number;
|
||||
renderedEl: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
checked?: boolean;
|
||||
onClick: (item: RecordType, e: React.MouseEvent<HTMLLIElement, MouseEvent>) => void;
|
||||
onRemove?: (item: RecordType) => void;
|
||||
item: RecordType;
|
||||
showRemove?: boolean;
|
||||
};
|
||||
declare const _default: React.MemoExoticComponent<(<RecordType extends KeyWiseTransferItem>(props: ListItemProps<RecordType>) => React.JSX.Element)>;
|
||||
export default _default;
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
"use client";
|
||||
|
||||
import * as React from 'react';
|
||||
import DeleteOutlined from "@ant-design/icons/es/icons/DeleteOutlined";
|
||||
import { clsx } from 'clsx';
|
||||
import { isNumber } from '../_util/is';
|
||||
import Checkbox from '../checkbox';
|
||||
import { useLocale } from '../locale';
|
||||
import defaultLocale from '../locale/en_US';
|
||||
const ListItem = props => {
|
||||
const {
|
||||
prefixCls,
|
||||
classNames,
|
||||
styles,
|
||||
renderedText,
|
||||
renderedEl,
|
||||
item,
|
||||
checked,
|
||||
disabled,
|
||||
onClick,
|
||||
onRemove,
|
||||
showRemove
|
||||
} = props;
|
||||
const mergedDisabled = disabled || item?.disabled;
|
||||
const classes = clsx(`${prefixCls}-content-item`, classNames.item, {
|
||||
[`${prefixCls}-content-item-disabled`]: mergedDisabled,
|
||||
[`${prefixCls}-content-item-checked`]: checked && !mergedDisabled
|
||||
});
|
||||
let title;
|
||||
if (typeof renderedText === 'string' || isNumber(renderedText)) {
|
||||
title = String(renderedText);
|
||||
}
|
||||
const [contextLocale] = useLocale('Transfer', defaultLocale.Transfer);
|
||||
const liProps = {
|
||||
className: classes,
|
||||
style: styles.item,
|
||||
title
|
||||
};
|
||||
const labelNode = /*#__PURE__*/React.createElement("span", {
|
||||
className: clsx(`${prefixCls}-content-item-text`, classNames.itemContent),
|
||||
style: styles.itemContent
|
||||
}, renderedEl);
|
||||
if (showRemove) {
|
||||
return /*#__PURE__*/React.createElement("li", {
|
||||
...liProps
|
||||
}, labelNode, /*#__PURE__*/React.createElement("button", {
|
||||
type: "button",
|
||||
disabled: mergedDisabled,
|
||||
className: `${prefixCls}-content-item-remove`,
|
||||
"aria-label": contextLocale?.remove,
|
||||
onClick: () => onRemove?.(item)
|
||||
}, /*#__PURE__*/React.createElement(DeleteOutlined, null)));
|
||||
}
|
||||
// Default click to select
|
||||
liProps.onClick = mergedDisabled ? undefined : event => onClick(item, event);
|
||||
return /*#__PURE__*/React.createElement("li", {
|
||||
...liProps
|
||||
}, /*#__PURE__*/React.createElement(Checkbox, {
|
||||
className: clsx(`${prefixCls}-checkbox`, classNames.itemIcon),
|
||||
style: styles.itemIcon,
|
||||
checked: checked,
|
||||
disabled: mergedDisabled
|
||||
}), labelNode);
|
||||
};
|
||||
export default /*#__PURE__*/React.memo(ListItem);
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import type { KeyWiseTransferItem, RenderResult, SelectAllLabel, TransferDirection, TransferLocale, TransferSearchOption, TransferSemanticClassNames, TransferSemanticStyles } from './';
|
||||
import type { PaginationType, TransferKey } from './interface';
|
||||
import type { TransferListBodyProps } from './ListBody';
|
||||
export interface RenderedItem<RecordType> {
|
||||
renderedText: string;
|
||||
renderedEl: React.ReactNode;
|
||||
item: RecordType;
|
||||
}
|
||||
type RenderListFunction<T> = (props: TransferListBodyProps<T>) => React.ReactNode;
|
||||
export interface TransferListProps<RecordType> extends TransferLocale {
|
||||
prefixCls: string;
|
||||
style?: React.CSSProperties;
|
||||
classNames: TransferSemanticClassNames;
|
||||
styles: TransferSemanticStyles;
|
||||
titleText: React.ReactNode;
|
||||
dataSource: RecordType[];
|
||||
filterOption?: (filterText: string, item: RecordType, direction: TransferDirection) => boolean;
|
||||
checkedKeys: TransferKey[];
|
||||
handleFilter: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onItemSelect: (key: TransferKey, check: boolean, e?: React.MouseEvent<Element, MouseEvent>) => void;
|
||||
onItemSelectAll: (dataSource: TransferKey[], checkAll: boolean | 'replace') => void;
|
||||
onItemRemove?: (keys: TransferKey[]) => void;
|
||||
handleClear: () => void;
|
||||
/** Render item */
|
||||
render?: (item: RecordType) => RenderResult;
|
||||
showSearch?: boolean | TransferSearchOption;
|
||||
searchPlaceholder: string;
|
||||
itemUnit: string;
|
||||
itemsUnit: string;
|
||||
renderList?: RenderListFunction<RecordType>;
|
||||
footer?: (props: TransferListProps<RecordType>, info?: {
|
||||
direction: TransferDirection;
|
||||
}) => React.ReactNode;
|
||||
onScroll: (e: React.UIEvent<HTMLUListElement, UIEvent>) => void;
|
||||
disabled?: boolean;
|
||||
direction: TransferDirection;
|
||||
showSelectAll?: boolean;
|
||||
selectAllLabel?: SelectAllLabel;
|
||||
showRemove?: boolean;
|
||||
pagination?: PaginationType;
|
||||
selectionsIcon?: React.ReactNode;
|
||||
}
|
||||
export interface TransferCustomListBodyProps<T> extends TransferListBodyProps<T> {
|
||||
}
|
||||
declare const TransferSection: {
|
||||
<RecordType extends KeyWiseTransferItem>(props: TransferListProps<RecordType>): React.JSX.Element;
|
||||
displayName: string;
|
||||
};
|
||||
export default TransferSection;
|
||||
+306
@@ -0,0 +1,306 @@
|
||||
"use client";
|
||||
|
||||
import _toConsumableArray from "@babel/runtime/helpers/esm/toConsumableArray";
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import DownOutlined from "@ant-design/icons/es/icons/DownOutlined";
|
||||
import { omit } from '@rc-component/util';
|
||||
import { clsx } from 'clsx';
|
||||
import { isNumber, isPlainObject } from '../_util/is';
|
||||
import { groupKeysMap } from '../_util/transKeys';
|
||||
import Checkbox from '../checkbox';
|
||||
import Dropdown from '../dropdown';
|
||||
import DefaultListBody, { OmitProps } from './ListBody';
|
||||
import Search from './search';
|
||||
const defaultRender = () => null;
|
||||
function isRenderResultPlainObject(result) {
|
||||
return !!(result && ! /*#__PURE__*/React.isValidElement(result) && Object.prototype.toString.call(result) === '[object Object]');
|
||||
}
|
||||
function getEnabledItemKeys(items) {
|
||||
return items.filter(data => !data.disabled).map(data => data.key);
|
||||
}
|
||||
function getTextFromRenderResult(renderResult, item) {
|
||||
for (const v of [renderResult, item.title, item.key]) {
|
||||
if (typeof v === 'string' || isNumber(v)) {
|
||||
return String(v);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
const isValidIcon = icon => icon !== undefined;
|
||||
const getShowSearchOption = showSearch => {
|
||||
if (isPlainObject(showSearch)) {
|
||||
return {
|
||||
...showSearch,
|
||||
defaultValue: showSearch.defaultValue || ''
|
||||
};
|
||||
}
|
||||
return {
|
||||
defaultValue: '',
|
||||
placeholder: ''
|
||||
};
|
||||
};
|
||||
const TransferSection = props => {
|
||||
const {
|
||||
prefixCls,
|
||||
style,
|
||||
classNames,
|
||||
styles,
|
||||
dataSource = [],
|
||||
titleText = '',
|
||||
checkedKeys,
|
||||
disabled,
|
||||
showSearch = false,
|
||||
searchPlaceholder,
|
||||
notFoundContent,
|
||||
selectAll,
|
||||
deselectAll,
|
||||
selectCurrent,
|
||||
selectInvert,
|
||||
removeAll,
|
||||
removeCurrent,
|
||||
showSelectAll = true,
|
||||
showRemove,
|
||||
pagination,
|
||||
direction,
|
||||
itemsUnit,
|
||||
itemUnit,
|
||||
selectAllLabel,
|
||||
selectionsIcon,
|
||||
footer,
|
||||
renderList,
|
||||
onItemSelectAll,
|
||||
onItemRemove,
|
||||
handleFilter,
|
||||
handleClear,
|
||||
filterOption,
|
||||
render = defaultRender
|
||||
} = props;
|
||||
const sectionPrefixCls = `${prefixCls}-section`;
|
||||
const listPrefixCls = `${prefixCls}-list`;
|
||||
const searchOptions = getShowSearchOption(showSearch);
|
||||
const [filterValue, setFilterValue] = useState(searchOptions.defaultValue);
|
||||
const listBodyRef = useRef({});
|
||||
const internalHandleFilter = e => {
|
||||
setFilterValue(e.target.value);
|
||||
handleFilter(e);
|
||||
};
|
||||
const internalHandleClear = () => {
|
||||
setFilterValue('');
|
||||
handleClear();
|
||||
};
|
||||
const matchFilter = (text, item) => {
|
||||
if (typeof filterOption === 'function') {
|
||||
return filterOption(filterValue, item, direction);
|
||||
}
|
||||
return text.includes(filterValue);
|
||||
};
|
||||
const customRenderListBody = listProps => {
|
||||
let bodyContent = renderList ? renderList({
|
||||
...listProps,
|
||||
onItemSelect: (key, check) => listProps.onItemSelect(key, check)
|
||||
}) : null;
|
||||
const customize = !!bodyContent;
|
||||
if (!customize) {
|
||||
// @ts-ignore
|
||||
bodyContent = /*#__PURE__*/React.createElement(DefaultListBody, {
|
||||
ref: listBodyRef,
|
||||
...listProps,
|
||||
prefixCls: listPrefixCls
|
||||
});
|
||||
}
|
||||
return {
|
||||
customize,
|
||||
bodyContent
|
||||
};
|
||||
};
|
||||
const renderItem = item => {
|
||||
const renderResult = render(item);
|
||||
const isRenderResultPlain = isRenderResultPlainObject(renderResult);
|
||||
const renderedEl = isRenderResultPlain ? renderResult.label : renderResult;
|
||||
const renderedText = isRenderResultPlain ? renderResult.value : getTextFromRenderResult(renderResult, item);
|
||||
return {
|
||||
item,
|
||||
renderedEl,
|
||||
renderedText
|
||||
};
|
||||
};
|
||||
const notFoundContentEle = useMemo(() => Array.isArray(notFoundContent) ? notFoundContent[direction === 'left' ? 0 : 1] : notFoundContent, [notFoundContent, direction]);
|
||||
const [filteredItems, filteredRenderItems] = useMemo(() => {
|
||||
const filterItems = [];
|
||||
const filterRenderItems = [];
|
||||
dataSource.forEach(item => {
|
||||
const renderedItem = renderItem(item);
|
||||
if (filterValue && !matchFilter(renderedItem.renderedText, item)) {
|
||||
return;
|
||||
}
|
||||
filterItems.push(item);
|
||||
filterRenderItems.push(renderedItem);
|
||||
});
|
||||
return [filterItems, filterRenderItems];
|
||||
}, [dataSource, filterValue]);
|
||||
const checkedActiveItems = useMemo(() => {
|
||||
return filteredItems.filter(item => checkedKeys.includes(item.key) && !item.disabled);
|
||||
}, [checkedKeys, filteredItems]);
|
||||
const checkStatus = useMemo(() => {
|
||||
if (checkedActiveItems.length === 0) {
|
||||
return 'none';
|
||||
}
|
||||
const checkedKeysMap = groupKeysMap(checkedKeys);
|
||||
if (filteredItems.every(item => checkedKeysMap.has(item.key) || !!item.disabled)) {
|
||||
return 'all';
|
||||
}
|
||||
return 'part';
|
||||
}, [checkedActiveItems.length, checkedKeys, filteredItems]);
|
||||
const renderListBody = () => {
|
||||
const search = showSearch ? (/*#__PURE__*/React.createElement("div", {
|
||||
className: `${listPrefixCls}-body-search-wrapper`
|
||||
}, /*#__PURE__*/React.createElement(Search, {
|
||||
prefixCls: `${listPrefixCls}-search`,
|
||||
onChange: internalHandleFilter,
|
||||
handleClear: internalHandleClear,
|
||||
placeholder: searchOptions.placeholder || searchPlaceholder,
|
||||
value: filterValue,
|
||||
disabled: disabled
|
||||
}))) : null;
|
||||
const {
|
||||
customize,
|
||||
bodyContent
|
||||
} = customRenderListBody({
|
||||
...omit(props, OmitProps),
|
||||
filteredItems,
|
||||
filteredRenderItems,
|
||||
selectedKeys: checkedKeys,
|
||||
classNames,
|
||||
styles
|
||||
});
|
||||
let bodyNode;
|
||||
// We should wrap customize list body in a classNamed div to use flex layout.
|
||||
if (customize) {
|
||||
bodyNode = /*#__PURE__*/React.createElement("div", {
|
||||
className: `${listPrefixCls}-body-customize-wrapper`
|
||||
}, bodyContent);
|
||||
} else {
|
||||
bodyNode = filteredItems.length ? bodyContent : (/*#__PURE__*/React.createElement("div", {
|
||||
className: `${listPrefixCls}-body-not-found`
|
||||
}, notFoundContentEle));
|
||||
}
|
||||
return /*#__PURE__*/React.createElement("div", {
|
||||
className: clsx(`${listPrefixCls}-body`, {
|
||||
[`${listPrefixCls}-body-with-search`]: showSearch
|
||||
}, classNames.body),
|
||||
style: styles.body
|
||||
}, search, bodyNode);
|
||||
};
|
||||
const checkBox = /*#__PURE__*/React.createElement(Checkbox, {
|
||||
disabled: dataSource.filter(d => !d.disabled).length === 0 || disabled,
|
||||
checked: checkStatus === 'all',
|
||||
indeterminate: checkStatus === 'part',
|
||||
className: `${listPrefixCls}-checkbox`,
|
||||
onChange: () => {
|
||||
// Only select enabled items
|
||||
onItemSelectAll?.(filteredItems.filter(item => !item.disabled).map(({
|
||||
key
|
||||
}) => key), checkStatus !== 'all');
|
||||
}
|
||||
});
|
||||
const getSelectAllLabel = (selectedCount, totalCount) => {
|
||||
if (selectAllLabel) {
|
||||
return typeof selectAllLabel === 'function' ? selectAllLabel({
|
||||
selectedCount,
|
||||
totalCount
|
||||
}) : selectAllLabel;
|
||||
}
|
||||
const unit = totalCount > 1 ? itemsUnit : itemUnit;
|
||||
return /*#__PURE__*/React.createElement(React.Fragment, null, (selectedCount > 0 ? `${selectedCount}/` : '') + totalCount, " ", unit);
|
||||
};
|
||||
// Custom Layout
|
||||
const footerDom = footer && (footer.length < 2 ? footer(props) : footer(props, {
|
||||
direction
|
||||
}));
|
||||
// Get filtered, checked item list
|
||||
const listFooter = footerDom ? (/*#__PURE__*/React.createElement("div", {
|
||||
className: clsx(`${listPrefixCls}-footer`, classNames.footer),
|
||||
style: styles.footer
|
||||
}, footerDom)) : null;
|
||||
const checkAllCheckbox = !showRemove && !pagination && checkBox;
|
||||
let items;
|
||||
if (showRemove) {
|
||||
items = [/* Remove Current Page */
|
||||
pagination ? {
|
||||
key: 'removeCurrent',
|
||||
label: removeCurrent,
|
||||
onClick() {
|
||||
const pageKeys = getEnabledItemKeys((listBodyRef.current?.items || []).map(entity => entity.item));
|
||||
onItemRemove?.(pageKeys);
|
||||
}
|
||||
} : null, /* Remove All */
|
||||
{
|
||||
key: 'removeAll',
|
||||
label: removeAll,
|
||||
onClick() {
|
||||
onItemRemove?.(getEnabledItemKeys(filteredItems));
|
||||
}
|
||||
}].filter(Boolean);
|
||||
} else {
|
||||
items = [{
|
||||
key: 'selectAll',
|
||||
label: checkStatus === 'all' ? deselectAll : selectAll,
|
||||
onClick() {
|
||||
const keys = getEnabledItemKeys(filteredItems);
|
||||
onItemSelectAll?.(keys, keys.length !== checkedKeys.length);
|
||||
}
|
||||
}, pagination ? {
|
||||
key: 'selectCurrent',
|
||||
label: selectCurrent,
|
||||
onClick() {
|
||||
const pageItems = listBodyRef.current?.items || [];
|
||||
onItemSelectAll?.(getEnabledItemKeys(pageItems.map(entity => entity.item)), true);
|
||||
}
|
||||
} : null, {
|
||||
key: 'selectInvert',
|
||||
label: selectInvert,
|
||||
onClick() {
|
||||
const availablePageItemKeys = getEnabledItemKeys((listBodyRef.current?.items || []).map(entity => entity.item));
|
||||
const checkedKeySet = new Set(checkedKeys);
|
||||
const newCheckedKeysSet = new Set(checkedKeySet);
|
||||
availablePageItemKeys.forEach(key => {
|
||||
if (checkedKeySet.has(key)) {
|
||||
newCheckedKeysSet.delete(key);
|
||||
} else {
|
||||
newCheckedKeysSet.add(key);
|
||||
}
|
||||
});
|
||||
onItemSelectAll?.(_toConsumableArray(newCheckedKeysSet), 'replace');
|
||||
}
|
||||
}];
|
||||
}
|
||||
const dropdown = /*#__PURE__*/React.createElement(Dropdown, {
|
||||
className: `${listPrefixCls}-header-dropdown`,
|
||||
menu: {
|
||||
items
|
||||
},
|
||||
disabled: disabled
|
||||
}, isValidIcon(selectionsIcon) ? selectionsIcon : /*#__PURE__*/React.createElement(DownOutlined, null));
|
||||
return /*#__PURE__*/React.createElement("div", {
|
||||
className: clsx(sectionPrefixCls, classNames.section, {
|
||||
[`${sectionPrefixCls}-with-pagination`]: !!pagination,
|
||||
[`${sectionPrefixCls}-with-footer`]: !!footerDom
|
||||
}),
|
||||
style: {
|
||||
...style,
|
||||
...styles.section
|
||||
}
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: clsx(`${listPrefixCls}-header`, classNames.header),
|
||||
style: styles.header
|
||||
}, showSelectAll ? (/*#__PURE__*/React.createElement(React.Fragment, null, checkAllCheckbox, dropdown)) : null, /*#__PURE__*/React.createElement("span", {
|
||||
className: `${listPrefixCls}-header-selected`
|
||||
}, getSelectAllLabel(checkedActiveItems.length, filteredItems.length)), /*#__PURE__*/React.createElement("span", {
|
||||
className: clsx(`${listPrefixCls}-header-title`, classNames.title),
|
||||
style: styles.title
|
||||
}, titleText)), renderListBody(), listFooter);
|
||||
};
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
TransferSection.displayName = 'TransferSection';
|
||||
}
|
||||
export default TransferSection;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import type { TransferProps } from '..';
|
||||
import type { AnyObject } from '../../_util/type';
|
||||
import type { TransferKey } from '../interface';
|
||||
declare const useData: <RecordType extends AnyObject>(dataSource?: RecordType[], rowKey?: TransferProps<RecordType>["rowKey"], targetKeys?: TransferKey[]) => RecordType[][];
|
||||
export default useData;
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import * as React from 'react';
|
||||
import { groupKeysMap } from '../../_util/transKeys';
|
||||
const useData = (dataSource, rowKey, targetKeys) => {
|
||||
const mergedDataSource = React.useMemo(() => (dataSource || []).map(record => {
|
||||
if (rowKey) {
|
||||
return {
|
||||
...record,
|
||||
key: rowKey(record)
|
||||
};
|
||||
}
|
||||
return record;
|
||||
}), [dataSource, rowKey]);
|
||||
const [leftDataSource, rightDataSource] = React.useMemo(() => {
|
||||
const leftData = [];
|
||||
const rightData = Array.from({
|
||||
length: targetKeys?.length ?? 0
|
||||
});
|
||||
const targetKeysMap = groupKeysMap(targetKeys || []);
|
||||
mergedDataSource.forEach(record => {
|
||||
// rightData should be ordered by targetKeys
|
||||
// leftData should be ordered by dataSource
|
||||
if (targetKeysMap.has(record.key)) {
|
||||
const idx = targetKeysMap.get(record.key);
|
||||
rightData[idx] = record;
|
||||
} else {
|
||||
leftData.push(record);
|
||||
}
|
||||
});
|
||||
return [leftData, rightData];
|
||||
}, [mergedDataSource, targetKeys]);
|
||||
return [mergedDataSource, leftDataSource.filter(Boolean), rightDataSource.filter(Boolean)];
|
||||
};
|
||||
export default useData;
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import type { TransferKey } from '../interface';
|
||||
declare function useSelection<T extends {
|
||||
key: TransferKey;
|
||||
}>(leftDataSource: T[], rightDataSource: T[], selectedKeys?: TransferKey[]): [
|
||||
sourceSelectedKeys: TransferKey[],
|
||||
targetSelectedKeys: TransferKey[],
|
||||
setSourceSelectedKeys: (srcKeys: TransferKey[]) => void,
|
||||
setTargetSelectedKeys: (srcKeys: TransferKey[]) => void
|
||||
];
|
||||
export default useSelection;
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import _toConsumableArray from "@babel/runtime/helpers/esm/toConsumableArray";
|
||||
import * as React from 'react';
|
||||
import { useControlledState, useEvent } from '@rc-component/util';
|
||||
const EMPTY_KEYS = [];
|
||||
function filterKeys(keys, dataKeys) {
|
||||
const filteredKeys = keys.filter(key => dataKeys.has(key));
|
||||
return keys.length === filteredKeys.length ? keys : filteredKeys;
|
||||
}
|
||||
function flattenKeys(keys) {
|
||||
return Array.from(keys).join(';');
|
||||
}
|
||||
function useSelection(leftDataSource, rightDataSource, selectedKeys) {
|
||||
// Prepare `dataSource` keys
|
||||
const [leftKeys, rightKeys] = React.useMemo(() => [new Set(leftDataSource.map(src => src?.key)), new Set(rightDataSource.map(src => src?.key))], [leftDataSource, rightDataSource]);
|
||||
// Selected Keys
|
||||
const [mergedSelectedKeys, setMergedSelectedKeys] = useControlledState(EMPTY_KEYS, selectedKeys);
|
||||
const sourceSelectedKeys = React.useMemo(() => filterKeys(mergedSelectedKeys, leftKeys), [mergedSelectedKeys, leftKeys]);
|
||||
const targetSelectedKeys = React.useMemo(() => filterKeys(mergedSelectedKeys, rightKeys), [mergedSelectedKeys, rightKeys]);
|
||||
// // Reset when data changed
|
||||
React.useEffect(() => {
|
||||
setMergedSelectedKeys([].concat(_toConsumableArray(filterKeys(mergedSelectedKeys, leftKeys)), _toConsumableArray(filterKeys(mergedSelectedKeys, rightKeys))));
|
||||
}, [flattenKeys(leftKeys), flattenKeys(rightKeys)]);
|
||||
// Update keys
|
||||
const setSourceSelectedKeys = useEvent(nextSrcKeys => {
|
||||
setMergedSelectedKeys([].concat(_toConsumableArray(nextSrcKeys), _toConsumableArray(targetSelectedKeys)));
|
||||
});
|
||||
const setTargetSelectedKeys = useEvent(nextTargetKeys => {
|
||||
setMergedSelectedKeys([].concat(_toConsumableArray(sourceSelectedKeys), _toConsumableArray(nextTargetKeys)));
|
||||
});
|
||||
return [
|
||||
// Keys
|
||||
sourceSelectedKeys, targetSelectedKeys,
|
||||
// Updater
|
||||
setSourceSelectedKeys, setTargetSelectedKeys];
|
||||
}
|
||||
export default useSelection;
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
import React from 'react';
|
||||
import type { SemanticClassNamesType, SemanticStylesType } from '../_util/hooks';
|
||||
import type { InputStatus } from '../_util/statusUtils';
|
||||
import type { PaginationType, TransferKey } from './interface';
|
||||
import type { TransferCustomListBodyProps, TransferListProps } from './Section';
|
||||
export type { TransferOperationProps } from './Actions';
|
||||
export type { TransferSearchProps } from './search';
|
||||
export type { TransferListProps } from './Section';
|
||||
export type TransferSemanticName = keyof TransferSemanticClassNames & keyof TransferSemanticStyles;
|
||||
export type TransferSemanticClassNames = {
|
||||
root?: string;
|
||||
section?: string;
|
||||
header?: string;
|
||||
title?: string;
|
||||
body?: string;
|
||||
list?: string;
|
||||
item?: string;
|
||||
itemIcon?: string;
|
||||
itemContent?: string;
|
||||
footer?: string;
|
||||
actions?: string;
|
||||
};
|
||||
export type TransferSemanticStyles = {
|
||||
root?: React.CSSProperties;
|
||||
section?: React.CSSProperties;
|
||||
header?: React.CSSProperties;
|
||||
title?: React.CSSProperties;
|
||||
body?: React.CSSProperties;
|
||||
list?: React.CSSProperties;
|
||||
item?: React.CSSProperties;
|
||||
itemIcon?: React.CSSProperties;
|
||||
itemContent?: React.CSSProperties;
|
||||
footer?: React.CSSProperties;
|
||||
actions?: React.CSSProperties;
|
||||
};
|
||||
export type TransferClassNamesType = SemanticClassNamesType<TransferProps, TransferSemanticClassNames>;
|
||||
export type TransferStylesType = SemanticStylesType<TransferProps, TransferSemanticStyles>;
|
||||
export type TransferDirection = 'left' | 'right';
|
||||
export interface RenderResultObject {
|
||||
label: React.ReactElement;
|
||||
value: string;
|
||||
}
|
||||
export type RenderResult = React.ReactElement | RenderResultObject | string | null;
|
||||
export interface TransferItem {
|
||||
key?: TransferKey;
|
||||
title?: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
[name: string]: any;
|
||||
}
|
||||
export type KeyWise<T> = T & {
|
||||
key: TransferKey;
|
||||
};
|
||||
export type KeyWiseTransferItem = KeyWise<TransferItem>;
|
||||
type TransferRender<RecordType> = (item: RecordType) => RenderResult;
|
||||
export interface ListStyle {
|
||||
direction: TransferDirection;
|
||||
}
|
||||
export type SelectAllLabel = React.ReactNode | ((info: {
|
||||
selectedCount: number;
|
||||
totalCount: number;
|
||||
}) => React.ReactNode);
|
||||
export interface TransferLocale {
|
||||
titles?: React.ReactNode[];
|
||||
notFoundContent?: React.ReactNode | React.ReactNode[];
|
||||
searchPlaceholder: string;
|
||||
itemUnit: string;
|
||||
itemsUnit: string;
|
||||
remove?: string;
|
||||
selectAll?: string;
|
||||
deselectAll?: string;
|
||||
selectCurrent?: string;
|
||||
selectInvert?: string;
|
||||
removeAll?: string;
|
||||
removeCurrent?: string;
|
||||
}
|
||||
export interface TransferSearchOption {
|
||||
placeholder?: string;
|
||||
defaultValue?: string;
|
||||
}
|
||||
export interface TransferProps<RecordType = any> {
|
||||
prefixCls?: string;
|
||||
className?: string;
|
||||
rootClassName?: string;
|
||||
style?: React.CSSProperties;
|
||||
/** @deprecated Please use `styles.section` instead. */
|
||||
listStyle?: ((style: ListStyle) => CSSProperties) | CSSProperties;
|
||||
/** @deprecated Please use `styles.actions` instead. */
|
||||
operationStyle?: CSSProperties;
|
||||
classNames?: TransferClassNamesType;
|
||||
styles?: TransferStylesType;
|
||||
disabled?: boolean;
|
||||
dataSource?: RecordType[];
|
||||
targetKeys?: TransferKey[];
|
||||
selectedKeys?: TransferKey[];
|
||||
render?: TransferRender<RecordType>;
|
||||
onChange?: (targetKeys: TransferKey[], direction: TransferDirection, moveKeys: TransferKey[]) => void;
|
||||
onSelectChange?: (sourceSelectedKeys: TransferKey[], targetSelectedKeys: TransferKey[]) => void;
|
||||
titles?: React.ReactNode[];
|
||||
/** @deprecated Please use `actions` instead. */
|
||||
operations?: string[];
|
||||
actions?: React.ReactNode[];
|
||||
showSearch?: boolean | TransferSearchOption;
|
||||
filterOption?: (inputValue: string, item: RecordType, direction: TransferDirection) => boolean;
|
||||
locale?: Partial<TransferLocale>;
|
||||
footer?: (props: TransferListProps<RecordType>, info?: {
|
||||
direction: TransferDirection;
|
||||
}) => React.ReactNode;
|
||||
rowKey?: (record: RecordType) => TransferKey;
|
||||
onSearch?: (direction: TransferDirection, value: string) => void;
|
||||
onScroll?: (direction: TransferDirection, e: React.SyntheticEvent<HTMLUListElement>) => void;
|
||||
children?: (props: TransferCustomListBodyProps<RecordType>) => React.ReactNode;
|
||||
showSelectAll?: boolean;
|
||||
selectAllLabels?: SelectAllLabel[];
|
||||
oneWay?: boolean;
|
||||
pagination?: PaginationType;
|
||||
status?: InputStatus;
|
||||
selectionsIcon?: React.ReactNode;
|
||||
}
|
||||
declare const Transfer: {
|
||||
<RecordType extends TransferItem = TransferItem>(props: TransferProps<RecordType>): React.JSX.Element;
|
||||
displayName: string;
|
||||
List: {
|
||||
<RecordType extends KeyWiseTransferItem>(props: TransferListProps<RecordType>): React.JSX.Element;
|
||||
displayName: string;
|
||||
};
|
||||
Search: React.FC<import("./search").TransferSearchProps>;
|
||||
Operation: React.FC<import("./Actions").TransferOperationProps>;
|
||||
};
|
||||
export default Transfer;
|
||||
+332
@@ -0,0 +1,332 @@
|
||||
"use client";
|
||||
|
||||
import _toConsumableArray from "@babel/runtime/helpers/esm/toConsumableArray";
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
import { useMergeSemantic, useMultipleSelect } from '../_util/hooks';
|
||||
import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils';
|
||||
import { groupDisabledKeysMap, groupKeysMap } from '../_util/transKeys';
|
||||
import { devUseWarning } from '../_util/warning';
|
||||
import { useComponentConfig } from '../config-provider/context';
|
||||
import DefaultRenderEmpty from '../config-provider/defaultRenderEmpty';
|
||||
import DisabledContext from '../config-provider/DisabledContext';
|
||||
import { FormItemInputContext } from '../form/context';
|
||||
import { useLocale } from '../locale';
|
||||
import defaultLocale from '../locale/en_US';
|
||||
import Actions from './Actions';
|
||||
import useData from './hooks/useData';
|
||||
import useSelection from './hooks/useSelection';
|
||||
import Search from './search';
|
||||
import Section from './Section';
|
||||
import useStyle from './style';
|
||||
const Transfer = props => {
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
className,
|
||||
rootClassName,
|
||||
classNames,
|
||||
styles,
|
||||
style,
|
||||
listStyle,
|
||||
operationStyle,
|
||||
operations,
|
||||
actions,
|
||||
dataSource,
|
||||
targetKeys = [],
|
||||
selectedKeys,
|
||||
selectAllLabels = [],
|
||||
locale = {},
|
||||
titles,
|
||||
disabled,
|
||||
showSearch = false,
|
||||
showSelectAll,
|
||||
oneWay,
|
||||
pagination,
|
||||
status: customStatus,
|
||||
selectionsIcon,
|
||||
filterOption,
|
||||
render,
|
||||
footer,
|
||||
children,
|
||||
rowKey,
|
||||
onScroll,
|
||||
onChange,
|
||||
onSearch,
|
||||
onSelectChange
|
||||
} = props;
|
||||
const {
|
||||
getPrefixCls,
|
||||
renderEmpty,
|
||||
direction: dir,
|
||||
className: contextClassName,
|
||||
style: contextStyle,
|
||||
classNames: contextClassNames,
|
||||
styles: contextStyles,
|
||||
selectionsIcon: contextSelectionsIcon
|
||||
} = useComponentConfig('transfer');
|
||||
const contextDisabled = useContext(DisabledContext);
|
||||
const mergedDisabled = disabled ?? contextDisabled;
|
||||
// =========== Merged Props for Semantic ==========
|
||||
const mergedProps = {
|
||||
...props,
|
||||
disabled: mergedDisabled
|
||||
};
|
||||
const prefixCls = getPrefixCls('transfer', customizePrefixCls);
|
||||
const [hashId, cssVarCls] = useStyle(prefixCls);
|
||||
const mergedActions = actions || operations || [];
|
||||
const isRtl = dir === 'rtl';
|
||||
// Fill record with `key`
|
||||
const [mergedDataSource, leftDataSource, rightDataSource] = useData(dataSource, rowKey, targetKeys);
|
||||
// Get direction selected keys
|
||||
const [
|
||||
// Keys
|
||||
sourceSelectedKeys, targetSelectedKeys,
|
||||
// Setters
|
||||
setSourceSelectedKeys, setTargetSelectedKeys] = useSelection(leftDataSource, rightDataSource, selectedKeys);
|
||||
const [leftMultipleSelect, updateLeftPrevSelectedIndex] = useMultipleSelect(item => item.key);
|
||||
const [rightMultipleSelect, updateRightPrevSelectedIndex] = useMultipleSelect(item => item.key);
|
||||
const setStateKeys = useCallback((direction, keys) => {
|
||||
if (direction === 'left') {
|
||||
const nextKeys = typeof keys === 'function' ? keys(sourceSelectedKeys || []) : keys;
|
||||
setSourceSelectedKeys(nextKeys);
|
||||
} else {
|
||||
const nextKeys = typeof keys === 'function' ? keys(targetSelectedKeys || []) : keys;
|
||||
setTargetSelectedKeys(nextKeys);
|
||||
}
|
||||
}, [sourceSelectedKeys, targetSelectedKeys]);
|
||||
const setPrevSelectedIndex = (direction, value) => {
|
||||
const isLeftDirection = direction === 'left';
|
||||
const updatePrevSelectedIndex = isLeftDirection ? updateLeftPrevSelectedIndex : updateRightPrevSelectedIndex;
|
||||
updatePrevSelectedIndex(value);
|
||||
};
|
||||
const handleSelectChange = useCallback((direction, holder) => {
|
||||
if (direction === 'left') {
|
||||
onSelectChange?.(holder, targetSelectedKeys);
|
||||
} else {
|
||||
onSelectChange?.(sourceSelectedKeys, holder);
|
||||
}
|
||||
}, [sourceSelectedKeys, targetSelectedKeys]);
|
||||
const getTitles = transferLocale => titles ?? transferLocale.titles ?? [];
|
||||
const handleLeftScroll = e => {
|
||||
onScroll?.('left', e);
|
||||
};
|
||||
const handleRightScroll = e => {
|
||||
onScroll?.('right', e);
|
||||
};
|
||||
const moveTo = direction => {
|
||||
const moveKeys = direction === 'right' ? sourceSelectedKeys : targetSelectedKeys;
|
||||
const dataSourceDisabledKeysMap = groupDisabledKeysMap(mergedDataSource);
|
||||
// filter the disabled options
|
||||
const newMoveKeys = moveKeys.filter(key => !dataSourceDisabledKeysMap.has(key));
|
||||
const newMoveKeysMap = groupKeysMap(newMoveKeys);
|
||||
// move items to target box
|
||||
const newTargetKeys = direction === 'right' ? newMoveKeys.concat(targetKeys) : targetKeys.filter(targetKey => !newMoveKeysMap.has(targetKey));
|
||||
// empty checked keys
|
||||
const oppositeDirection = direction === 'right' ? 'left' : 'right';
|
||||
setStateKeys(oppositeDirection, []);
|
||||
handleSelectChange(oppositeDirection, []);
|
||||
onChange?.(newTargetKeys, direction, newMoveKeys);
|
||||
};
|
||||
const moveToLeft = () => {
|
||||
moveTo('left');
|
||||
setPrevSelectedIndex('left', null);
|
||||
};
|
||||
const moveToRight = () => {
|
||||
moveTo('right');
|
||||
setPrevSelectedIndex('right', null);
|
||||
};
|
||||
const onItemSelectAll = (direction, keys, checkAll) => {
|
||||
setStateKeys(direction, prevKeys => {
|
||||
let mergedCheckedKeys = [];
|
||||
if (checkAll === 'replace') {
|
||||
mergedCheckedKeys = keys;
|
||||
} else if (checkAll) {
|
||||
// Merge current keys with origin key
|
||||
mergedCheckedKeys = Array.from(new Set([].concat(_toConsumableArray(prevKeys), _toConsumableArray(keys))));
|
||||
} else {
|
||||
const selectedKeysMap = groupKeysMap(keys);
|
||||
// Remove current keys from origin keys
|
||||
mergedCheckedKeys = prevKeys.filter(key => !selectedKeysMap.has(key));
|
||||
}
|
||||
handleSelectChange(direction, mergedCheckedKeys);
|
||||
return mergedCheckedKeys;
|
||||
});
|
||||
setPrevSelectedIndex(direction, null);
|
||||
};
|
||||
const onLeftItemSelectAll = (keys, checkAll) => onItemSelectAll('left', keys, checkAll);
|
||||
const onRightItemSelectAll = (keys, checkAll) => onItemSelectAll('right', keys, checkAll);
|
||||
const leftFilter = e => onSearch?.('left', e.target.value);
|
||||
const rightFilter = e => onSearch?.('right', e.target.value);
|
||||
const handleLeftClear = () => onSearch?.('left', '');
|
||||
const handleRightClear = () => onSearch?.('right', '');
|
||||
const handleSingleSelect = (direction, holder, selectedKey, checked, currentSelectedIndex) => {
|
||||
const isSelected = holder.has(selectedKey);
|
||||
if (isSelected) {
|
||||
holder.delete(selectedKey);
|
||||
setPrevSelectedIndex(direction, null);
|
||||
}
|
||||
if (checked) {
|
||||
holder.add(selectedKey);
|
||||
setPrevSelectedIndex(direction, currentSelectedIndex);
|
||||
}
|
||||
};
|
||||
const handleMultipleSelect = (direction, data, holder, currentSelectedIndex) => {
|
||||
const isLeftDirection = direction === 'left';
|
||||
const multipleSelect = isLeftDirection ? leftMultipleSelect : rightMultipleSelect;
|
||||
multipleSelect(currentSelectedIndex, data, holder);
|
||||
};
|
||||
const onItemSelect = (direction, selectedKey, checked, multiple) => {
|
||||
const isLeftDirection = direction === 'left';
|
||||
const holder = isLeftDirection ? sourceSelectedKeys : targetSelectedKeys;
|
||||
const holderSet = new Set(holder);
|
||||
const data = (isLeftDirection ? leftDataSource : rightDataSource).filter(item => !item.disabled);
|
||||
const currentSelectedIndex = data.findIndex(item => item.key === selectedKey);
|
||||
// multiple select by hold down the shift key
|
||||
if (multiple && holder.length > 0) {
|
||||
handleMultipleSelect(direction, data, holderSet, currentSelectedIndex);
|
||||
} else {
|
||||
handleSingleSelect(direction, holderSet, selectedKey, checked, currentSelectedIndex);
|
||||
}
|
||||
const holderArr = Array.from(holderSet);
|
||||
handleSelectChange(direction, holderArr);
|
||||
if (!props.selectedKeys) {
|
||||
setStateKeys(direction, holderArr);
|
||||
}
|
||||
};
|
||||
const onLeftItemSelect = (selectedKey, checked, e) => {
|
||||
onItemSelect('left', selectedKey, checked, e?.shiftKey);
|
||||
};
|
||||
const onRightItemSelect = (selectedKey, checked, e) => onItemSelect('right', selectedKey, checked, e?.shiftKey);
|
||||
const onRightItemRemove = keys => {
|
||||
setStateKeys('right', []);
|
||||
onChange?.(targetKeys.filter(key => !keys.includes(key)), 'left', _toConsumableArray(keys));
|
||||
};
|
||||
const handleListStyle = direction => {
|
||||
if (typeof listStyle === 'function') {
|
||||
return listStyle({
|
||||
direction
|
||||
});
|
||||
}
|
||||
return listStyle || {};
|
||||
};
|
||||
const formItemContext = useContext(FormItemInputContext);
|
||||
const {
|
||||
hasFeedback,
|
||||
status
|
||||
} = formItemContext;
|
||||
const getLocale = transferLocale => ({
|
||||
...transferLocale,
|
||||
notFoundContent: renderEmpty?.('Transfer') || /*#__PURE__*/React.createElement(DefaultRenderEmpty, {
|
||||
componentName: "Transfer"
|
||||
}),
|
||||
...locale
|
||||
});
|
||||
const mergedStatus = getMergedStatus(status, customStatus);
|
||||
const mergedPagination = !children && pagination;
|
||||
const leftActive = rightDataSource.filter(d => targetSelectedKeys.includes(d.key) && !d.disabled).length > 0;
|
||||
const rightActive = leftDataSource.filter(d => sourceSelectedKeys.includes(d.key) && !d.disabled).length > 0;
|
||||
// ====================== Styles ======================
|
||||
const [mergedClassNames, mergedStyles] = useMergeSemantic([contextClassNames, classNames], [contextStyles, styles], {
|
||||
props: mergedProps
|
||||
});
|
||||
const cls = clsx(prefixCls, {
|
||||
[`${prefixCls}-disabled`]: mergedDisabled,
|
||||
[`${prefixCls}-customize-list`]: !!children,
|
||||
[`${prefixCls}-rtl`]: isRtl
|
||||
}, getStatusClassNames(prefixCls, mergedStatus, hasFeedback), contextClassName, className, rootClassName, hashId, cssVarCls, mergedClassNames.root);
|
||||
// ====================== Locale ======================
|
||||
const [contextLocale] = useLocale('Transfer', defaultLocale.Transfer);
|
||||
const listLocale = getLocale(contextLocale);
|
||||
const [leftTitle, rightTitle] = getTitles(listLocale);
|
||||
const mergedSelectionsIcon = selectionsIcon ?? contextSelectionsIcon;
|
||||
// ===================== Warning ======================
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const warning = devUseWarning('Transfer');
|
||||
process.env.NODE_ENV !== "production" ? warning(!pagination || !children, 'usage', '`pagination` not support customize render list.') : void 0;
|
||||
[['listStyle', 'styles.section'], ['operationStyle', 'styles.actions'], ['operations', 'actions']].forEach(([deprecatedName, newName]) => {
|
||||
warning.deprecated(!(deprecatedName in props), deprecatedName, newName);
|
||||
});
|
||||
}
|
||||
// ====================== Render ======================
|
||||
return /*#__PURE__*/React.createElement("div", {
|
||||
className: cls,
|
||||
style: {
|
||||
...contextStyle,
|
||||
...mergedStyles.root,
|
||||
...style
|
||||
}
|
||||
}, /*#__PURE__*/React.createElement(Section, {
|
||||
prefixCls: prefixCls,
|
||||
style: handleListStyle('left'),
|
||||
classNames: mergedClassNames,
|
||||
styles: mergedStyles,
|
||||
titleText: leftTitle,
|
||||
dataSource: leftDataSource,
|
||||
filterOption: filterOption,
|
||||
checkedKeys: sourceSelectedKeys,
|
||||
handleFilter: leftFilter,
|
||||
handleClear: handleLeftClear,
|
||||
onItemSelect: onLeftItemSelect,
|
||||
onItemSelectAll: onLeftItemSelectAll,
|
||||
render: render,
|
||||
showSearch: showSearch,
|
||||
renderList: children,
|
||||
footer: footer,
|
||||
onScroll: handleLeftScroll,
|
||||
disabled: mergedDisabled,
|
||||
direction: isRtl ? 'right' : 'left',
|
||||
showSelectAll: showSelectAll,
|
||||
selectAllLabel: selectAllLabels[0],
|
||||
pagination: mergedPagination,
|
||||
selectionsIcon: mergedSelectionsIcon,
|
||||
...listLocale
|
||||
}), /*#__PURE__*/React.createElement(Actions, {
|
||||
className: clsx(`${prefixCls}-actions`, mergedClassNames.actions),
|
||||
rightActive: rightActive,
|
||||
moveToRight: moveToRight,
|
||||
leftActive: leftActive,
|
||||
actions: mergedActions,
|
||||
moveToLeft: moveToLeft,
|
||||
style: {
|
||||
...operationStyle,
|
||||
...mergedStyles.actions
|
||||
},
|
||||
disabled: mergedDisabled,
|
||||
direction: dir,
|
||||
oneWay: oneWay
|
||||
}), /*#__PURE__*/React.createElement(Section, {
|
||||
prefixCls: prefixCls,
|
||||
style: handleListStyle('right'),
|
||||
classNames: mergedClassNames,
|
||||
styles: mergedStyles,
|
||||
titleText: rightTitle,
|
||||
dataSource: rightDataSource,
|
||||
filterOption: filterOption,
|
||||
checkedKeys: targetSelectedKeys,
|
||||
handleFilter: rightFilter,
|
||||
handleClear: handleRightClear,
|
||||
onItemSelect: onRightItemSelect,
|
||||
onItemSelectAll: onRightItemSelectAll,
|
||||
onItemRemove: onRightItemRemove,
|
||||
render: render,
|
||||
showSearch: showSearch,
|
||||
renderList: children,
|
||||
footer: footer,
|
||||
onScroll: handleRightScroll,
|
||||
disabled: mergedDisabled,
|
||||
direction: isRtl ? 'left' : 'right',
|
||||
showSelectAll: showSelectAll,
|
||||
selectAllLabel: selectAllLabels[1],
|
||||
showRemove: oneWay,
|
||||
pagination: mergedPagination,
|
||||
selectionsIcon: mergedSelectionsIcon,
|
||||
...listLocale
|
||||
}));
|
||||
};
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
Transfer.displayName = 'Transfer';
|
||||
}
|
||||
Transfer.List = Section;
|
||||
Transfer.Search = Search;
|
||||
Transfer.Operation = Actions;
|
||||
export default Transfer;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
export type TransferKey = React.Key;
|
||||
export type PaginationType = boolean | {
|
||||
pageSize?: number;
|
||||
simple?: boolean;
|
||||
showSizeChanger?: boolean;
|
||||
showLessItems?: boolean;
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import * as React from 'react';
|
||||
export interface TransferSearchProps {
|
||||
prefixCls?: string;
|
||||
placeholder?: string;
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
handleClear?: () => void;
|
||||
value?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
declare const Search: React.FC<TransferSearchProps>;
|
||||
export default Search;
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import * as React from 'react';
|
||||
import SearchOutlined from "@ant-design/icons/es/icons/SearchOutlined";
|
||||
import Input from '../input/Input';
|
||||
const Search = props => {
|
||||
const {
|
||||
placeholder = '',
|
||||
value,
|
||||
prefixCls,
|
||||
disabled,
|
||||
onChange,
|
||||
handleClear
|
||||
} = props;
|
||||
const handleChange = React.useCallback(e => {
|
||||
onChange?.(e);
|
||||
if (e.target.value === '') {
|
||||
handleClear?.();
|
||||
}
|
||||
}, [onChange]);
|
||||
return /*#__PURE__*/React.createElement(Input, {
|
||||
placeholder: placeholder,
|
||||
className: prefixCls,
|
||||
value: value,
|
||||
onChange: handleChange,
|
||||
disabled: disabled,
|
||||
allowClear: true,
|
||||
prefix: /*#__PURE__*/React.createElement(SearchOutlined, null)
|
||||
});
|
||||
};
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
Search.displayName = 'Search';
|
||||
}
|
||||
export default Search;
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import type { GetDefaultToken } from '../../theme/internal';
|
||||
export interface ComponentToken {
|
||||
/**
|
||||
* @desc 列表宽度
|
||||
* @descEN Width of list
|
||||
*/
|
||||
listWidth: number | string;
|
||||
/**
|
||||
* @desc 大号列表宽度
|
||||
* @descEN Width of large list
|
||||
*/
|
||||
listWidthLG: number | string;
|
||||
/**
|
||||
* @desc 列表高度
|
||||
* @descEN Height of list
|
||||
*/
|
||||
listHeight: number | string;
|
||||
/**
|
||||
* @desc 列表项高度
|
||||
* @descEN Height of list item
|
||||
*/
|
||||
itemHeight: number | string;
|
||||
/**
|
||||
* @desc 列表项纵向内边距
|
||||
* @descEN Vertical padding of list item
|
||||
*/
|
||||
itemPaddingBlock: number | string;
|
||||
/**
|
||||
* @desc 顶部高度
|
||||
* @descEN Height of header
|
||||
*/
|
||||
headerHeight: number | string;
|
||||
}
|
||||
export declare const prepareComponentToken: GetDefaultToken<'Transfer'>;
|
||||
declare const _default: (prefixCls: string, rootCls?: string) => readonly [string, string];
|
||||
export default _default;
|
||||
+315
@@ -0,0 +1,315 @@
|
||||
import { unit } from '@ant-design/cssinjs';
|
||||
import { operationUnit, resetComponent, resetIcon, textEllipsis } from '../../style';
|
||||
import { genStyleHooks, mergeToken } from '../../theme/internal';
|
||||
const genTransferCustomizeStyle = token => {
|
||||
const {
|
||||
antCls,
|
||||
componentCls,
|
||||
listHeight,
|
||||
controlHeightLG
|
||||
} = token;
|
||||
const tableCls = `${antCls}-table`;
|
||||
const inputCls = `${antCls}-input`;
|
||||
return {
|
||||
[`${componentCls}-customize-list`]: {
|
||||
[`${componentCls}-section`]: {
|
||||
flex: '1 1 50%',
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
minHeight: listHeight,
|
||||
minWidth: 0
|
||||
},
|
||||
// =================== Hook Components ===================
|
||||
[`${tableCls}-wrapper`]: {
|
||||
[`${tableCls}-small`]: {
|
||||
border: 0,
|
||||
borderRadius: 0,
|
||||
[`${tableCls}-selection-column`]: {
|
||||
width: controlHeightLG,
|
||||
minWidth: controlHeightLG
|
||||
}
|
||||
},
|
||||
[`${tableCls}-pagination${tableCls}-pagination`]: {
|
||||
margin: 0,
|
||||
padding: token.paddingXS
|
||||
}
|
||||
},
|
||||
[`${inputCls}[disabled]`]: {
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
const genTransferStatusColor = (token, color) => {
|
||||
const {
|
||||
componentCls,
|
||||
colorBorder
|
||||
} = token;
|
||||
return {
|
||||
[`${componentCls}-section`]: {
|
||||
borderColor: color,
|
||||
[`${componentCls}-list-search:not([disabled])`]: {
|
||||
borderColor: colorBorder
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
const genTransferStatusStyle = token => {
|
||||
const {
|
||||
componentCls
|
||||
} = token;
|
||||
return {
|
||||
[`${componentCls}-status-error`]: {
|
||||
...genTransferStatusColor(token, token.colorError)
|
||||
},
|
||||
[`${componentCls}-status-warning`]: {
|
||||
...genTransferStatusColor(token, token.colorWarning)
|
||||
}
|
||||
};
|
||||
};
|
||||
const genTransferListStyle = token => {
|
||||
const {
|
||||
componentCls,
|
||||
colorBorder,
|
||||
colorSplit,
|
||||
lineWidth,
|
||||
itemHeight,
|
||||
headerHeight,
|
||||
transferHeaderVerticalPadding,
|
||||
itemPaddingBlock,
|
||||
controlItemBgActive,
|
||||
colorTextDisabled,
|
||||
colorTextSecondary,
|
||||
listHeight,
|
||||
listWidth,
|
||||
listWidthLG,
|
||||
fontSizeIcon,
|
||||
marginXS,
|
||||
paddingSM,
|
||||
lineType,
|
||||
antCls,
|
||||
iconCls,
|
||||
motionDurationSlow,
|
||||
controlItemBgHover,
|
||||
borderRadiusLG,
|
||||
colorBgContainer,
|
||||
colorText,
|
||||
controlItemBgActiveHover
|
||||
} = token;
|
||||
const contentBorderRadius = unit(token.calc(borderRadiusLG).sub(lineWidth).equal());
|
||||
return {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: listWidth,
|
||||
height: listHeight,
|
||||
border: `${unit(lineWidth)} ${lineType} ${colorBorder}`,
|
||||
borderRadius: token.borderRadiusLG,
|
||||
'&-with-pagination': {
|
||||
width: listWidthLG,
|
||||
height: 'auto'
|
||||
},
|
||||
[`${componentCls}-list`]: {
|
||||
'&-search': {
|
||||
[`${iconCls}-search`]: {
|
||||
color: colorTextDisabled
|
||||
}
|
||||
},
|
||||
'&-header': {
|
||||
display: 'flex',
|
||||
flex: 'none',
|
||||
alignItems: 'center',
|
||||
height: headerHeight,
|
||||
// border-top is on the transfer dom. We should minus 1px for this
|
||||
padding: `${unit(token.calc(transferHeaderVerticalPadding).sub(lineWidth).equal())} ${unit(paddingSM)} ${unit(transferHeaderVerticalPadding)}`,
|
||||
color: colorText,
|
||||
background: colorBgContainer,
|
||||
borderBottom: `${unit(lineWidth)} ${lineType} ${colorSplit}`,
|
||||
borderRadius: `${unit(borderRadiusLG)} ${unit(borderRadiusLG)} 0 0`,
|
||||
'> *:not(:last-child)': {
|
||||
marginInlineEnd: 4 // This is magic and fixed number, DO NOT use token since it may change.
|
||||
},
|
||||
'> *': {
|
||||
flex: 'none'
|
||||
},
|
||||
'&-title': {
|
||||
...textEllipsis,
|
||||
flex: '0 1 auto',
|
||||
textAlign: 'end',
|
||||
marginInlineStart: 'auto'
|
||||
},
|
||||
'&-dropdown': {
|
||||
...resetIcon(),
|
||||
fontSize: fontSizeIcon,
|
||||
transform: 'translateY(10%)',
|
||||
cursor: 'pointer',
|
||||
'&[disabled]': {
|
||||
cursor: 'not-allowed'
|
||||
}
|
||||
}
|
||||
},
|
||||
'&-body': {
|
||||
display: 'flex',
|
||||
flex: 'auto',
|
||||
flexDirection: 'column',
|
||||
fontSize: token.fontSize,
|
||||
// https://blog.csdn.net/qq449245884/article/details/107373672/
|
||||
minHeight: 0,
|
||||
'&-search-wrapper': {
|
||||
position: 'relative',
|
||||
flex: 'none',
|
||||
padding: paddingSM
|
||||
}
|
||||
},
|
||||
'&-content': {
|
||||
flex: 'auto',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
overflow: 'auto',
|
||||
listStyle: 'none',
|
||||
borderRadius: `0 0 ${contentBorderRadius} ${contentBorderRadius}`,
|
||||
'&-item': {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
minHeight: itemHeight,
|
||||
padding: `${unit(itemPaddingBlock)} ${unit(paddingSM)}`,
|
||||
transition: `all ${motionDurationSlow}`,
|
||||
'> *:not(:last-child)': {
|
||||
marginInlineEnd: marginXS
|
||||
},
|
||||
'> *': {
|
||||
flex: 'none'
|
||||
},
|
||||
'&-text': {
|
||||
...textEllipsis,
|
||||
flex: 'auto'
|
||||
},
|
||||
'&-remove': {
|
||||
...operationUnit(token),
|
||||
color: colorBorder,
|
||||
'&:hover, &:focus': {
|
||||
color: colorTextSecondary
|
||||
},
|
||||
'&:disabled': {
|
||||
color: colorTextDisabled,
|
||||
cursor: 'not-allowed'
|
||||
}
|
||||
},
|
||||
[`&:not(${componentCls}-list-content-item-disabled)`]: {
|
||||
'&:hover': {
|
||||
backgroundColor: controlItemBgHover,
|
||||
cursor: 'pointer'
|
||||
},
|
||||
[`&${componentCls}-list-content-item-checked:hover`]: {
|
||||
backgroundColor: controlItemBgActiveHover
|
||||
}
|
||||
},
|
||||
'&-checked': {
|
||||
backgroundColor: controlItemBgActive
|
||||
},
|
||||
'&-disabled': {
|
||||
color: colorTextDisabled,
|
||||
cursor: 'not-allowed'
|
||||
}
|
||||
},
|
||||
// Do not change hover style when `oneWay` mode
|
||||
[`&-show-remove ${componentCls}-list-content-item:not(${componentCls}-list-content-item-disabled):hover`]: {
|
||||
background: 'transparent',
|
||||
cursor: 'default'
|
||||
}
|
||||
},
|
||||
'&-pagination': {
|
||||
padding: token.paddingXS,
|
||||
textAlign: 'end',
|
||||
borderTop: `${unit(lineWidth)} ${lineType} ${colorSplit}`,
|
||||
[`${antCls}-pagination-options`]: {
|
||||
paddingInlineEnd: token.paddingXS
|
||||
}
|
||||
},
|
||||
'&-body-not-found': {
|
||||
flex: 'none',
|
||||
width: '100%',
|
||||
margin: 'auto 0',
|
||||
color: colorTextDisabled,
|
||||
textAlign: 'center'
|
||||
},
|
||||
'&-footer': {
|
||||
borderTop: `${unit(lineWidth)} ${lineType} ${colorSplit}`
|
||||
},
|
||||
// fix: https://github.com/ant-design/ant-design/issues/44489
|
||||
'&-checkbox': {
|
||||
lineHeight: 1
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
const genTransferStyle = token => {
|
||||
const {
|
||||
antCls,
|
||||
iconCls,
|
||||
componentCls,
|
||||
marginXS,
|
||||
marginXXS,
|
||||
fontSizeIcon,
|
||||
colorBgContainerDisabled
|
||||
} = token;
|
||||
return {
|
||||
[componentCls]: {
|
||||
...resetComponent(token),
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'stretch',
|
||||
[`${componentCls}-disabled`]: {
|
||||
[`${componentCls}-section`]: {
|
||||
background: colorBgContainerDisabled
|
||||
}
|
||||
},
|
||||
[`${componentCls}-section`]: genTransferListStyle(token),
|
||||
[`${componentCls}-actions`]: {
|
||||
display: 'flex',
|
||||
flex: 'none',
|
||||
flexDirection: 'column',
|
||||
alignSelf: 'center',
|
||||
margin: `0 ${unit(marginXS)}`,
|
||||
verticalAlign: 'middle',
|
||||
gap: marginXXS,
|
||||
[`${antCls}-btn ${iconCls}`]: {
|
||||
fontSize: fontSizeIcon
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
const genTransferRTLStyle = token => {
|
||||
const {
|
||||
componentCls
|
||||
} = token;
|
||||
return {
|
||||
[`${componentCls}-rtl`]: {
|
||||
direction: 'rtl'
|
||||
}
|
||||
};
|
||||
};
|
||||
export const prepareComponentToken = token => {
|
||||
const {
|
||||
fontSize,
|
||||
lineHeight,
|
||||
controlHeight,
|
||||
controlHeightLG,
|
||||
lineWidth
|
||||
} = token;
|
||||
const fontHeight = Math.round(fontSize * lineHeight);
|
||||
return {
|
||||
listWidth: 180,
|
||||
listHeight: 200,
|
||||
listWidthLG: 250,
|
||||
headerHeight: controlHeightLG,
|
||||
itemHeight: controlHeight,
|
||||
itemPaddingBlock: (controlHeight - fontHeight) / 2,
|
||||
transferHeaderVerticalPadding: Math.ceil((controlHeightLG - lineWidth - fontHeight) / 2)
|
||||
};
|
||||
};
|
||||
// ============================== Export ==============================
|
||||
export default genStyleHooks('Transfer', token => {
|
||||
const transferToken = mergeToken(token);
|
||||
return [genTransferStyle(transferToken), genTransferCustomizeStyle(transferToken), genTransferStatusStyle(transferToken), genTransferRTLStyle(transferToken)];
|
||||
}, prepareComponentToken);
|
||||
Reference in New Issue
Block a user