@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.
455 lines • 16.5 kB
JavaScript
import _times from "lodash/times";
import _findIndex from "lodash/findIndex";
import _map from "lodash/map";
import _find from "lodash/find";
import _throttle from "lodash/throttle";
import _debounce from "lodash/debounce";
import _noop from "lodash/noop";
import React from 'react';
import BaseComponent from '../_base/baseComponent';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { cssClasses, numbers, strings } from '@douyinfe/semi-foundation/lib/es/scrollList/constants';
import ItemFoundation from '@douyinfe/semi-foundation/lib/es/scrollList/itemFoundation';
import animatedScrollTo from '@douyinfe/semi-foundation/lib/es/scrollList/scrollTo';
import isElement from '@douyinfe/semi-foundation/lib/es/utils/isElement';
const msPerFrame = 1000 / 60;
const blankReg = /^\s*$/;
const wheelMode = 'wheel';
export default class ScrollItem extends BaseComponent {
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 (isElement(node) && isElement(list)) {
const chilren = list.children;
const index = _findIndex(chilren, node);
return index === 0;
}
return false;
};
/* istanbul ignore next */
this._isLast = node => {
const {
list
} = this;
if (isElement(node) && isElement(list)) {
const {
children
} = list;
const index = _findIndex(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 (isElement(node) && isElement(listWrapper)) {
const index = _findIndex(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] : cssClasses.SELECTED;
const {
list
} = _this;
selectedNode = selectedNode || _this.selectedNode;
if (isElement(selectedNode) && isElement(list)) {
const {
children
} = list;
const reg = new RegExp(`\\s*${selectedCls}\\s*`, 'g');
_map(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 => _findIndex(this.list.children, node);
this.getNodeByIndex = index => {
if (index > -1) {
return _find(this.list.children, (node, idx) => idx === index);
}
const defaultSelectedNode = _find(this.list.children, child => !this.isDisabledNode(child));
return defaultSelectedNode;
};
this.scrollToIndex = (selectedIndex, duration) => {
// move to selected item
duration = typeof duration === 'number' ? duration : 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] : 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 = animatedScrollTo(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 (isElement(selectedNode) && isElement(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 : 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 || 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 = classnames({
[`${cssClasses.PREFIX}-item-sel`]: selected && mode !== wheelMode,
[`${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.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 = classnames(`${cssClasses.PREFIX}-item`, className);
return /*#__PURE__*/React.createElement("div", {
style: style,
className: wrapperCls,
ref: this._cacheWrapperNode
}, /*#__PURE__*/React.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 = _times(prependCount).reduce((arr, num) => {
const items = this.renderItemList(`pre_${num}_`);
arr.unshift(...items);
return arr;
}, []);
const appendList = _times(appendCount).reduce((arr, num) => {
const items = this.renderItemList(`app_${num}_`);
arr.push(...items);
return arr;
}, []);
const inner = this.renderItemList();
const listWrapperCls = classnames(`${cssClasses.PREFIX}-list-outer`, {
[`${cssClasses.PREFIX}-list-outer-nocycle`]: !cycled
});
const wrapperCls = classnames(`${cssClasses.PREFIX}-item-wheel`, className);
const selectorCls = classnames(`${cssClasses.PREFIX}-selector`);
const preShadeCls = classnames(`${cssClasses.PREFIX}-shade`, `${cssClasses.PREFIX}-shade-pre`);
const postShadeCls = classnames(`${cssClasses.PREFIX}-shade`, `${cssClasses.PREFIX}-shade-post`);
return /*#__PURE__*/React.createElement("div", {
className: wrapperCls,
style: style
}, /*#__PURE__*/React.createElement("div", {
className: preShadeCls
}), /*#__PURE__*/React.createElement("div", {
className: selectorCls,
ref: this._cacheSelectorNode
}), /*#__PURE__*/React.createElement("div", {
className: postShadeCls
}), /*#__PURE__*/React.createElement("div", {
className: listWrapperCls,
ref: this._cacheWrapperNode,
onScroll: this.scrollToSelectItem
}, /*#__PURE__*/React.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(this.adapter);
this.throttledAdjustList = _throttle((e, nearestNode) => {
this.foundation.adjustInfiniteList(this.list, this.wrapper, nearestNode);
}, msPerFrame);
this.debouncedSelect = _debounce((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 (isElement(refNode) && isElement(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();
}
}
ScrollItem.propTypes = {
mode: PropTypes.oneOf(strings.MODE),
cycled: PropTypes.bool,
list: PropTypes.array,
selectedIndex: PropTypes.number,
onSelect: PropTypes.func,
transform: PropTypes.func,
className: PropTypes.string,
style: PropTypes.object,
motion: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
type: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
};
ScrollItem.defaultProps = {
selectedIndex: 0,
motion: true,
// transform: identity,
list: [],
onSelect: _noop,
cycled: false,
mode: wheelMode
};