UNPKG

9.25 kBJavaScriptView Raw
1import React from 'react';
2import ReactDOM from 'react-dom';
3import PropTypes from 'prop-types';
4import ResizeObserver from 'resize-observer-polyfill';
5import SubMenu from './SubMenu';
6import { getWidth, setStyle, menuAllProps } from './util';
7
8const canUseDOM = !!(
9 typeof window !== 'undefined' &&
10 window.document &&
11 window.document.createElement
12);
13
14const MENUITEM_OVERFLOWED_CLASSNAME = 'menuitem-overflowed';
15
16// Fix ssr
17if (canUseDOM) {
18 require('mutationobserver-shim');
19}
20
21class DOMWrap extends React.Component {
22 state = {
23 lastVisibleIndex: undefined,
24 };
25
26 componentDidMount() {
27 this.setChildrenWidthAndResize();
28 if (this.props.level === 1 && this.props.mode === 'horizontal') {
29 const menuUl = ReactDOM.findDOMNode(this);
30 if (!menuUl) {
31 return;
32 }
33 this.resizeObserver = new ResizeObserver(entries => {
34 entries.forEach(this.setChildrenWidthAndResize);
35 });
36
37 [].slice.call(menuUl.children).concat(menuUl).forEach(el => {
38 this.resizeObserver.observe(el);
39 });
40
41 if (typeof MutationObserver !== 'undefined') {
42 this.mutationObserver = new MutationObserver(() => {
43 this.resizeObserver.disconnect();
44 [].slice.call(menuUl.children).concat(menuUl).forEach(el => {
45 this.resizeObserver.observe(el);
46 });
47 this.setChildrenWidthAndResize();
48 });
49 this.mutationObserver.observe(
50 menuUl,
51 { attributes: false, childList: true, subTree: false }
52 );
53 }
54 }
55 }
56
57 componentWillUnmount() {
58 if (this.resizeObserver) {
59 this.resizeObserver.disconnect();
60 }
61 if (this.mutationObserver) {
62 this.resizeObserver.disconnect();
63 }
64 }
65
66 // get all valid menuItem nodes
67 getMenuItemNodes = () => {
68 const { prefixCls } = this.props;
69 const ul = ReactDOM.findDOMNode(this);
70 if (!ul) {
71 return [];
72 }
73
74 // filter out all overflowed indicator placeholder
75 return [].slice.call(ul.children)
76 .filter(node => {
77 return node.className.split(' ').indexOf(`${prefixCls}-overflowed-submenu`) < 0;
78 });
79 }
80
81 getOverflowedSubMenuItem = (keyPrefix, overflowedItems, renderPlaceholder) => {
82 const { overflowedIndicator, level, mode, prefixCls, theme, style: propStyle } = this.props;
83 if (level !== 1 || mode !== 'horizontal') {
84 return null;
85 }
86 // put all the overflowed item inside a submenu
87 // with a title of overflow indicator ('...')
88 const copy = this.props.children[0];
89 const { children: throwAway, title, eventKey, ...rest } = copy.props;
90
91 let style = { ...propStyle };
92 let key = `${keyPrefix}-overflowed-indicator`;
93
94 if (overflowedItems.length === 0 && renderPlaceholder !== true) {
95 style = {
96 ...style,
97 display: 'none',
98 };
99 } else if (renderPlaceholder) {
100 style = {
101 ...style,
102 visibility: 'hidden',
103 // prevent from taking normal dom space
104 position: 'absolute',
105 };
106 key = `${key}-placeholder`;
107 }
108
109 const popupClassName = theme ? `${prefixCls}-${theme}` : '';
110 const props = {};
111 menuAllProps.forEach(k => {
112 if (rest[k] !== undefined) {
113 props[k] = rest[k];
114 }
115 });
116
117 return (
118 <SubMenu
119 title={overflowedIndicator}
120 className={`${prefixCls}-overflowed-submenu`}
121 popupClassName={popupClassName}
122 {...props}
123 key={key}
124 eventKey={`${keyPrefix}-overflowed-indicator`}
125 disabled={false}
126 style={style}
127 >
128 {overflowedItems}
129 </SubMenu>
130 );
131 }
132
133 // memorize rendered menuSize
134 setChildrenWidthAndResize = () => {
135 if (this.props.mode !== 'horizontal') {
136 return;
137 }
138 const ul = ReactDOM.findDOMNode(this);
139
140 if (!ul) {
141 return;
142 }
143
144 const ulChildrenNodes = ul.children;
145
146 if (!ulChildrenNodes || ulChildrenNodes.length === 0) {
147 return;
148 }
149
150 const lastOverflowedIndicatorPlaceholder = ul.children[ulChildrenNodes.length - 1];
151
152 // need last overflowed indicator for calculating length;
153 setStyle(lastOverflowedIndicatorPlaceholder, 'display', 'inline-block');
154
155 const menuItemNodes = this.getMenuItemNodes();
156
157 // reset display attribute for all hidden elements caused by overflow to calculate updated width
158 // and then reset to original state after width calculation
159
160 const overflowedItems = menuItemNodes
161 .filter(c => c.className.split(' ').indexOf(MENUITEM_OVERFLOWED_CLASSNAME) >= 0);
162
163 overflowedItems.forEach(c => {
164 setStyle(c, 'display', 'inline-block');
165 });
166
167 this.menuItemSizes = menuItemNodes.map(c => getWidth(c));
168
169 overflowedItems.forEach(c => {
170 setStyle(c, 'display', 'none');
171 });
172 this.overflowedIndicatorWidth = getWidth(ul.children[ul.children.length - 1]);
173 this.originalTotalWidth = this.menuItemSizes.reduce((acc, cur) => acc + cur, 0);
174 this.handleResize();
175 // prevent the overflowed indicator from taking space;
176 setStyle(lastOverflowedIndicatorPlaceholder, 'display', 'none');
177 }
178
179 resizeObserver = null;
180 mutationObserver = null;
181
182 // original scroll size of the list
183 originalTotalWidth = 0;
184
185 // copy of overflowed items
186 overflowedItems = [];
187
188 // cache item of the original items (so we can track the size and order)
189 menuItemSizes = [];
190
191 handleResize = () => {
192 if (this.props.mode !== 'horizontal') {
193 return;
194 }
195
196 const ul = ReactDOM.findDOMNode(this);
197 if (!ul) {
198 return;
199 }
200 const width = getWidth(ul);
201
202 this.overflowedItems = [];
203 let currentSumWidth = 0;
204
205 // index for last visible child in horizontal mode
206 let lastVisibleIndex = undefined;
207
208 if (this.originalTotalWidth > width) {
209 lastVisibleIndex = -1;
210
211 this.menuItemSizes.forEach(liWidth => {
212 currentSumWidth += liWidth;
213 if (currentSumWidth + this.overflowedIndicatorWidth <= width) {
214 lastVisibleIndex++;
215 }
216 });
217 }
218
219 this.setState({ lastVisibleIndex });
220 }
221
222 renderChildren(children) {
223 // need to take care of overflowed items in horizontal mode
224 const { lastVisibleIndex } = this.state;
225 return (children || []).reduce((acc, childNode, index) => {
226 let item = childNode;
227 if (this.props.mode === 'horizontal') {
228 let overflowed = this.getOverflowedSubMenuItem(childNode.props.eventKey, []);
229 if (lastVisibleIndex !== undefined
230 &&
231 this.props.className.indexOf(`${this.props.prefixCls}-root`) !== -1
232 ) {
233 if (index > lastVisibleIndex) {
234 item = React.cloneElement(
235 childNode,
236 // 这里修改 eventKey 是为了防止隐藏状态下还会触发 openkeys 事件
237 {
238 style: { display: 'none'},
239 eventKey: `${childNode.props.eventKey}-hidden`,
240 className: `${childNode.className} ${MENUITEM_OVERFLOWED_CLASSNAME}`,
241 },
242 );
243 }
244 if (index === lastVisibleIndex + 1) {
245 this.overflowedItems = children.slice(lastVisibleIndex + 1).map(c => {
246 return React.cloneElement(
247 c,
248 // children[index].key will become '.$key' in clone by default,
249 // we have to overwrite with the correct key explicitly
250 { key: c.props.eventKey, mode: 'vertical-left' },
251 );
252 });
253
254 overflowed = this.getOverflowedSubMenuItem(
255 childNode.props.eventKey,
256 this.overflowedItems,
257 );
258 }
259 }
260
261 // const ret = [...acc, overflowed, item];//更改
262 const ret = [...acc, item];
263
264 if (index === children.length - 1) {
265 // need a placeholder for calculating overflowed indicator width
266 ret.push(this.getOverflowedSubMenuItem(childNode.props.eventKey, [], true));
267 }
268 return ret;
269 }
270 return [...acc, item];
271 }, []);
272 }
273
274 render() {
275 const {
276 hiddenClassName,
277 visible,
278 prefixCls,
279 overflowedIndicator,
280 mode,
281 level,
282 tag: Tag,
283 children,
284 theme,
285 ...rest,
286 } = this.props;
287
288 if (!visible) {
289 rest.className += ` ${hiddenClassName}`;
290 }
291
292 return (
293 <Tag {...rest}>
294 {this.renderChildren(this.props.children)}
295 </Tag>
296 );
297 }
298}
299
300DOMWrap.propTypes = {
301 className: PropTypes.string,
302 children: PropTypes.node,
303 mode: PropTypes.oneOf(['horizontal', 'vertical', 'vertical-left', 'vertical-right', 'inline']),
304 prefixCls: PropTypes.string,
305 level: PropTypes.number,
306 theme: PropTypes.string,
307 overflowedIndicator: PropTypes.node,
308 visible: PropTypes.bool,
309 hiddenClassName: PropTypes.string,
310 tag: PropTypes.string,
311 style: PropTypes.object,
312};
313
314DOMWrap.defaultProps = {
315 tag: 'div',
316 className: '',
317};
318
319export default DOMWrap;