1 |
|
2 |
|
3 |
|
4 |
|
5 | import React from 'react';
|
6 | import PropTypes from 'prop-types';
|
7 | import classnames from 'classnames';
|
8 | import debounce from 'lodash/debounce';
|
9 | import ResizeObserver from 'resize-observer-polyfill';
|
10 | import { setTransform, isTransformSupported } from './utils';
|
11 |
|
12 | export default class ScrollableTabBarNode extends React.Component {
|
13 | constructor(props) {
|
14 | super(props);
|
15 | this.offset = 0;
|
16 |
|
17 | this.state = {
|
18 | next: false,
|
19 | prev: false,
|
20 | };
|
21 | }
|
22 |
|
23 | componentDidMount() {
|
24 | this.componentDidUpdate();
|
25 | this.debouncedResize = debounce(() => {
|
26 | this.setNextPrev();
|
27 | this.scrollToActiveTab();
|
28 | }, 200);
|
29 | this.resizeObserver = new ResizeObserver(this.debouncedResize);
|
30 | this.resizeObserver.observe(this.props.getRef('container'));
|
31 | }
|
32 |
|
33 | componentDidUpdate(prevProps) {
|
34 | const props = this.props;
|
35 | if (prevProps && prevProps.tabBarPosition !== props.tabBarPosition) {
|
36 | this.setOffset(0);
|
37 | return;
|
38 | }
|
39 | const nextPrev = this.setNextPrev();
|
40 |
|
41 |
|
42 | if (this.isNextPrevShown(this.state) !== this.isNextPrevShown(nextPrev)) {
|
43 | this.setState({}, this.scrollToActiveTab);
|
44 | } else if (!prevProps || props.activeKey !== prevProps.activeKey) {
|
45 |
|
46 | this.scrollToActiveTab();
|
47 | }
|
48 | }
|
49 |
|
50 | componentWillUnmount() {
|
51 | if (this.resizeObserver) {
|
52 | this.resizeObserver.disconnect();
|
53 | }
|
54 | if (this.debouncedResize && this.debouncedResize.cancel) {
|
55 | this.debouncedResize.cancel();
|
56 | }
|
57 | }
|
58 |
|
59 | setNextPrev() {
|
60 | const navNode = this.props.getRef('nav');
|
61 | const navTabsContainer = this.props.getRef('navTabsContainer');
|
62 | const navNodeWH = this.getScrollWH(navTabsContainer || navNode);
|
63 |
|
64 |
|
65 | const containerWH = this.getOffsetWH(this.props.getRef('container')) + 1;
|
66 | const navWrapNodeWH = this.getOffsetWH(this.props.getRef('navWrap'));
|
67 | let { offset } = this;
|
68 | const minOffset = containerWH - navNodeWH;
|
69 | let { next, prev } = this.state;
|
70 | if (minOffset >= 0) {
|
71 | next = false;
|
72 | this.setOffset(0, false);
|
73 | offset = 0;
|
74 | } else if (minOffset < offset) {
|
75 | next = true;
|
76 | } else {
|
77 | next = false;
|
78 |
|
79 |
|
80 |
|
81 | const realOffset = navWrapNodeWH - navNodeWH;
|
82 | this.setOffset(realOffset, false);
|
83 | offset = realOffset;
|
84 | }
|
85 |
|
86 | if (offset < 0) {
|
87 | prev = true;
|
88 | } else {
|
89 | prev = false;
|
90 | }
|
91 |
|
92 | this.setNext(next);
|
93 | this.setPrev(prev);
|
94 | return {
|
95 | next,
|
96 | prev,
|
97 | };
|
98 | }
|
99 |
|
100 | getOffsetWH(node) {
|
101 | const tabBarPosition = this.props.tabBarPosition;
|
102 | let prop = 'offsetWidth';
|
103 | if (tabBarPosition === 'left' || tabBarPosition === 'right') {
|
104 | prop = 'offsetHeight';
|
105 | }
|
106 | return node[prop];
|
107 | }
|
108 |
|
109 | getScrollWH(node) {
|
110 | const tabBarPosition = this.props.tabBarPosition;
|
111 | let prop = 'scrollWidth';
|
112 | if (tabBarPosition === 'left' || tabBarPosition === 'right') {
|
113 | prop = 'scrollHeight';
|
114 | }
|
115 | return node[prop];
|
116 | }
|
117 |
|
118 |
|
119 | getOffsetLT(node) {
|
120 | const tabBarPosition = this.props.tabBarPosition;
|
121 | let prop = 'left';
|
122 | if (tabBarPosition === 'left' || tabBarPosition === 'right') {
|
123 | prop = 'top';
|
124 | }
|
125 | return node.getBoundingClientRect()[prop];
|
126 | }
|
127 |
|
128 | setOffset(offset, checkNextPrev = true) {
|
129 | let target = Math.min(0, offset);
|
130 | if (this.offset !== target) {
|
131 | this.offset = target;
|
132 | let navOffset = {};
|
133 | const tabBarPosition = this.props.tabBarPosition;
|
134 | const navStyle = this.props.getRef('nav').style;
|
135 | const transformSupported = isTransformSupported(navStyle);
|
136 | if (tabBarPosition === 'left' || tabBarPosition === 'right') {
|
137 | if (transformSupported) {
|
138 | navOffset = {
|
139 | value: `translate3d(0,${target}px,0)`,
|
140 | };
|
141 | } else {
|
142 | navOffset = {
|
143 | name: 'top',
|
144 | value: `${target}px`,
|
145 | };
|
146 | }
|
147 | } else if (transformSupported) {
|
148 | if (this.props.direction === 'rtl') {
|
149 | target = -target;
|
150 | }
|
151 | navOffset = {
|
152 | value: `translate3d(${target}px,0,0)`,
|
153 | };
|
154 | } else {
|
155 | navOffset = {
|
156 | name: 'left',
|
157 | value: `${target}px`,
|
158 | };
|
159 | }
|
160 | if (transformSupported) {
|
161 | setTransform(navStyle, navOffset.value);
|
162 | } else {
|
163 | navStyle[navOffset.name] = navOffset.value;
|
164 | }
|
165 | if (checkNextPrev) {
|
166 | this.setNextPrev();
|
167 | }
|
168 | }
|
169 | }
|
170 |
|
171 | setPrev(v) {
|
172 | if (this.state.prev !== v) {
|
173 | this.setState({
|
174 | prev: v,
|
175 | });
|
176 | }
|
177 | }
|
178 |
|
179 | setNext(v) {
|
180 | if (this.state.next !== v) {
|
181 | this.setState({
|
182 | next: v,
|
183 | });
|
184 | }
|
185 | }
|
186 |
|
187 | isNextPrevShown(state) {
|
188 | if (state) {
|
189 | return state.next || state.prev;
|
190 | }
|
191 | return this.state.next || this.state.prev;
|
192 | }
|
193 |
|
194 | prevTransitionEnd = (e) => {
|
195 | if (e.propertyName !== 'opacity') {
|
196 | return;
|
197 | }
|
198 | const container = this.props.getRef('container');
|
199 | this.scrollToActiveTab({
|
200 | target: container,
|
201 | currentTarget: container,
|
202 | });
|
203 | }
|
204 |
|
205 | scrollToActiveTab = (e) => {
|
206 | const activeTab = this.props.getRef('activeTab');
|
207 | const navWrap = this.props.getRef('navWrap');
|
208 | if (e && e.target !== e.currentTarget || !activeTab) {
|
209 | return;
|
210 | }
|
211 |
|
212 |
|
213 | const needToSroll = this.isNextPrevShown() && this.lastNextPrevShown;
|
214 | this.lastNextPrevShown = this.isNextPrevShown();
|
215 | if (!needToSroll) {
|
216 | return;
|
217 | }
|
218 |
|
219 | const activeTabWH = this.getScrollWH(activeTab);
|
220 | const navWrapNodeWH = this.getOffsetWH(navWrap);
|
221 | let { offset } = this;
|
222 | const wrapOffset = this.getOffsetLT(navWrap);
|
223 | const activeTabOffset = this.getOffsetLT(activeTab);
|
224 | if (wrapOffset > activeTabOffset) {
|
225 | offset += (wrapOffset - activeTabOffset);
|
226 | this.setOffset(offset);
|
227 | } else if ((wrapOffset + navWrapNodeWH) < (activeTabOffset + activeTabWH)) {
|
228 | offset -= (activeTabOffset + activeTabWH) - (wrapOffset + navWrapNodeWH);
|
229 | this.setOffset(offset);
|
230 | }
|
231 | }
|
232 |
|
233 | prev = (e) => {
|
234 | this.props.onPrevClick(e);
|
235 | const navWrapNode = this.props.getRef('navWrap');
|
236 | const navWrapNodeWH = this.getOffsetWH(navWrapNode);
|
237 | const { offset } = this;
|
238 | this.setOffset(offset + navWrapNodeWH);
|
239 | }
|
240 |
|
241 | next = (e) => {
|
242 | this.props.onNextClick(e);
|
243 | const navWrapNode = this.props.getRef('navWrap');
|
244 | const navWrapNodeWH = this.getOffsetWH(navWrapNode);
|
245 | const { offset } = this;
|
246 | this.setOffset(offset - navWrapNodeWH);
|
247 | }
|
248 |
|
249 | render() {
|
250 | const { next, prev } = this.state;
|
251 | const { clsPrefix,
|
252 | scrollAnimated,
|
253 | navWrapper,
|
254 | prevIcon,
|
255 | nextIcon,
|
256 | } = this.props;
|
257 | const showNextPrev = prev || next;
|
258 |
|
259 | const prevButton = (
|
260 | <span
|
261 | onClick={prev ? this.prev : null}
|
262 | unselectable="unselectable"
|
263 | className={classnames({
|
264 | [`${clsPrefix}-tab-prev`]: 1,
|
265 | [`${clsPrefix}-tab-btn-disabled`]: !prev,
|
266 | [`${clsPrefix}-tab-arrow-show`]: showNextPrev,
|
267 | })}
|
268 | onTransitionEnd={this.prevTransitionEnd}
|
269 | >
|
270 | {prevIcon || <span className={`${clsPrefix}-tab-prev-icon`} />}
|
271 | </span>
|
272 | );
|
273 |
|
274 | const nextButton = (
|
275 | <span
|
276 | onClick={next ? this.next : null}
|
277 | unselectable="unselectable"
|
278 | className={classnames({
|
279 | [`${clsPrefix}-tab-next`]: 1,
|
280 | [`${clsPrefix}-tab-btn-disabled`]: !next,
|
281 | [`${clsPrefix}-tab-arrow-show`]: showNextPrev,
|
282 | })}
|
283 | >
|
284 | {nextIcon || <span className={`${clsPrefix}-tab-next-icon`} />}
|
285 | </span>
|
286 | );
|
287 |
|
288 | const navClassName = `${clsPrefix}-nav`;
|
289 | const navClasses = classnames({
|
290 | [navClassName]: true,
|
291 | [
|
292 | scrollAnimated ?
|
293 | `${navClassName}-animated` :
|
294 | `${navClassName}-no-animated`
|
295 | ]: true,
|
296 | });
|
297 |
|
298 | return (
|
299 | <div
|
300 | className={classnames({
|
301 | [`${clsPrefix}-nav-container`]: 1,
|
302 | [`${clsPrefix}-nav-container-scrolling`]: showNextPrev,
|
303 | })}
|
304 | key="container"
|
305 | ref={this.props.saveRef('container')}
|
306 | >
|
307 | {prevButton}
|
308 | {nextButton}
|
309 | <div className={`${clsPrefix}-nav-wrap`} ref={this.props.saveRef('navWrap')}>
|
310 | <div className={`${clsPrefix}-nav-scroll`}>
|
311 | <div className={navClasses} ref={this.props.saveRef('nav')}>
|
312 | {navWrapper(this.props.children)}
|
313 | </div>
|
314 | </div>
|
315 | </div>
|
316 | </div>
|
317 | );
|
318 | }
|
319 | }
|
320 |
|
321 | ScrollableTabBarNode.propTypes = {
|
322 | activeKey: PropTypes.string,
|
323 | getRef: PropTypes.func.isRequired,
|
324 | saveRef: PropTypes.func.isRequired,
|
325 | tabBarPosition: PropTypes.oneOf(['left', 'right', 'top', 'bottom']),
|
326 | clsPrefix: PropTypes.string,
|
327 | scrollAnimated: PropTypes.bool,
|
328 | onPrevClick: PropTypes.func,
|
329 | onNextClick: PropTypes.func,
|
330 | navWrapper: PropTypes.func,
|
331 | children: PropTypes.node,
|
332 | prevIcon: PropTypes.node,
|
333 | nextIcon: PropTypes.node,
|
334 | direction: PropTypes.node,
|
335 | };
|
336 |
|
337 | ScrollableTabBarNode.defaultProps = {
|
338 | tabBarPosition: 'left',
|
339 | clsPrefix: '',
|
340 | scrollAnimated: true,
|
341 | onPrevClick: () => { },
|
342 | onNextClick: () => { },
|
343 | navWrapper: (ele) => ele,
|
344 | };
|