@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
JavaScript
"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;