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.

463 lines (462 loc) 17.9 kB
"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 };