1 |
|
2 |
|
3 |
|
4 |
|
5 | import React from 'react';
|
6 | import PropTypes from 'prop-types';
|
7 | import { connect } from 'mini-store';
|
8 | import { KeyCode } from 'tinper-bee-core';
|
9 | import createChainedFunction from 'rc-util/lib/createChainedFunction';
|
10 | import classNames from 'classnames';
|
11 | import { getKeyFromChildrenIndex, loopMenuItem, noop, menuAllProps } from './util';
|
12 | import DOMWrap from './DOMWrap';
|
13 |
|
14 | function allDisabled(arr) {
|
15 | if (!arr.length) {
|
16 | return true;
|
17 | }
|
18 | return arr.every(c => !!c.props.disabled);
|
19 | }
|
20 |
|
21 | function updateActiveKey(store, menuId, activeKey) {
|
22 | const state = store.getState();
|
23 | store.setState({
|
24 | activeKey: {
|
25 | ...state.activeKey,
|
26 | [menuId]: activeKey,
|
27 | },
|
28 | });
|
29 | }
|
30 |
|
31 | function getEventKey(props) {
|
32 |
|
33 | return props.eventKey || '0-menu-';
|
34 | }
|
35 |
|
36 | export function getActiveKey(props, originalActiveKey) {
|
37 | let activeKey = originalActiveKey;
|
38 | const { children, eventKey } = props;
|
39 | if (activeKey) {
|
40 | let found;
|
41 | loopMenuItem(children, (c, i) => {
|
42 | if (c && !c.props.disabled && activeKey === getKeyFromChildrenIndex(c, eventKey, i)) {
|
43 | found = true;
|
44 | }
|
45 | });
|
46 | if (found) {
|
47 | return activeKey;
|
48 | }
|
49 | }
|
50 | activeKey = null;
|
51 | if (props.defaultActiveFirst) {
|
52 | loopMenuItem(children, (c, i) => {
|
53 | if (!activeKey && c && !c.props.disabled) {
|
54 | activeKey = getKeyFromChildrenIndex(c, eventKey, i);
|
55 | }
|
56 | });
|
57 | return activeKey;
|
58 | }
|
59 | return activeKey;
|
60 | }
|
61 |
|
62 | export function saveRef(c) {
|
63 | if (c) {
|
64 | const index = this.instanceArray.indexOf(c);
|
65 | if (index !== -1) {
|
66 |
|
67 | this.instanceArray[index] = c;
|
68 | } else {
|
69 |
|
70 | this.instanceArray.push(c);
|
71 | }
|
72 | }
|
73 | }
|
74 |
|
75 | export class SubPopupMenu extends React.Component {
|
76 | static propTypes = {
|
77 | onSelect: PropTypes.func,
|
78 | onClick: PropTypes.func,
|
79 | onDeselect: PropTypes.func,
|
80 | onOpenChange: PropTypes.func,
|
81 | onDestroy: PropTypes.func,
|
82 | openTransitionName: PropTypes.string,
|
83 | openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
84 | openKeys: PropTypes.arrayOf(PropTypes.string),
|
85 | visible: PropTypes.bool,
|
86 | children: PropTypes.any,
|
87 | parentMenu: PropTypes.object,
|
88 | eventKey: PropTypes.string,
|
89 | store: PropTypes.shape({
|
90 | getState: PropTypes.func,
|
91 | setState: PropTypes.func,
|
92 | }),
|
93 |
|
94 |
|
95 | focusable: PropTypes.bool,
|
96 | multiple: PropTypes.bool,
|
97 | style: PropTypes.object,
|
98 | defaultActiveFirst: PropTypes.bool,
|
99 | activeKey: PropTypes.string,
|
100 | selectedKeys: PropTypes.arrayOf(PropTypes.string),
|
101 | defaultSelectedKeys: PropTypes.arrayOf(PropTypes.string),
|
102 | defaultOpenKeys: PropTypes.arrayOf(PropTypes.string),
|
103 | level: PropTypes.number,
|
104 | mode: PropTypes.oneOf(['horizontal', 'vertical', 'vertical-left', 'vertical-right', 'inline']),
|
105 | triggerSubMenuAction: PropTypes.oneOf(['click', 'hover']),
|
106 | inlineIndent: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
107 | manualRef: PropTypes.func,
|
108 | itemIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
|
109 | expandIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
|
110 | };
|
111 |
|
112 | static defaultProps = {
|
113 | prefixCls: 'rc-menu',
|
114 | className: '',
|
115 | mode: 'vertical',
|
116 | level: 1,
|
117 | inlineIndent: 24,
|
118 | visible: true,
|
119 | focusable: true,
|
120 | style: {},
|
121 | manualRef: noop,
|
122 | };
|
123 |
|
124 | constructor(props) {
|
125 | super(props);
|
126 |
|
127 | props.store.setState({
|
128 | activeKey: {
|
129 | ...props.store.getState().activeKey,
|
130 | [props.eventKey]: getActiveKey(props, props.activeKey),
|
131 | },
|
132 | });
|
133 |
|
134 | this.instanceArray = [];
|
135 | }
|
136 |
|
137 | componentDidMount() {
|
138 |
|
139 | if (this.props.manualRef) {
|
140 | this.props.manualRef(this);
|
141 | }
|
142 | }
|
143 |
|
144 | shouldComponentUpdate(nextProps) {
|
145 | return this.props.visible || nextProps.visible;
|
146 | }
|
147 |
|
148 | componentDidUpdate(prevProps) {
|
149 | const props = this.props;
|
150 | const originalActiveKey = 'activeKey' in props ? props.activeKey :
|
151 | props.store.getState().activeKey[getEventKey(props)];
|
152 | const activeKey = getActiveKey(props, originalActiveKey);
|
153 | if (activeKey !== originalActiveKey) {
|
154 | updateActiveKey(props.store, getEventKey(props), activeKey);
|
155 | } else if ('activeKey' in prevProps) {
|
156 |
|
157 |
|
158 | const prevActiveKey = getActiveKey(prevProps, prevProps.activeKey);
|
159 | if (activeKey !== prevActiveKey) {
|
160 | updateActiveKey(props.store, getEventKey(props), activeKey);
|
161 | }
|
162 | }
|
163 | }
|
164 |
|
165 |
|
166 | onKeyDown = (e, callback) => {
|
167 | const keyCode = e.keyCode;
|
168 | let handled;
|
169 | this.getFlatInstanceArray().forEach((obj) => {
|
170 | if (obj && obj.props.active && obj.onKeyDown) {
|
171 | handled = obj.onKeyDown(e);
|
172 | }
|
173 | });
|
174 | if (handled) {
|
175 | return 1;
|
176 | }
|
177 | let activeItem = null;
|
178 | if (keyCode === KeyCode.UP || keyCode === KeyCode.DOWN) {
|
179 | if(this.props.store.getState().keyboard){
|
180 | activeItem = this.step(keyCode === KeyCode.UP ? -2 : 2)
|
181 | }
|
182 | }
|
183 |
|
184 | if (activeItem) {
|
185 | e.preventDefault();
|
186 | updateActiveKey(this.props.store, getEventKey(this.props), activeItem.props.eventKey);
|
187 |
|
188 | if (typeof callback === 'function') {
|
189 | callback(activeItem);
|
190 | }
|
191 |
|
192 | return 1;
|
193 | }
|
194 | };
|
195 |
|
196 | onItemHover = (e) => {
|
197 | const { key, hover } = e;
|
198 | updateActiveKey(this.props.store, getEventKey(this.props), hover ? key : null);
|
199 | };
|
200 |
|
201 | onDeselect = (selectInfo) => {
|
202 | this.props.onDeselect(selectInfo);
|
203 | };
|
204 |
|
205 | onSelect = (selectInfo) => {
|
206 | this.props.onSelect(selectInfo);
|
207 | };
|
208 |
|
209 | onClick = (e) => {
|
210 | this.props.onClick(e);
|
211 | };
|
212 |
|
213 | onOpenChange = (e) => {
|
214 | this.props.onOpenChange(e);
|
215 | };
|
216 |
|
217 | onDestroy = (key) => {
|
218 |
|
219 | this.props.onDestroy(key);
|
220 | };
|
221 |
|
222 | getFlatInstanceArray = () => {
|
223 | return this.instanceArray;
|
224 | };
|
225 |
|
226 | getOpenTransitionName = () => {
|
227 | return this.props.openTransitionName;
|
228 | };
|
229 |
|
230 | step = (direction) => {
|
231 | let children = this.getFlatInstanceArray();
|
232 | const activeKey = this.props.store.getState().activeKey[getEventKey(this.props)];
|
233 | const len = children.length;
|
234 | if (!len) {
|
235 | return null;
|
236 | }
|
237 | if (direction < 0) {
|
238 | children = children.concat().reverse();
|
239 | }
|
240 |
|
241 | let activeIndex = -1;
|
242 | children.every((c, ci) => {
|
243 | if (c && c.props.eventKey === activeKey) {
|
244 | activeIndex = ci;
|
245 | return false;
|
246 | }
|
247 | return true;
|
248 | });
|
249 | if (
|
250 | !this.props.defaultActiveFirst && activeIndex !== -1
|
251 | &&
|
252 | allDisabled(children.slice(activeIndex, len - 1))
|
253 | ) {
|
254 | return undefined;
|
255 | }
|
256 | const start = (activeIndex + 1) % len;
|
257 | let i = start;
|
258 |
|
259 | do {
|
260 | const child = children[i];
|
261 | if (!child || child.props.disabled) {
|
262 | i = (i + 1) % len;
|
263 | } else {
|
264 | return child;
|
265 | }
|
266 | } while (i !== start);
|
267 |
|
268 | return null;
|
269 | };
|
270 |
|
271 | renderCommonMenuItem = (child, i, extraProps) => {
|
272 | const state = this.props.store.getState();
|
273 | const props = this.props;
|
274 | const key = getKeyFromChildrenIndex(child, props.eventKey, i);
|
275 | const childProps = child.props;
|
276 | const isActive = key === state.activeKey;
|
277 | const newChildProps = {
|
278 | mode: childProps.mode || props.mode,
|
279 | level: props.level,
|
280 | inlineIndent: props.inlineIndent,
|
281 | renderMenuItem: this.renderMenuItem,
|
282 | rootPrefixCls: props.prefixCls,
|
283 | index: i,
|
284 | parentMenu: props.parentMenu,
|
285 |
|
286 | manualRef: childProps.disabled ? undefined :
|
287 | createChainedFunction(child.ref, saveRef.bind(this)),
|
288 | eventKey: key,
|
289 | active: !childProps.disabled && isActive,
|
290 | multiple: props.multiple,
|
291 | onClick: (e) => {
|
292 | (childProps.onClick || noop)(e);
|
293 | this.onClick(e);
|
294 | },
|
295 | onItemHover: this.onItemHover,
|
296 | openTransitionName: this.getOpenTransitionName(),
|
297 | openAnimation: props.openAnimation,
|
298 | subMenuOpenDelay: props.subMenuOpenDelay,
|
299 | subMenuCloseDelay: props.subMenuCloseDelay,
|
300 | forceSubMenuRender: props.forceSubMenuRender,
|
301 | onOpenChange: this.onOpenChange,
|
302 | onDeselect: this.onDeselect,
|
303 | onSelect: this.onSelect,
|
304 | builtinPlacements: props.builtinPlacements,
|
305 | itemIcon: childProps.itemIcon || this.props.itemIcon,
|
306 | expandIcon: childProps.expandIcon || this.props.expandIcon,
|
307 | ...extraProps
|
308 | };
|
309 | if (props.mode === 'inline') {
|
310 | newChildProps.triggerSubMenuAction = 'click';
|
311 | }
|
312 | return React.cloneElement(child, newChildProps);
|
313 | };
|
314 |
|
315 | renderMenuItem = (c, i, subMenuKey) => {
|
316 |
|
317 |
|
318 | if (!c) {
|
319 | return null;
|
320 | }
|
321 | const state = this.props.store.getState();
|
322 | const extraProps = {
|
323 | openKeys: state.openKeys,
|
324 | selectedKeys: state.selectedKeys,
|
325 | triggerSubMenuAction: this.props.triggerSubMenuAction,
|
326 | subMenuKey,
|
327 | };
|
328 | return this.renderCommonMenuItem(c, i, extraProps);
|
329 | };
|
330 |
|
331 | render() {
|
332 | const { ...props } = this.props;
|
333 | this.instanceArray = [];
|
334 | const className = classNames(
|
335 | props.prefixCls,
|
336 | props.className,
|
337 | `${props.prefixCls}-${props.mode}`,
|
338 | );
|
339 | const domProps = {
|
340 | className,
|
341 |
|
342 | role: props.role || 'menu',
|
343 | };
|
344 | if (props.id) {
|
345 | domProps.id = props.id;
|
346 | }
|
347 | if (props.focusable) {
|
348 | domProps.tabIndex = this.props.tabIndex;
|
349 | domProps.onKeyDown = this.onKeyDown;
|
350 | }
|
351 | const { prefixCls, eventKey, visible, level, mode, overflowedIndicator, theme } = props;
|
352 | menuAllProps.forEach(key => delete props[key]);
|
353 |
|
354 |
|
355 | delete props.onClick;
|
356 | delete props.keyboard;
|
357 |
|
358 | return (
|
359 |
|
360 |
|
361 | <DOMWrap
|
362 | {...props}
|
363 | prefixCls={prefixCls}
|
364 | mode={mode}
|
365 | tag="ul"
|
366 | level={level}
|
367 | theme={theme}
|
368 | hiddenClassName={`${prefixCls}-hidden`}
|
369 | visible={visible}
|
370 | overflowedIndicator={overflowedIndicator}
|
371 | {...domProps}
|
372 | >
|
373 | {React.Children.map(
|
374 | props.children,
|
375 | (c, i) => this.renderMenuItem(c, i, eventKey || '0-menu-'),
|
376 | )}
|
377 | </DOMWrap>
|
378 |
|
379 | );
|
380 | }
|
381 | }
|
382 | const connected = connect()(SubPopupMenu);
|
383 |
|
384 | export default connected;
|