UNPKG

@douyinfe/semi-ui

Version:

A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.

618 lines (617 loc) 22.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _isArray2 = _interopRequireDefault(require("lodash/isArray")); var _isEmpty2 = _interopRequireDefault(require("lodash/isEmpty")); var _omit2 = _interopRequireDefault(require("lodash/omit")); var _noop2 = _interopRequireDefault(require("lodash/noop")); var _isEqual2 = _interopRequireDefault(require("lodash/isEqual")); var _react = _interopRequireDefault(require("react")); var _classnames = _interopRequireDefault(require("classnames")); var _propTypes = _interopRequireDefault(require("prop-types")); var _foundation = _interopRequireDefault(require("@douyinfe/semi-foundation/lib/cjs/transfer/foundation")); var _transferUtils = require("@douyinfe/semi-foundation/lib/cjs/transfer/transferUtils"); var _constants = require("@douyinfe/semi-foundation/lib/cjs/transfer/constants"); require("@douyinfe/semi-foundation/lib/cjs/transfer/transfer.css"); var _baseComponent = _interopRequireDefault(require("../_base/baseComponent")); var _localeConsumer = _interopRequireDefault(require("../locale/localeConsumer")); var _index = require("../checkbox/index"); var _index2 = _interopRequireDefault(require("../input/index")); var _spin = _interopRequireDefault(require("../spin")); var _button = _interopRequireDefault(require("../button")); var _tree = _interopRequireDefault(require("../tree")); var _semiIcons = require("@douyinfe/semi-icons"); var _sortable = require("../_sortable"); var _sortable2 = require("@dnd-kit/sortable"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } var __rest = void 0 && (void 0).__rest || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; const prefixCls = _constants.cssClasses.PREFIX; class Transfer extends _baseComponent.default { constructor(props) { super(props); this._treeRef = null; this.renderRightItem = (item, sortableHandle) => { const { renderSelectedItem, draggable, type, showPath } = this.props; const onRemove = () => this.foundation.handleSelectOrRemove(item); const rightItemCls = (0, _classnames.default)({ [`${prefixCls}-item`]: true, [`${prefixCls}-right-item`]: true, [`${prefixCls}-right-item-draggable`]: draggable }); const shouldShowPath = type === _constants.strings.TYPE_TREE_TO_LIST && showPath === true; const label = shouldShowPath ? this.foundation._generatePath(item) : item.label; if (renderSelectedItem) { return renderSelectedItem(Object.assign(Object.assign({}, item), { onRemove, sortableHandle })); } const DragHandle = sortableHandle && sortableHandle(() => (/*#__PURE__*/_react.default.createElement(_semiIcons.IconHandle, { role: "button", "aria-label": "Drag and sort", className: `${prefixCls}-right-item-drag-handler` }))); return ( /*#__PURE__*/ // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex _react.default.createElement("div", { role: "listitem", className: rightItemCls, key: item.key }, draggable && sortableHandle ? /*#__PURE__*/_react.default.createElement(DragHandle, null) : null, /*#__PURE__*/_react.default.createElement("div", { className: `${prefixCls}-right-item-text` }, label), /*#__PURE__*/_react.default.createElement(_semiIcons.IconClose, { onClick: onRemove, "aria-disabled": item.disabled, className: (0, _classnames.default)(`${prefixCls}-item-close-icon`, { [`${prefixCls}-item-close-icon-disabled`]: item.disabled }) })) ); }; this.renderSortItem = props => { const { id, sortableHandle } = props; const { selectedItems } = this.state; const selectedData = [...selectedItems.values()]; const item = selectedData.find(item => item.key === id); return this.renderRightItem(item, sortableHandle); }; const { defaultValue = [], dataSource, type } = props; this.foundation = new _foundation.default(this.adapter); this.state = { data: [], selectedItems: new Map(), searchResult: new Set(), inputValue: '' }; if (Boolean(dataSource) && (0, _isArray2.default)(dataSource)) { // @ts-ignore Avoid reporting errors this.state.xxx is read-only this.state.data = (0, _transferUtils._generateDataByType)(dataSource, type); } if (Boolean(defaultValue) && (0, _isArray2.default)(defaultValue)) { // @ts-ignore Avoid reporting errors this.state.xxx is read-only this.state.selectedItems = (0, _transferUtils._generateSelectedItems)(defaultValue, this.state.data); } this.onSelectOrRemove = this.onSelectOrRemove.bind(this); this.onInputChange = this.onInputChange.bind(this); this.onSortEnd = this.onSortEnd.bind(this); } static getDerivedStateFromProps(props, state) { const { value, dataSource, type, filter } = props; const mergedState = {}; let newData = state.data; let newSelectedItems = state.selectedItems; if (Boolean(dataSource) && Array.isArray(dataSource)) { newData = (0, _transferUtils._generateDataByType)(dataSource, type); mergedState.data = newData; } if (Boolean(value) && Array.isArray(value)) { newSelectedItems = (0, _transferUtils._generateSelectedItems)(value, newData); mergedState.selectedItems = newSelectedItems; } if (!(0, _isEqual2.default)(state.data, newData)) { if (typeof state.inputValue === 'string' && state.inputValue !== '') { const filterFunc = typeof filter === 'function' ? item => filter(state.inputValue, item) : item => typeof item.label === 'string' && item.label.includes(state.inputValue); const searchData = newData.filter(filterFunc); const searchResult = new Set(searchData.map(item => item.key)); mergedState.searchResult = searchResult; } } return (0, _isEmpty2.default)(mergedState) ? null : mergedState; } get adapter() { return Object.assign(Object.assign({}, super.adapter), { getSelected: () => new Map(this.state.selectedItems), updateSelected: selectedItems => { this.setState({ selectedItems }); }, notifyChange: (values, items) => { this.props.onChange(values, items); }, notifySearch: input => { this.props.onSearch(input); }, notifySelect: item => { this.props.onSelect(item); }, notifyDeselect: item => { this.props.onDeselect(item); }, updateInput: input => { this.setState({ inputValue: input }); }, updateSearchResult: searchResult => { this.setState({ searchResult }); }, searchTree: keyword => { this._treeRef && this._treeRef.search(keyword); // TODO check this._treeRef.current? } }); } onInputChange(value) { this.foundation.handleInputChange(value, true); } search(value) { // The search method is used to provide the user with a manually triggered search // Since the method is manually called by the user, setting the second parameter to false does not trigger the onSearch callback to notify the user this.foundation.handleInputChange(value, false); } onSelectOrRemove(item) { this.foundation.handleSelectOrRemove(item); } onSortEnd(callbackProps) { this.foundation.handleSortEnd(callbackProps); } renderFilter(locale) { const { inputProps, filter, disabled } = this.props; if (typeof filter === 'boolean' && !filter) { return null; } return /*#__PURE__*/_react.default.createElement("div", { role: "search", "aria-label": "Transfer filter", className: `${prefixCls}-filter` }, /*#__PURE__*/_react.default.createElement(_index2.default, Object.assign({ prefix: /*#__PURE__*/_react.default.createElement(_semiIcons.IconSearch, null), placeholder: locale.placeholder, showClear: true, value: this.state.inputValue, disabled: disabled, onChange: this.onInputChange }, inputProps))); } renderHeader(headerConfig) { const { disabled, renderSourceHeader, renderSelectedHeader } = this.props; const { totalContent, allContent, onAllClick, type, showButton } = headerConfig; const headerCls = (0, _classnames.default)({ [`${prefixCls}-header`]: true, [`${prefixCls}-right-header`]: type === 'right', [`${prefixCls}-left-header`]: type === 'left' }); if (type === 'left' && typeof renderSourceHeader === 'function') { const { num, showButton, allChecked, onAllClick } = headerConfig; return renderSourceHeader({ num, showButton, allChecked, onAllClick }); } if (type === 'right' && typeof renderSelectedHeader === 'function') { const { num, showButton, onAllClick: onClear } = headerConfig; return renderSelectedHeader({ num, showButton, onClear }); } return /*#__PURE__*/_react.default.createElement("div", { className: headerCls }, /*#__PURE__*/_react.default.createElement("span", { className: `${prefixCls}-header-total` }, totalContent), showButton ? (/*#__PURE__*/_react.default.createElement(_button.default, { theme: "borderless", disabled: disabled, type: "tertiary", size: "small", className: `${prefixCls}-header-all`, onClick: onAllClick }, allContent)) : null); } renderLeftItem(item, index) { const { renderSourceItem, disabled } = this.props; const { selectedItems } = this.state; const checked = selectedItems.has(item.key); if (renderSourceItem) { return renderSourceItem(Object.assign(Object.assign({}, item), { checked, onChange: () => this.onSelectOrRemove(item) })); } const leftItemCls = (0, _classnames.default)({ [`${prefixCls}-item`]: true, [`${prefixCls}-item-disabled`]: item.disabled }); return /*#__PURE__*/_react.default.createElement(_index.Checkbox, { key: index, disabled: item.disabled || disabled, className: leftItemCls, checked: checked, role: "listitem", onChange: () => this.onSelectOrRemove(item), "x-semi-children-alias": `dataSource[${index}].label` }, item.label); } renderLeft(locale) { const { data, selectedItems, inputValue, searchResult } = this.state; const { loading, type, emptyContent, renderSourcePanel, dataSource } = this.props; const totalToken = locale.total; const inSearchMode = inputValue !== ''; const showNumber = inSearchMode ? searchResult.size : data.length; const filterData = inSearchMode ? data.filter(item => searchResult.has(item.key)) : data; // Whether to select all should be a judgment, whether the filtered data on the left is a subset of the selected items // For example, the filtered data on the left is 1, 3, 4; // The selected option is 1,2,3,4, it is true // The selected option is 2,3,4, then it is false let filterDataAllDisabled = true; const leftContainsNotInSelected = Boolean(filterData.find(f => { if (f.disabled) { return false; } else { if (filterDataAllDisabled) { filterDataAllDisabled = false; } return !selectedItems.has(f.key); } })); const totalText = totalToken.replace('${total}', `${showNumber}`); const headerConfig = { totalContent: totalText, allContent: leftContainsNotInSelected ? locale.selectAll : locale.clearSelectAll, onAllClick: () => this.foundation.handleAll(leftContainsNotInSelected), type: 'left', showButton: type !== _constants.strings.TYPE_TREE_TO_LIST && !filterDataAllDisabled, num: showNumber, allChecked: !leftContainsNotInSelected }; const inputCom = this.renderFilter(locale); const headerCom = this.renderHeader(headerConfig); const noMatch = inSearchMode && searchResult.size === 0; const emptySearch = emptyContent.search ? emptyContent.search : locale.emptySearch; const emptyLeft = emptyContent.left ? emptyContent.left : locale.emptyLeft; const emptyDataCom = this.renderEmpty('left', emptyLeft); const emptySearchCom = this.renderEmpty('left', emptySearch); const loadingCom = /*#__PURE__*/_react.default.createElement(_spin.default, null); let content = null; switch (true) { case loading: content = loadingCom; break; case noMatch: content = emptySearchCom; break; case data.length === 0: content = emptyDataCom; break; case type === _constants.strings.TYPE_TREE_TO_LIST: content = /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, headerCom, this.renderLeftTree()); break; case !noMatch && (type === _constants.strings.TYPE_LIST || type === _constants.strings.TYPE_GROUP_LIST): content = /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, headerCom, this.renderLeftList(filterData)); break; default: content = null; break; } const { values } = this.foundation.getValuesAndItemsFromMap(selectedItems); const renderProps = { loading, noMatch, filterData, sourceData: data, propsDataSource: dataSource, allChecked: !leftContainsNotInSelected, showNumber, inputValue, selectedItems, value: values, onSelect: this.foundation.handleSelect.bind(this.foundation), onAllClick: () => this.foundation.handleAll(leftContainsNotInSelected), onSearch: this.onInputChange, onSelectOrRemove: item => this.onSelectOrRemove(item) }; if (renderSourcePanel) { return renderSourcePanel(renderProps); } return /*#__PURE__*/_react.default.createElement("section", { className: `${prefixCls}-left` }, inputCom, content); } renderGroupTitle(group, index) { const groupCls = (0, _classnames.default)(`${prefixCls}-group-title`); return /*#__PURE__*/_react.default.createElement("div", { className: groupCls, key: `title-${index}` }, group.title); } renderLeftTree() { const { selectedItems } = this.state; const { disabled, dataSource, treeProps } = this.props; const { values } = this.foundation.getValuesAndItemsFromMap(selectedItems); const onChange = value => { this.foundation.handleSelect(value); }; const restTreeProps = (0, _omit2.default)(treeProps, ['value', 'ref', 'onChange']); return /*#__PURE__*/_react.default.createElement(_tree.default, Object.assign({ disabled: disabled, treeData: dataSource, multiple: true, disableStrictly: true, value: values, defaultExpandAll: true, leafOnly: true, ref: tree => this._treeRef = tree, filterTreeNode: true, searchRender: false, searchStyle: { padding: 0 }, style: { flex: 1, overflow: 'overlay' }, onChange: onChange }, restTreeProps)); } renderLeftList(visibileItems) { const content = []; const groupStatus = new Map(); visibileItems.forEach((item, index) => { const parentGroup = item._parent; const optionContent = this.renderLeftItem(item, index); if (parentGroup && groupStatus.has(parentGroup.title)) { // group content already insert content.push(optionContent); } else if (parentGroup) { const groupContent = this.renderGroupTitle(parentGroup, index); groupStatus.set(parentGroup.title, true); content.push(groupContent); content.push(optionContent); } else { content.push(optionContent); } }); return /*#__PURE__*/_react.default.createElement("div", { className: `${prefixCls}-left-list`, role: "list", "aria-label": "Option list" }, content); } renderEmpty(type, emptyText) { const emptyCls = (0, _classnames.default)({ [`${prefixCls}-empty`]: true, [`${prefixCls}-right-empty`]: type === 'right', [`${prefixCls}-left-empty`]: type === 'left' }); return /*#__PURE__*/_react.default.createElement("div", { "aria-label": "empty", className: emptyCls }, emptyText); } renderRightSortableList(selectedData) { const sortItems = selectedData.map(item => item.key); const sortList = /*#__PURE__*/_react.default.createElement(_sortable.Sortable, { strategy: _sortable2.verticalListSortingStrategy, onSortEnd: this.onSortEnd, items: sortItems, renderItem: this.renderSortItem, prefix: `${prefixCls}-right-item`, dragOverlayCls: `${prefixCls}-right-item-drag-item-move` }); return sortList; } renderRight(locale) { const { selectedItems } = this.state; const { emptyContent, renderSelectedPanel, draggable } = this.props; const selectedData = [...selectedItems.values()]; // when custom render panel const renderProps = { length: selectedData.length, selectedData, onClear: () => this.foundation.handleClear(), onRemove: item => this.foundation.handleSelectOrRemove(item), onSortEnd: props => this.onSortEnd(props) }; if (renderSelectedPanel) { return renderSelectedPanel(renderProps); } const selectedToken = locale.selected; const selectedText = selectedToken.replace('${total}', `${selectedData.length}`); const hasValidSelected = selectedData.findIndex(item => !item.disabled) !== -1; const headerConfig = { totalContent: selectedText, allContent: locale.clear, onAllClick: () => this.foundation.handleClear(), type: 'right', showButton: Boolean(selectedData.length) && hasValidSelected, num: selectedData.length }; const headerCom = this.renderHeader(headerConfig); const emptyCom = this.renderEmpty('right', emptyContent.right ? emptyContent.right : locale.emptyRight); const panelCls = `${prefixCls}-right`; let content = null; switch (true) { // when empty case !selectedData.length: content = emptyCom; break; case selectedData.length && !draggable: const list = /*#__PURE__*/_react.default.createElement("div", { className: `${prefixCls}-right-list`, role: "list", "aria-label": "Selected list" }, selectedData.map(item => this.renderRightItem(Object.assign({}, item)))); content = list; break; case selectedData.length && draggable: content = this.renderRightSortableList(selectedData); break; default: break; } return /*#__PURE__*/_react.default.createElement("section", { className: panelCls }, headerCom, content); } render() { const _a = this.props, { className, style, disabled, renderSelectedPanel, renderSourcePanel } = _a, rest = __rest(_a, ["className", "style", "disabled", "renderSelectedPanel", "renderSourcePanel"]); const transferCls = (0, _classnames.default)(prefixCls, className, { [`${prefixCls}-disabled`]: disabled, [`${prefixCls}-custom-panel`]: renderSelectedPanel && renderSourcePanel }); return /*#__PURE__*/_react.default.createElement(_localeConsumer.default, { componentName: "Transfer" }, locale => (/*#__PURE__*/_react.default.createElement("div", Object.assign({ className: transferCls, style: style }, this.getDataAttr(rest)), this.renderLeft(locale), this.renderRight(locale)))); } } Transfer.propTypes = { style: _propTypes.default.object, className: _propTypes.default.string, disabled: _propTypes.default.bool, dataSource: _propTypes.default.array, filter: _propTypes.default.oneOfType([_propTypes.default.func, _propTypes.default.bool]), onSearch: _propTypes.default.func, inputProps: _propTypes.default.object, value: _propTypes.default.array, defaultValue: _propTypes.default.array, onChange: _propTypes.default.func, onSelect: _propTypes.default.func, onDeselect: _propTypes.default.func, renderSourceItem: _propTypes.default.func, renderSelectedItem: _propTypes.default.func, loading: _propTypes.default.bool, type: _propTypes.default.oneOf(['list', 'groupList', 'treeList']), treeProps: _propTypes.default.object, showPath: _propTypes.default.bool, emptyContent: _propTypes.default.shape({ search: _propTypes.default.node, left: _propTypes.default.node, right: _propTypes.default.node }), renderSourcePanel: _propTypes.default.func, renderSelectedPanel: _propTypes.default.func, draggable: _propTypes.default.bool }; Transfer.defaultProps = { type: _constants.strings.TYPE_LIST, dataSource: [], onSearch: _noop2.default, onChange: _noop2.default, onSelect: _noop2.default, onDeselect: _noop2.default, onClear: _noop2.default, defaultValue: [], emptyContent: {}, showPath: false }; var _default = exports.default = Transfer;