",
19 | "license": "MIT",
20 | "bugs": {
21 | "url": "https://github.com/AlanWei/react-sider/issues"
22 | },
23 | "homepage": "https://github.com/AlanWei/react-sider#readme",
24 | "devDependencies": {
25 | "babel-cli": "^6.26.0",
26 | "babel-core": "^6.26.3",
27 | "babel-eslint": "^8.2.3",
28 | "babel-jest": "^23.0.1",
29 | "babel-plugin-transform-class-properties": "^6.24.1",
30 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
31 | "babel-preset-env": "^1.7.0",
32 | "babel-preset-react": "^6.24.1",
33 | "cross-env": "^5.2.0",
34 | "eslint": "^4.19.1",
35 | "eslint-config-airbnb": "^16.1.0",
36 | "eslint-plugin-import": "^2.12.0",
37 | "eslint-plugin-jsx-a11y": "^6.0.3",
38 | "eslint-plugin-react": "^7.8.2",
39 | "jest": "^23.6.0",
40 | "react-test-renderer": "^16.6.3",
41 | "sass": "^1.15.0"
42 | },
43 | "dependencies": {
44 | "antd": "^3.10.7",
45 | "lodash": "^4.17.11",
46 | "memoize-one": "^4.0.3",
47 | "path-to-regexp": "^2.4.0",
48 | "prop-types": "^15.6.2",
49 | "react": "^16.6.3",
50 | "react-router-dom": "^4.3.1"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Sider.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Link } from 'react-router-dom';
4 | import memoize from 'memoize-one';
5 | import map from 'lodash/map';
6 | import Menu from 'antd/lib/menu';
7 | import Icon from 'antd/lib/icon';
8 | import 'antd/lib/menu/style';
9 | import 'antd/lib/icon/style';
10 | import formatMenuPath from './utils/formatMenuPath';
11 | import getFlatMenuKeys from './utils/getFlatMenuKeys';
12 | import getMeunMatchKeys from './utils/getMeunMatchKeys';
13 | import urlToList from './utils/urlToList';
14 |
15 | const { SubMenu } = Menu;
16 |
17 | const propTypes = {
18 | prefixCls: PropTypes.string,
19 | className: PropTypes.string,
20 | style: PropTypes.object,
21 | appName: PropTypes.string,
22 | appLogo: PropTypes.string,
23 | appBaseUrl: PropTypes.string,
24 | width: PropTypes.number,
25 | menuData: PropTypes.arrayOf(PropTypes.shape({
26 | name: PropTypes.string,
27 | path: PropTypes.string,
28 | icon: PropTypes.string,
29 | children: PropTypes.array,
30 | })),
31 | pathname: PropTypes.string,
32 | };
33 |
34 | const defaultProps = {
35 | prefixCls: 'react-sider',
36 | className: '',
37 | style: {},
38 | appName: '',
39 | appLogo: '',
40 | appBaseUrl: '/',
41 | width: 256,
42 | menuData: [],
43 | pathname: '/',
44 | };
45 |
46 | class Sider extends Component {
47 | constructor(props) {
48 | super(props);
49 |
50 | this.fullPathMenuData = memoize(menuData => formatMenuPath(menuData));
51 | this.selectedKeys = memoize((pathname, fullPathMenu) => (
52 | getMeunMatchKeys(getFlatMenuKeys(fullPathMenu), urlToList(pathname))
53 | ));
54 |
55 | const { pathname, menuData } = props;
56 |
57 | this.state = {
58 | openKeys: this.selectedKeys(pathname, this.fullPathMenuData(menuData)),
59 | };
60 | }
61 |
62 | handleOpenChange = (openKeys) => {
63 | this.setState({
64 | openKeys,
65 | });
66 | };
67 |
68 | renderMenu = data => (
69 | map(data, (item) => {
70 | if (item.children) {
71 | return (
72 |
76 | {item.icon && }
77 | {item.name}
78 |
79 | }
80 | >
81 | {this.renderMenu(item.children)}
82 |
83 | );
84 | }
85 |
86 | return (
87 |
88 |
89 | {item.icon && }
90 | {item.name}
91 |
92 |
93 | );
94 | })
95 | )
96 |
97 | renderSiderHeader = () => {
98 | const {
99 | appBaseUrl,
100 | prefixCls,
101 | appLogo,
102 | appName,
103 | } = this.props;
104 |
105 | return (
106 |
107 |
108 |

113 |
114 | {appName}
115 |
116 |
117 |
118 | );
119 | }
120 |
121 | renderSiderBody = () => {
122 | const { prefixCls, pathname, menuData } = this.props;
123 | const { openKeys } = this.state;
124 |
125 | return (
126 |
127 |
137 |
138 | );
139 | }
140 |
141 | render() {
142 | const {
143 | prefixCls,
144 | className,
145 | style,
146 | width,
147 | } = this.props;
148 |
149 | const classes = `${prefixCls} ${className}`;
150 | const styles = {
151 | ...style,
152 | width,
153 | };
154 |
155 | return (
156 |
157 | {this.renderSiderHeader()}
158 | {this.renderSiderBody()}
159 |
160 | );
161 | }
162 | }
163 |
164 | Sider.propTypes = propTypes;
165 | Sider.defaultProps = defaultProps;
166 | export default Sider;
167 |
--------------------------------------------------------------------------------
/src/Sider.scss:
--------------------------------------------------------------------------------
1 | .react-sider {
2 | z-index: 1;
3 | display: flex;
4 | display: -webkit-box;
5 | display: -ms-flexbox;
6 | flex-direction: column;
7 | -webkit-box-orient: vertical;
8 | -webkit-box-direction: normal;
9 | -ms-flex-direction: column;
10 | width: 256px;
11 | box-shadow: 2px 0 8px rgba(0, 0, 0, .15);
12 | &-header {
13 | display: flex;
14 | align-items: center;
15 | background: #002140;
16 | height: 64px;
17 | padding-left: 24px;
18 | }
19 | &-body {
20 | flex: 1;
21 | box-flex: 1;
22 | -webkit-box-flex: 1;
23 | -ms-flex: 1;
24 | background: #001529;
25 | }
26 | &-logo {
27 | height: 48px;
28 | width: 48px;
29 | }
30 | &-appName {
31 | color: #ffffff;
32 | font-weight: bold;
33 | font-size: 20px;
34 | }
35 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Sider from './Sider';
2 |
3 | export default Sider;
4 |
--------------------------------------------------------------------------------
/src/utils/formatMenuPath.js:
--------------------------------------------------------------------------------
1 | import map from 'lodash/map';
2 |
3 | const formatMenuPath = (data, parentPath = '/') => (
4 | map(data, (item) => {
5 | const result = {
6 | ...item,
7 | path: `${parentPath}${item.path}`,
8 | };
9 | if (item.children) {
10 | result.children = formatMenuPath(item.children, `${parentPath}${item.path}/`);
11 | }
12 | return result;
13 | })
14 | );
15 |
16 | export default formatMenuPath;
17 |
--------------------------------------------------------------------------------
/src/utils/formatMenuPath.spec.js:
--------------------------------------------------------------------------------
1 | import formatMenuPath from './formatMenuPath';
2 |
3 | test('empty menu', () => {
4 | expect(formatMenuPath([])).toEqual([]);
5 | });
6 |
7 | test('simple menu', () => {
8 | const simpleMenu = [{
9 | path: 'dashboard',
10 | }, {
11 | path: 'user',
12 | }, {
13 | path: 'form',
14 | }];
15 |
16 | expect(formatMenuPath(simpleMenu)).toEqual([{
17 | path: '/dashboard',
18 | }, {
19 | path: '/user',
20 | }, {
21 | path: '/form',
22 | }]);
23 | });
24 |
25 |
26 | test('nested menu', () => {
27 | const nestedMenu = [{
28 | path: 'dashboard',
29 | children: [{
30 | path: 'analysis',
31 | children: [{
32 | path: 'realtime',
33 | }, {
34 | path: 'offline',
35 | }],
36 | }, {
37 | path: 'monitor',
38 | }],
39 | }, {
40 | path: 'user',
41 | }, {
42 | path: 'form',
43 | }];
44 |
45 | expect(formatMenuPath(nestedMenu)).toEqual([{
46 | path: '/dashboard',
47 | children: [{
48 | path: '/dashboard/analysis',
49 | children: [{
50 | path: '/dashboard/analysis/realtime',
51 | }, {
52 | path: '/dashboard/analysis/offline',
53 | }],
54 | }, {
55 | path: '/dashboard/monitor',
56 | }],
57 | }, {
58 | path: '/user',
59 | }, {
60 | path: '/form',
61 | }]);
62 | });
63 |
--------------------------------------------------------------------------------
/src/utils/getFlatMenuKeys.js:
--------------------------------------------------------------------------------
1 | import reduce from 'lodash/reduce';
2 |
3 | const getFlatMenuKeys = menuData => (
4 | reduce(menuData, (keys, item) => {
5 | keys.push(item.path);
6 | if (item.children) {
7 | return keys.concat(getFlatMenuKeys(item.children));
8 | }
9 | return keys;
10 | }, [])
11 | );
12 |
13 | export default getFlatMenuKeys;
14 |
--------------------------------------------------------------------------------
/src/utils/getFlatMenuKeys.spec.js:
--------------------------------------------------------------------------------
1 | import getFlatMenuKeys from './getFlatMenuKeys';
2 |
3 | test('empty menu', () => {
4 | expect(getFlatMenuKeys([])).toEqual([]);
5 | });
6 |
7 | test('flat menu', () => {
8 | const simpleMenu = [{
9 | path: '/dashboard',
10 | }, {
11 | path: '/user',
12 | }, {
13 | path: '/form',
14 | }];
15 | expect(getFlatMenuKeys(simpleMenu)).toEqual(['/dashboard', '/user', '/form']);
16 | });
17 |
18 | test('nested menu', () => {
19 | const nestedMenu = [{
20 | path: '/dashboard',
21 | children: [{
22 | path: '/dashboard/analysis',
23 | children: [{
24 | path: '/dashboard/analysis/realtime',
25 | }, {
26 | path: '/dashboard/analysis/offline',
27 | }],
28 | }, {
29 | path: '/dashboard/monitor',
30 | }],
31 | }, {
32 | path: '/user',
33 | }, {
34 | path: '/form',
35 | }];
36 | expect(getFlatMenuKeys(nestedMenu)).toEqual([
37 | '/dashboard',
38 | '/dashboard/analysis',
39 | '/dashboard/analysis/realtime',
40 | '/dashboard/analysis/offline',
41 | '/dashboard/monitor',
42 | '/user',
43 | '/form',
44 | ]);
45 | });
46 |
--------------------------------------------------------------------------------
/src/utils/getMeunMatchKeys.js:
--------------------------------------------------------------------------------
1 | import pathToRegexp from 'path-to-regexp';
2 | import reduce from 'lodash/reduce';
3 | import filter from 'lodash/filter';
4 |
5 | const getMeunMatchKeys = (flatMenuKeys, paths) =>
6 | reduce(paths, (matchKeys, path) => (
7 | matchKeys.concat(filter(flatMenuKeys, item => pathToRegexp(item).test(path)))
8 | ), []);
9 |
10 | export default getMeunMatchKeys;
11 |
--------------------------------------------------------------------------------
/src/utils/getMeunMatchKeys.spec.js:
--------------------------------------------------------------------------------
1 | import getMeunMatchKeys from './getMeunMatchKeys';
2 | import urlToList from './urlToList';
3 |
4 | const flatMenuKeys = [
5 | '/dashboard',
6 | '/dashboard/analysis',
7 | '/dashboard/analysis/realtime',
8 | '/dashboard/analysis/offline',
9 | '/dashboard/monitor',
10 | '/user',
11 | '/form',
12 | ];
13 |
14 | test('empty path', () => {
15 | expect(getMeunMatchKeys(flatMenuKeys, urlToList(''))).toEqual([]);
16 | });
17 |
18 | test('simple path', () => {
19 | expect(getMeunMatchKeys(flatMenuKeys, urlToList('/dashboard'))).toEqual(['/dashboard']);
20 | });
21 |
22 | test('error path', () => {
23 | expect(getMeunMatchKeys(flatMenuKeys, urlToList('/dashboardabc'))).toEqual([]);
24 | });
25 |
26 | test('nested path', () => {
27 | expect(getMeunMatchKeys(flatMenuKeys, urlToList('/dashboard/analysis/realtime'))).toEqual([
28 | '/dashboard',
29 | '/dashboard/analysis',
30 | '/dashboard/analysis/realtime',
31 | ]);
32 | });
33 |
--------------------------------------------------------------------------------
/src/utils/urlToList.js:
--------------------------------------------------------------------------------
1 | import map from 'lodash/map';
2 |
3 | const urlToList = (url) => {
4 | if (url) {
5 | const urlList = url.split('/').filter(i => i);
6 | return map(urlList, (urlItem, index) => `/${urlList.slice(0, index + 1).join('/')}`);
7 | }
8 | return [];
9 | };
10 |
11 | export default urlToList;
12 |
--------------------------------------------------------------------------------
/src/utils/urlToList.spec.js:
--------------------------------------------------------------------------------
1 | import urlToList from './urlToList';
2 |
3 | test('undefined path', () => {
4 | expect(urlToList(undefined)).toEqual([]);
5 | });
6 |
7 | test('empty path', () => {
8 | expect(urlToList('')).toEqual([]);
9 | });
10 |
11 | test('simple path', () => {
12 | expect(urlToList('/dashboard')).toEqual(['/dashboard']);
13 | });
14 |
15 | test('nested path', () => {
16 | expect(urlToList('/dashboard/analysis/realtime')).toEqual([
17 | '/dashboard',
18 | '/dashboard/analysis',
19 | '/dashboard/analysis/realtime',
20 | ]);
21 | });
22 |
--------------------------------------------------------------------------------