411 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Vue
		
	
	
			
		
		
	
	
			411 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Vue
		
	
	
| import classNames from '../_util/classNames';
 | |
| 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 ListBody from './ListBody';
 | |
| import type { VNode, VNodeTypes, ExtractPropTypes, PropType } from 'vue';
 | |
| import { watchEffect, computed, defineComponent, ref } from 'vue';
 | |
| import type { RadioChangeEvent } from '../radio/interface';
 | |
| import type { TransferDirection, TransferItem } from './index';
 | |
| 
 | |
| const defaultRender = () => null;
 | |
| 
 | |
| function isRenderResultPlainObject(result: VNode) {
 | |
|   return (
 | |
|     result &&
 | |
|     !isValidElement(result) &&
 | |
|     Object.prototype.toString.call(result) === '[object Object]'
 | |
|   );
 | |
| }
 | |
| 
 | |
| function getEnabledItemKeys<RecordType extends TransferItem>(items: RecordType[]) {
 | |
|   return items.filter(data => !data.disabled).map(data => data.key);
 | |
| }
 | |
| 
 | |
| export const transferListProps = {
 | |
|   prefixCls: String,
 | |
|   dataSource: { type: Array as PropType<TransferItem[]>, default: [] },
 | |
|   filter: String,
 | |
|   filterOption: Function,
 | |
|   checkedKeys: PropTypes.arrayOf(PropTypes.string),
 | |
|   handleFilter: Function,
 | |
|   handleClear: Function,
 | |
|   renderItem: Function,
 | |
|   showSearch: { type: Boolean, default: false },
 | |
|   searchPlaceholder: String,
 | |
|   notFoundContent: PropTypes.any,
 | |
|   itemUnit: String,
 | |
|   itemsUnit: String,
 | |
|   renderList: PropTypes.any,
 | |
|   disabled: { type: Boolean, default: undefined },
 | |
|   direction: String as PropType<TransferDirection>,
 | |
|   showSelectAll: { type: Boolean, default: undefined },
 | |
|   remove: String,
 | |
|   selectAll: String,
 | |
|   selectCurrent: String,
 | |
|   selectInvert: String,
 | |
|   removeAll: String,
 | |
|   removeCurrent: String,
 | |
|   selectAllLabel: PropTypes.any,
 | |
|   showRemove: { type: Boolean, default: undefined },
 | |
|   pagination: PropTypes.any,
 | |
|   onItemSelect: Function,
 | |
|   onItemSelectAll: Function,
 | |
|   onItemRemove: Function,
 | |
|   onScroll: Function,
 | |
| };
 | |
| 
 | |
| export type TransferListProps = Partial<ExtractPropTypes<typeof transferListProps>>;
 | |
| 
 | |
| export default defineComponent({
 | |
|   name: 'TransferList',
 | |
|   inheritAttrs: false,
 | |
|   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,
 | |
|       };
 | |
|     };
 | |
| 
 | |
|     const renderItemHtml = (item: TransferItem) => {
 | |
|       const { renderItem = defaultRender } = props;
 | |
|       const renderResult = renderItem(item);
 | |
|       const isRenderResultPlain = isRenderResultPlainObject(renderResult);
 | |
|       return {
 | |
|         renderedText: isRenderResultPlain ? renderResult.value : renderResult,
 | |
|         renderedEl: isRenderResultPlain ? renderResult.label : renderResult,
 | |
|         item,
 | |
|       };
 | |
|     };
 | |
| 
 | |
|     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);
 | |
|       });
 | |
|       filteredItems.value = fItems;
 | |
|       filteredRenderItems.value = fRenderItems;
 | |
|     });
 | |
| 
 | |
|     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 enabledItemKeys = computed(() => {
 | |
|       return getEnabledItemKeys(filteredItems.value);
 | |
|     });
 | |
| 
 | |
|     const getNewSelectKeys = (keys, unCheckedKeys) => {
 | |
|       return Array.from(new Set([...keys, ...props.checkedKeys])).filter(
 | |
|         key => unCheckedKeys.indexOf(key) === -1,
 | |
|       );
 | |
|     };
 | |
| 
 | |
|     const getCheckBox = ({ disabled, prefixCls }: { disabled?: boolean; prefixCls?: string }) => {
 | |
|       const checkedAll = checkStatus.value === 'all';
 | |
|       const checkAllCheckbox = (
 | |
|         <Checkbox
 | |
|           disabled={disabled}
 | |
|           checked={checkedAll}
 | |
|           indeterminate={checkStatus.value === 'part'}
 | |
|           class={`${prefixCls}-checkbox`}
 | |
|           onChange={() => {
 | |
|             // Only select enabled items
 | |
| 
 | |
|             const keys = enabledItemKeys.value;
 | |
|             props.onItemSelectAll(
 | |
|               getNewSelectKeys(!checkedAll ? keys : [], checkedAll ? props.checkedKeys : []),
 | |
|             );
 | |
|           }}
 | |
|         />
 | |
|       );
 | |
| 
 | |
|       return checkAllCheckbox;
 | |
|     };
 | |
| 
 | |
|     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;
 | |
|     };
 | |
| 
 | |
|     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>
 | |
|       ) : 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 = true,
 | |
|         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({ disabled, prefixCls });
 | |
| 
 | |
|       let menu = null;
 | |
|       if (showRemove) {
 | |
|         menu = (
 | |
|           <Menu>
 | |
|             {/* Remove Current Page */}
 | |
|             {pagination && (
 | |
|               <Menu.Item
 | |
|                 key="removeCurrent"
 | |
|                 onClick={() => {
 | |
|                   const pageKeys = getEnabledItemKeys(
 | |
|                     (defaultListBodyRef.value.items || []).map(entity => entity.item),
 | |
|                   );
 | |
|                   onItemRemove?.(pageKeys);
 | |
|                 }}
 | |
|               >
 | |
|                 {removeCurrent}
 | |
|               </Menu.Item>
 | |
|             )}
 | |
| 
 | |
|             {/* Remove All */}
 | |
|             <Menu.Item
 | |
|               key="removeAll"
 | |
|               onClick={() => {
 | |
|                 onItemRemove?.(enabledItemKeys.value);
 | |
|               }}
 | |
|             >
 | |
|               {removeAll}
 | |
|             </Menu.Item>
 | |
|           </Menu>
 | |
|         );
 | |
|       } else {
 | |
|         menu = (
 | |
|           <Menu>
 | |
|             <Menu.Item
 | |
|               key="selectAll"
 | |
|               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
 | |
|               key="selectInvert"
 | |
|               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`}>
 | |
|             {showSelectAll ? (
 | |
|               <>
 | |
|                 {checkAllCheckbox}
 | |
|                 {dropdown}
 | |
|               </>
 | |
|             ) : null}
 | |
|             <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>
 | |
|       );
 | |
|     };
 | |
|   },
 | |
| });
 |