import type { DId } from '../../utils/types';
import type { DFormControl } from '../form';
import React, { useCallback, useMemo } from 'react';
import { LeftOutlined, RightOutlined } from '@react-devui/icons';
import { getClassName } from '@react-devui/utils';
import { useGeneralContext, useDValue } from '../../hooks';
import { registerComponentMate } from '../../utils';
import { DBaseDesign } from '../_base-design';
import { DButton } from '../button';
import { useFormControl } from '../form';
import { useComponentConfig, usePrefixConfig } from '../root';
import { DPanel } from './Panel';
export const IS_SELECTED = Symbol();
export interface DTransferItem<V extends DId> {
label: string;
value: V;
disabled?: boolean;
}
export interface DTransferProps<V extends DId, T extends DTransferItem<V>> extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
dFormControl?: DFormControl;
dList: T[];
dModel?: V[];
dSelected?: V[];
dTitle?: [React.ReactNode?, React.ReactNode?];
dActions?: React.ReactNode[];
dLoading?: [boolean?, boolean?];
dSearchable?: boolean;
dSearchValue?: [string, string];
dDisabled?: boolean;
dVirtual?: boolean;
dCustomItem?: (item: T) => React.ReactNode;
dCustomSearch?: {
filter?: (value: string, item: T) => boolean;
sort?: (a: T, b: T) => number;
};
onModelChange?: (value: T['value'][], item: (T | undefined)[]) => void;
onSelectedChange?: (value: T['value'][], item: (T | undefined)[]) => void;
onSearchValueChange?: (value: [string, string]) => void;
onScrollBottom?: (direction: 'left' | 'right') => void;
}
const { COMPONENT_NAME } = registerComponentMate({ COMPONENT_NAME: 'DTransfer' as const });
export function DTransfer<V extends DId, T extends DTransferItem<V>>(props: DTransferProps<V, T>): JSX.Element | null {
const {
dFormControl,
dList,
dModel,
dSelected,
dTitle,
dActions = ['right', 'left'],
dLoading = [false, false],
dSearchable = false,
dSearchValue,
dDisabled = false,
dVirtual = false,
dCustomItem,
dCustomSearch,
onModelChange,
onSelectedChange,
onSearchValueChange,
onScrollBottom,
...restProps
} = useComponentConfig(COMPONENT_NAME, props);
const dPrefix = usePrefixConfig();
const { gDisabled } = useGeneralContext();
const itemsMap = useMemo(() => new Map(dList.map((item) => [item.value, item])), [dList]);
const formControlInject = useFormControl(dFormControl);
const [_valueRight, changeValueRight] = useDValue<V[]>(
[],
dModel,
(value) => {
if (onModelChange) {
onModelChange(
value,
value.map((v) => itemsMap.get(v))
);
}
},
undefined,
formControlInject
);
const valueRight = useMemo(() => new Set(_valueRight), [_valueRight]);
const [_selected, changeSelected] = useDValue<V[]>([], dSelected, (value) => {
if (onSelectedChange) {
onSelectedChange(
value,
value.map((v) => itemsMap.get(v))
);
}
});
const selected = useMemo(() => new Set(_selected), [_selected]);
const disabled = dDisabled || gDisabled || dFormControl?.control.disabled;
const [searchValues, changeSearchValue] = useDValue<[string, string]>(['', ''], dSearchValue, onSearchValueChange);
const _filterFn = dCustomSearch?.filter;
const filterFn = useCallback(
(searchStr: string, item: T) => {
const defaultFilterFn = (item: T) => {
return item.label.includes(searchStr);
};
return _filterFn ? _filterFn(searchStr, item) : defaultFilterFn(item);
},
[_filterFn]
);
const sortFn = dCustomSearch?.sort;
const [listLeft, listRight, selectedNumLeft, selectedNumRight, stateLeft, stateRight] = useMemo(() => {
const listL: T[] = [];
const listR: T[] = [];
let selectedNumL = 0;
let selectedNumR = 0;
let emptyL = true;
let hasSelectedL = false;
let checkAllL = true;
let emptyR = true;
let hasSelectedR = false;
let checkAllR = true;
dList.forEach((item) => {
const isLeft = !valueRight.has(item.value);
const newItem = Object.assign({}, item);
const searchValue = searchValues[isLeft ? 0 : 1];
if (!searchValue || filterFn(searchValue, item)) {
newItem[IS_SELECTED] = false;
if (selected.has(item.value)) {
newItem[IS_SELECTED] = true;
isLeft ? (selectedNumL += 1) : (selectedNumR += 1);
if (!item.disabled) {
isLeft ? (hasSelectedL = true) : (hasSelectedR = true);
}
} else {
if (!item.disabled) {
isLeft ? (checkAllL = false) : (checkAllR = false);
}
}
if (isLeft) {
listL.push(newItem);
if (!item.disabled) {
emptyL = false;
}
} else {
listR.push(newItem);
if (!item.disabled) {
emptyR = false;
}
}
}
});
if (searchValues[0] && sortFn) {
listL.sort(sortFn);
}
if (searchValues[1] && sortFn) {
listR.sort(sortFn);
}
const stateL: boolean | 'mixed' = emptyL ? false : checkAllL ? true : hasSelectedL ? 'mixed' : false;
const stateR: boolean | 'mixed' = emptyR ? false : checkAllR ? true : hasSelectedR ? 'mixed' : false;
return [listL, listR, selectedNumL, selectedNumR, stateL, stateR];
}, [dList, filterFn, searchValues, selected, sortFn, valueRight]);
const handleSelectedChange = (val: V) => {
changeSelected((draft) => {
const index = draft.findIndex((v) => v === val);
if (index !== -1) {
draft.splice(index, 1);
} else {
draft.push(val as V);
}
});
};
const handleAllSelected = (isSelected: boolean, isLeft: boolean) => {
changeSelected((draft) => {
const newSelected = new Set(draft);
for (const item of isLeft ? listLeft : listRight) {
if (item.disabled) {
continue;
}
if (isSelected && !item[IS_SELECTED]) {
newSelected.add(item.value);
} else if (!isSelected && item[IS_SELECTED]) {
newSelected.delete(item.value);
}
}
return Array.from(newSelected);
});
};
const handleButtonClick = (isLeft: boolean) => {
changeValueRight((draft) => {
const newValueRight = new Set(draft);
(isLeft ? listLeft : listRight).forEach((item) => {
if (item[IS_SELECTED]) {
if (isLeft) {
newValueRight.add(item.value);
} else {
newValueRight.delete(item.value);
}
}
});
return Array.from(newValueRight);
});
changeSelected((draft) => {
const newSelected = new Set(draft);
(isLeft ? listLeft : listRight).forEach((item) => {
if (item[IS_SELECTED]) {
newSelected.delete(item.value);
}
});
return Array.from(newSelected);
});
};
return (
<DBaseDesign
dComposeDesign={false}
dFormDesign={{
control: dFormControl,
}}
>
{({ render: renderBaseDesign }) =>
renderBaseDesign(
<div
{...restProps}
className={getClassName(restProps.className, `${dPrefix}transfer`, {
'is-disabled': disabled,
})}
>
<DPanel
dList={listLeft}
dSelectedNum={selectedNumLeft}
dState={stateLeft}
dTitle={dTitle?.[0]}
dLoading={dLoading[0] ?? false}
dSearchable={dSearchable}
dVirtual={dVirtual}
dCustomItem={dCustomItem}
onSelectedChange={handleSelectedChange}
onAllSelected={(selected) => {
handleAllSelected(selected, true);
}}
onSearch={(val) => {
changeSearchValue((draft) => {
draft[0] = val;
});
}}
onScrollBottom={() => {
onScrollBottom?.('left');
}}
></DPanel>
<div className={`${dPrefix}transfer__actions`}>
{React.Children.map(dActions, (action) =>
action === 'right' ? (
<DButton
key="$$right"
disabled={stateLeft === false}
onClick={() => {
handleButtonClick(true);
}}
dType="secondary"
dIcon={<RightOutlined />}
></DButton>
) : action === 'left' ? (
<DButton
key="$$left"
disabled={stateRight === false}
onClick={() => {
handleButtonClick(false);
}}
dType="secondary"
dIcon={<LeftOutlined />}
></DButton>
) : (
action
)
)}
</div>
<DPanel
dList={listRight}
dSelectedNum={selectedNumRight}
dState={stateRight}
dTitle={dTitle?.[1]}
dLoading={dLoading[1] ?? false}
dSearchable={dSearchable}
dVirtual={dVirtual}
dCustomItem={dCustomItem}
onSelectedChange={handleSelectedChange}
onAllSelected={(selected) => {
handleAllSelected(selected, false);
}}
onSearch={(val) => {
changeSearchValue((draft) => {
draft[1] = val;
});
}}
onScrollBottom={() => {
onScrollBottom?.('right');
}}
></DPanel>
</div>
)
}
</DBaseDesign>
);
}