@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.
463 lines (462 loc) • 17.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _times2 = _interopRequireDefault(require("lodash/times"));
var _findIndex2 = _interopRequireDefault(require("lodash/findIndex"));
var _map2 = _interopRequireDefault(require("lodash/map"));
var _find2 = _interopRequireDefault(require("lodash/find"));
var _throttle2 = _interopRequireDefault(require("lodash/throttle"));
var _debounce2 = _interopRequireDefault(require("lodash/debounce"));
var _noop2 = _interopRequireDefault(require("lodash/noop"));
var _react = _interopRequireDefault(require("react"));
var _baseComponent = _interopRequireDefault(require("../_base/baseComponent"));
var _propTypes = _interopRequireDefault(require("prop-types"));
var _classnames = _interopRequireDefault(require("classnames"));
var _constants = require("@douyinfe/semi-foundation/lib/cjs/scrollList/constants");
var _itemFoundation = _interopRequireDefault(require("@douyinfe/semi-foundation/lib/cjs/scrollList/itemFoundation"));
var _scrollTo = _interopRequireDefault(require("@douyinfe/semi-foundation/lib/cjs/scrollList/scrollTo"));
var _isElement = _interopRequireDefault(require("@douyinfe/semi-foundation/lib/cjs/utils/isElement"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
const msPerFrame = 1000 / 60;
const blankReg = /^\s*$/;
const wheelMode = 'wheel';
class ScrollItem extends _baseComponent.default {
constructor() {
var _this;
let props = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
super(props);
_this = this;
this._cacheNode = (name, node) => name && node && Object.prototype.hasOwnProperty.call(this, name) && (this[name] = node);
this._cacheSelectedNode = selectedNode => this._cacheNode('selectedNode', selectedNode);
this._cacheWillSelectNode = node => this._cacheNode('willSelectNode', node);
this._cacheListNode = list => this._cacheNode('list', list);
this._cacheSelectorNode = selector => this._cacheNode('selector', selector);
this._cacheWrapperNode = wrapper => this._cacheNode('wrapper', wrapper);
/* istanbul ignore next */
this._isFirst = node => {
const {
list
} = this;
if ((0, _isElement.default)(node) && (0, _isElement.default)(list)) {
const chilren = list.children;
const index = (0, _findIndex2.default)(chilren, node);
return index === 0;
}
return false;
};
/* istanbul ignore next */
this._isLast = node => {
const {
list
} = this;
if ((0, _isElement.default)(node) && (0, _isElement.default)(list)) {
const {
children
} = list;
const index = (0, _findIndex2.default)(children, node);
return index === children.length - 1;
}
return false;
};
this.indexIsSame = (index1, index2) => {
const {
list
} = this.props;
if (list.length) {
return index1 % list.length === index2 % list.length;
}
return undefined;
};
this.isDisabledIndex = index => {
const {
list
} = this.props;
if (Array.isArray(list) && list.length && index > -1) {
const size = list.length;
const indexInData = index % size;
return this.isDisabledData(list[indexInData]);
}
return false;
};
this.isDisabledNode = node => {
const listWrapper = this.list;
if ((0, _isElement.default)(node) && (0, _isElement.default)(listWrapper)) {
const index = (0, _findIndex2.default)(listWrapper.children, child => child === node);
return this.isDisabledIndex(index);
}
return false;
};
this.isDisabledData = data => data && typeof data === 'object' && data.disabled;
this.isWheelMode = () => this.props.mode === wheelMode;
this.addClassToNode = function (selectedNode) {
let selectedCls = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : _constants.cssClasses.SELECTED;
const {
list
} = _this;
selectedNode = selectedNode || _this.selectedNode;
if ((0, _isElement.default)(selectedNode) && (0, _isElement.default)(list)) {
const {
children
} = list;
const reg = new RegExp(`\\s*${selectedCls}\\s*`, 'g');
(0, _map2.default)(children, node => {
node.className = node.className && node.className.replace(reg, ' ');
if (blankReg.test(node.className)) {
node.className = '';
}
});
if (selectedNode.className && !blankReg.test(selectedNode.className)) {
selectedNode.className += ` ${selectedCls}`;
} else {
selectedNode.className = selectedCls;
}
}
};
this.getIndexByNode = node => (0, _findIndex2.default)(this.list.children, node);
this.getNodeByIndex = index => {
if (index > -1) {
return (0, _find2.default)(this.list.children, (node, idx) => idx === index);
}
const defaultSelectedNode = (0, _find2.default)(this.list.children, child => !this.isDisabledNode(child));
return defaultSelectedNode;
};
this.scrollToIndex = (selectedIndex, duration) => {
// move to selected item
duration = typeof duration === 'number' ? duration : _constants.numbers.DEFAULT_SCROLL_DURATION;
selectedIndex = selectedIndex == null ? this.props.selectedIndex : selectedIndex;
// this.isWheelMode() && this.addClassToNode();
this.scrollToNode(this.selectedNode, duration);
};
this.scrollToNode = (node, duration) => {
const {
wrapper
} = this;
const wrapperHeight = wrapper.offsetHeight;
const itemHeight = this.getItmHeight(node);
const targetTop = (node.offsetTop || this.list.children.length * itemHeight / 2) - (wrapperHeight - itemHeight) / 2;
this.scrollToPos(targetTop, duration);
};
this.scrollToPos = function (targetTop) {
let duration = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : _constants.numbers.DEFAULT_SCROLL_DURATION;
const {
wrapper
} = _this;
// this.isWheelMode() && this.addClassToNode();
if (duration && _this.props.motion) {
if (_this.scrollAnimation) {
_this.scrollAnimation.destroy();
_this.scrolling = false;
}
if (wrapper.scrollTop === targetTop) {
if (_this.isWheelMode()) {
const nodeInfo = _this.foundation.getNearestNodeInfo(_this.list, _this.selector);
_this.addClassToNode(nodeInfo.nearestNode);
}
} else {
_this.scrollAnimation = (0, _scrollTo.default)(wrapper, targetTop, duration);
_this.scrollAnimation.on('rest', () => {
if (_this.isWheelMode()) {
const nodeInfo = _this.foundation.getNearestNodeInfo(_this.list, _this.selector);
_this.addClassToNode(nodeInfo.nearestNode);
}
});
_this.scrollAnimation.start();
}
} else {
wrapper.scrollTop = targetTop;
}
};
this.scrollToSelectItem = e => {
const {
nearestNode
} = this.foundation.getNearestNodeInfo(this.list, this.selector);
if (this.props.cycled) {
this.throttledAdjustList(e, nearestNode);
}
this.debouncedSelect(e, nearestNode);
};
/**
*
* reset position to center of the scrollWrapper
*
* @param {HTMLElement} selectedNode
* @param {HTMLElement} scrollWnumber
* @param {number} duration
*/
this.scrollToCenter = (selectedNode, scrollWrapper, duration) => {
selectedNode = selectedNode || this.selectedNode;
scrollWrapper = scrollWrapper || this.wrapper;
if ((0, _isElement.default)(selectedNode) && (0, _isElement.default)(scrollWrapper)) {
const scrollRect = scrollWrapper.getBoundingClientRect();
const selectedRect = selectedNode.getBoundingClientRect();
const targetTop = scrollWrapper.scrollTop + (selectedRect.top - (scrollRect.top + scrollRect.height / 2 - selectedRect.height / 2));
this.scrollToPos(targetTop, typeof duration === 'number' ? duration : _constants.numbers.DEFAULT_SCROLL_DURATION);
}
};
this.clickToSelectItem = e => {
// const index = this.foundation.selectNearestIndex(e.nativeEvent, this.list);
e && e.nativeEvent && e.nativeEvent.stopImmediatePropagation();
const {
targetNode: node,
infoInList
} = this.foundation.getTargetNode(e, this.list);
if (node && infoInList && !infoInList.disabled) {
this.debouncedSelect(null, node);
}
};
this.getItmHeight = itm => itm && itm.offsetHeight || _constants.numbers.DEFAULT_ITEM_HEIGHT;
this.renderItemList = function () {
let prefixKey = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
const {
selectedIndex,
mode,
transform: commonTrans,
list
} = _this.props;
return list.map((item, index) => {
const {
transform: itemTrans
} = item;
const transform = typeof itemTrans === 'function' ? itemTrans : commonTrans;
const selected = selectedIndex === index;
const cls = (0, _classnames.default)({
[`${_constants.cssClasses.PREFIX}-item-sel`]: selected && mode !== wheelMode,
[`${_constants.cssClasses.PREFIX}-item-disabled`]: Boolean(item.disabled)
});
let text = '';
if (selected) {
if (typeof transform === 'function') {
text = transform(item.value, item.text);
} else {
text = item.text == null ? item.value : item.text;
}
} else {
text = item.text == null ? item.value : item.text;
}
const events = {};
if (!_this.isWheelMode() && !item.disabled) {
events.onClick = () => _this.foundation.selectIndex(index, _this.list);
}
return (
/*#__PURE__*/
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
_react.default.createElement("li", Object.assign({
key: prefixKey + index
}, events, {
className: cls,
// eslint-disable-next-line jsx-a11y/role-has-required-aria-props
role: "option",
"aria-disabled": item.disabled
}), text)
);
});
};
this.renderNormalList = () => {
const {
list,
className,
style
} = this.props;
const inner = this.renderItemList();
const wrapperCls = (0, _classnames.default)(`${_constants.cssClasses.PREFIX}-item`, className);
return /*#__PURE__*/_react.default.createElement("div", {
style: style,
className: wrapperCls,
ref: this._cacheWrapperNode
}, /*#__PURE__*/_react.default.createElement("ul", {
role: "listbox",
"aria-multiselectable": false,
"aria-label": this.props['aria-label'],
ref: this._cacheListNode
}, inner));
};
/**
* List of Rendering Unlimited Modes
*/
this.renderInfiniteList = () => {
const {
list,
cycled,
className,
style
} = this.props;
const {
prependCount,
appendCount
} = this.state;
const prependList = (0, _times2.default)(prependCount).reduce((arr, num) => {
const items = this.renderItemList(`pre_${num}_`);
arr.unshift(...items);
return arr;
}, []);
const appendList = (0, _times2.default)(appendCount).reduce((arr, num) => {
const items = this.renderItemList(`app_${num}_`);
arr.push(...items);
return arr;
}, []);
const inner = this.renderItemList();
const listWrapperCls = (0, _classnames.default)(`${_constants.cssClasses.PREFIX}-list-outer`, {
[`${_constants.cssClasses.PREFIX}-list-outer-nocycle`]: !cycled
});
const wrapperCls = (0, _classnames.default)(`${_constants.cssClasses.PREFIX}-item-wheel`, className);
const selectorCls = (0, _classnames.default)(`${_constants.cssClasses.PREFIX}-selector`);
const preShadeCls = (0, _classnames.default)(`${_constants.cssClasses.PREFIX}-shade`, `${_constants.cssClasses.PREFIX}-shade-pre`);
const postShadeCls = (0, _classnames.default)(`${_constants.cssClasses.PREFIX}-shade`, `${_constants.cssClasses.PREFIX}-shade-post`);
return /*#__PURE__*/_react.default.createElement("div", {
className: wrapperCls,
style: style
}, /*#__PURE__*/_react.default.createElement("div", {
className: preShadeCls
}), /*#__PURE__*/_react.default.createElement("div", {
className: selectorCls,
ref: this._cacheSelectorNode
}), /*#__PURE__*/_react.default.createElement("div", {
className: postShadeCls
}), /*#__PURE__*/_react.default.createElement("div", {
className: listWrapperCls,
ref: this._cacheWrapperNode,
onScroll: this.scrollToSelectItem
}, /*#__PURE__*/_react.default.createElement("ul", {
role: "listbox",
"aria-label": this.props['aria-label'],
"aria-multiselectable": false,
ref: this._cacheListNode,
onClick: this.clickToSelectItem
}, prependList, inner, appendList)));
};
this.state = {
prependCount: 0,
appendCount: 0
// selectedIndex: props.selectedIndex,
// fakeSelectedIndex: props.selectedIndex,
};
this.selectedNode = null;
this.willSelectNode = null;
this.list = null;
this.wrapper = null;
this.selector = null;
this.scrollAnimation = null;
// cache if select action comes from outside
this.foundation = new _itemFoundation.default(this.adapter);
this.throttledAdjustList = (0, _throttle2.default)((e, nearestNode) => {
this.foundation.adjustInfiniteList(this.list, this.wrapper, nearestNode);
}, msPerFrame);
this.debouncedSelect = (0, _debounce2.default)((e, nearestNode) => {
this._cacheSelectedNode(nearestNode);
this.foundation.selectNode(nearestNode, this.list);
}, msPerFrame * 2);
}
get adapter() {
var _this2 = this;
return Object.assign(Object.assign({}, super.adapter), {
setState: (states, callback) => this.setState(Object.assign({}, states), callback),
setPrependCount: prependCount => this.setState({
prependCount
}),
setAppendCount: appendCount => this.setState({
appendCount
}),
isDisabledIndex: this.isDisabledIndex,
setSelectedNode: selectedNode => this._cacheWillSelectNode(selectedNode),
notifySelectItem: function () {
return _this2.props.onSelect(...arguments);
},
scrollToCenter: this.scrollToCenter
});
}
componentWillUnmount() {
if (this.props.cycled) {
this.throttledAdjustList.cancel();
this.debouncedSelect.cancel();
}
}
componentDidMount() {
this.foundation.init();
const {
mode,
cycled,
selectedIndex,
list
} = this.props;
const selectedNode = this.getNodeByIndex(typeof selectedIndex === 'number' && selectedIndex > -1 ? selectedIndex : 0);
this._cacheSelectedNode(selectedNode);
this._cacheWillSelectNode(selectedNode);
if (mode === wheelMode && cycled) {
this.foundation.initWheelList(this.list, this.wrapper, () => {
// we have to scroll in next tick
// setTimeout(() => {
this.scrollToNode(selectedNode, 0);
// });
});
} else {
this.scrollToNode(selectedNode, 0);
}
}
componentDidUpdate(prevProps) {
const {
selectedIndex
} = this.props;
// smooth scroll to selected option
if (prevProps.selectedIndex !== selectedIndex) {
const willSelectIndex = this.getIndexByNode(this.willSelectNode);
if (!this.indexIsSame(willSelectIndex, selectedIndex)) {
const newSelectedNode = this.getNodeByOffset(this.selectedNode, selectedIndex - prevProps.selectedIndex, this.list);
this._cacheWillSelectNode(newSelectedNode);
}
this._cacheSelectedNode(this.willSelectNode);
this.scrollToIndex(selectedIndex);
}
}
/**
*
* @param {HTMLElement} refNode
* @param {number} offset
* @param {HTMLElement} listWrapper
*
* @returns {HTMLElement}
*/
getNodeByOffset(refNode, offset, listWrapper) {
const {
list
} = this.props;
if ((0, _isElement.default)(refNode) && (0, _isElement.default)(listWrapper) && typeof offset === 'number' && Array.isArray(list) && list.length) {
offset = offset % list.length;
const refIndex = this.getIndexByNode(refNode);
let targetIndex = refIndex + offset;
while (targetIndex < 0) {
targetIndex += list.length;
}
if (offset) {
return this.getNodeByIndex(targetIndex);
}
}
return refNode;
}
render() {
return this.isWheelMode() ? this.renderInfiniteList() : this.renderNormalList();
}
}
exports.default = ScrollItem;
ScrollItem.propTypes = {
mode: _propTypes.default.oneOf(_constants.strings.MODE),
cycled: _propTypes.default.bool,
list: _propTypes.default.array,
selectedIndex: _propTypes.default.number,
onSelect: _propTypes.default.func,
transform: _propTypes.default.func,
className: _propTypes.default.string,
style: _propTypes.default.object,
motion: _propTypes.default.oneOfType([_propTypes.default.func, _propTypes.default.bool]),
type: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number])
};
ScrollItem.defaultProps = {
selectedIndex: 0,
motion: true,
// transform: identity,
list: [],
onSelect: _noop2.default,
cycled: false,
mode: wheelMode
};