@blocklet/ui-react 2.1.26 → 2.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -35,6 +35,8 @@ require("@iconify/iconify");
35
35
 
36
36
  var _types = require("../types");
37
37
 
38
+ var _utils = require("../utils");
39
+
38
40
  var _jsxRuntime = require("react/jsx-runtime");
39
41
 
40
42
  const _excluded = ["meta", "addons", "sessionManagerProps"];
@@ -51,18 +53,18 @@ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { va
51
53
 
52
54
  function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
53
55
 
54
- const muiTheme = (0, _Theme.create)();
56
+ const muiTheme = (0, _Theme.create)(); // blocklet meta 中的 navigation 数据 => NavMenu 组件的 items
55
57
 
56
- const isLinkActive = link => {
57
- if (window.location.pathname === '/') {
58
- return link === '/';
58
+ const parseNavigation = navigation => {
59
+ if (!(navigation !== null && navigation !== void 0 && navigation.length)) {
60
+ return {
61
+ navItems: [],
62
+ activeId: null
63
+ };
59
64
  }
60
65
 
61
- return link && link.indexOf(window.location.pathname) > -1;
62
- }; // blocklet meta 中的 navigation 数据 => NavMenu 组件的 items
66
+ let counter = 1;
63
67
 
64
-
65
- const parseNavigation = navigation => {
66
68
  const parseItem = item => {
67
69
  var _item$link, _item$link2;
68
70
 
@@ -73,9 +75,10 @@ const parseNavigation = navigation => {
73
75
 
74
76
  if (item.items) {
75
77
  return {
78
+ id: "".concat(counter++),
76
79
  label: item.title,
77
80
  icon,
78
- children: item.items.map(parseItem)
81
+ children: item.items
79
82
  };
80
83
  }
81
84
 
@@ -89,18 +92,24 @@ const parseNavigation = navigation => {
89
92
  }
90
93
 
91
94
  return {
95
+ id: "".concat(counter++),
92
96
  label: /*#__PURE__*/(0, _jsxRuntime.jsx)("a", _objectSpread(_objectSpread({
93
97
  href: item.link
94
98
  }, props), {}, {
95
99
  children: item.title
96
100
  })),
97
- active: isLinkActive(item.link),
98
- icon
101
+ icon,
102
+ link: item.link
99
103
  };
100
104
  };
101
105
 
102
- const items = navigation.map(parseItem);
103
- return items;
106
+ const navItems = (0, _utils.mapRecursive)(navigation, parseItem, 'items');
107
+ const flattened = (0, _utils.flatRecursive)(navItems);
108
+ const matchedIndex = (0, _utils.matchPaths)(flattened.map(item => item.link));
109
+ return {
110
+ navItems,
111
+ activeId: matchedIndex >= 0 ? flattened[matchedIndex].id : null
112
+ };
104
113
  };
105
114
  /**
106
115
  * 专门用于 (composable) blocklet 的 Header 组件, 解析 blocklet meta 中的数据, 通过组合 UX 中的 Header & NavMenu 组件来渲染
@@ -134,7 +143,10 @@ function Header(_ref) {
134
143
  enableConnect = true,
135
144
  enableLocale = true
136
145
  } = blocklet;
137
- const navItems = parseNavigation(navigation);
146
+ const {
147
+ navItems,
148
+ activeId
149
+ } = parseNavigation(navigation);
138
150
 
139
151
  const renderAddons = () => {
140
152
  // 不关心内置的 session manager 和 locale selector, 直接覆盖 UX Header 的 addons
@@ -187,6 +199,7 @@ function Header(_ref) {
187
199
  } = _ref2;
188
200
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_NavMenu.default, {
189
201
  mode: isMobile ? 'inline' : 'horizontal',
202
+ activeId: activeId,
190
203
  items: navItems,
191
204
  onSelected: closeMenu,
192
205
  className: "header-nav",
package/lib/utils.js CHANGED
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.mapRecursive = exports.isUrl = void 0;
6
+ exports.matchPaths = exports.matchPath = exports.mapRecursive = exports.isUrl = exports.flatRecursive = void 0;
7
7
 
8
8
  function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
9
9
 
@@ -22,13 +22,65 @@ const mapRecursive = function mapRecursive(array, fn) {
22
22
 
23
23
  return fn(item);
24
24
  });
25
- }; // "http://", "https://", "//" 开头的 3 种情况
25
+ }; // 展平有层级结构的 array
26
26
 
27
27
 
28
28
  exports.mapRecursive = mapRecursive;
29
29
 
30
+ const flatRecursive = function flatRecursive(array) {
31
+ let childrenKey = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'children';
32
+ const result = [];
33
+ mapRecursive(array, item => result.push(item), childrenKey);
34
+ return result;
35
+ }; // "http://", "https://", "//" 开头的 3 种情况
36
+
37
+
38
+ exports.flatRecursive = flatRecursive;
39
+
30
40
  const isUrl = str => {
31
41
  return /^(https?:)?\/\//.test(str);
32
42
  };
43
+ /**
44
+ * 检测 path 是否匹配当前 location, path 只考虑 "/" 开头的相对路径
45
+ */
46
+
47
+
48
+ exports.isUrl = isUrl;
49
+
50
+ const matchPath = path => {
51
+ if (!path || !(path !== null && path !== void 0 && path.startsWith('/'))) {
52
+ return false;
53
+ }
54
+
55
+ const ensureTrailingSlash = str => str.endsWith('/') ? str : "".concat(str, "/");
56
+
57
+ const pathname = ensureTrailingSlash(window.location.pathname);
58
+ const normalizedPath = ensureTrailingSlash(new URL(path, window.location.origin).pathname);
59
+ return pathname.startsWith(normalizedPath);
60
+ };
61
+ /**
62
+ * 从一组 paths 中, 找到匹配当前 location 的 path, 返回序号
63
+ */
64
+
65
+
66
+ exports.matchPath = matchPath;
67
+
68
+ const matchPaths = function matchPaths() {
69
+ let paths = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
70
+ const matched = paths.map((item, index) => ({
71
+ path: item,
72
+ index
73
+ })).filter(item => matchPath(item.path));
74
+
75
+ if (!(matched !== null && matched !== void 0 && matched.length)) {
76
+ return -1;
77
+ } // 多个 path 都匹配时, 取一个最具体 (最长的) path
78
+
79
+
80
+ const mostSpecific = matched.slice(1).reduce((prev, cur) => {
81
+ return prev.path.length >= cur.path.length ? prev : cur;
82
+ }, matched[0]);
83
+ return mostSpecific.index;
84
+ };
33
85
 
34
- exports.isUrl = isUrl;
86
+ exports.matchPaths = matchPaths;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/ui-react",
3
- "version": "2.1.26",
3
+ "version": "2.1.29",
4
4
  "description": "Some useful front-end web components that can be used in Blocklets.",
5
5
  "keywords": [
6
6
  "react",
@@ -34,8 +34,8 @@
34
34
  "url": "https://github.com/ArcBlock/ux/issues"
35
35
  },
36
36
  "dependencies": {
37
- "@arcblock/did-connect": "^2.1.26",
38
- "@arcblock/ux": "^2.1.26",
37
+ "@arcblock/did-connect": "^2.1.29",
38
+ "@arcblock/ux": "^2.1.29",
39
39
  "@iconify/iconify": "^2.2.1",
40
40
  "@mui/material": "^5.6.4",
41
41
  "core-js": "^3.6.4",
@@ -57,5 +57,5 @@
57
57
  "eslint-plugin-react-hooks": "^4.2.0",
58
58
  "jest": "^24.1.0"
59
59
  },
60
- "gitHead": "5c5282d38dad227f1855d482d7164d07a6b12ff4"
60
+ "gitHead": "710f8e28829c64030a36e3d9301358834b8937e4"
61
61
  }
@@ -14,25 +14,24 @@ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
14
14
  import '@iconify/iconify';
15
15
 
16
16
  import { blockletMetaProps, sessionManagerProps } from '../types';
17
+ import { mapRecursive, flatRecursive, matchPaths } from '../utils';
17
18
 
18
19
  const muiTheme = create();
19
20
 
20
- const isLinkActive = (link) => {
21
- if (window.location.pathname === '/') {
22
- return link === '/';
23
- }
24
- return link && link.indexOf(window.location.pathname) > -1;
25
- };
26
-
27
21
  // blocklet meta 中的 navigation 数据 => NavMenu 组件的 items
28
22
  const parseNavigation = (navigation) => {
23
+ if (!navigation?.length) {
24
+ return { navItems: [], activeId: null };
25
+ }
26
+ let counter = 1;
29
27
  const parseItem = (item) => {
30
28
  const icon = item.icon ? <span className="iconify" data-icon={item.icon} /> : null;
31
29
  if (item.items) {
32
30
  return {
31
+ id: `${counter++}`,
33
32
  label: item.title,
34
33
  icon,
35
- children: item.items.map(parseItem),
34
+ children: item.items,
36
35
  };
37
36
  }
38
37
  let props = {};
@@ -43,17 +42,20 @@ const parseNavigation = (navigation) => {
43
42
  };
44
43
  }
45
44
  return {
45
+ id: `${counter++}`,
46
46
  label: (
47
47
  <a href={item.link} {...props}>
48
48
  {item.title}
49
49
  </a>
50
50
  ),
51
- active: isLinkActive(item.link),
52
51
  icon,
52
+ link: item.link,
53
53
  };
54
54
  };
55
- const items = navigation.map(parseItem);
56
- return items;
55
+ const navItems = mapRecursive(navigation, parseItem, 'items');
56
+ const flattened = flatRecursive(navItems);
57
+ const matchedIndex = matchPaths(flattened.map((item) => item.link));
58
+ return { navItems, activeId: matchedIndex >= 0 ? flattened[matchedIndex].id : null };
57
59
  };
58
60
 
59
61
  /**
@@ -69,7 +71,7 @@ function Header({ meta, addons, sessionManagerProps, ...rest }) {
69
71
  }
70
72
 
71
73
  const { appLogo, appName, theme, navigation = [], enableConnect = true, enableLocale = true } = blocklet;
72
- const navItems = parseNavigation(navigation);
74
+ const { navItems, activeId } = parseNavigation(navigation);
73
75
 
74
76
  const renderAddons = () => {
75
77
  // 不关心内置的 session manager 和 locale selector, 直接覆盖 UX Header 的 addons
@@ -111,6 +113,7 @@ function Header({ meta, addons, sessionManagerProps, ...rest }) {
111
113
  : ({ isMobile, closeMenu }) => (
112
114
  <NavMenu
113
115
  mode={isMobile ? 'inline' : 'horizontal'}
116
+ activeId={activeId}
114
117
  items={navItems}
115
118
  onSelected={closeMenu}
116
119
  className="header-nav"
package/src/utils.js CHANGED
@@ -10,7 +10,42 @@ export const mapRecursive = (array, fn, childrenKey = 'children') => {
10
10
  });
11
11
  };
12
12
 
13
+ // 展平有层级结构的 array
14
+ export const flatRecursive = (array, childrenKey = 'children') => {
15
+ const result = [];
16
+ mapRecursive(array, (item) => result.push(item), childrenKey);
17
+ return result;
18
+ };
19
+
13
20
  // "http://", "https://", "//" 开头的 3 种情况
14
21
  export const isUrl = (str) => {
15
22
  return /^(https?:)?\/\//.test(str);
16
23
  };
24
+
25
+ /**
26
+ * 检测 path 是否匹配当前 location, path 只考虑 "/" 开头的相对路径
27
+ */
28
+ export const matchPath = (path) => {
29
+ if (!path || !path?.startsWith('/')) {
30
+ return false;
31
+ }
32
+ const ensureTrailingSlash = (str) => (str.endsWith('/') ? str : `${str}/`);
33
+ const pathname = ensureTrailingSlash(window.location.pathname);
34
+ const normalizedPath = ensureTrailingSlash(new URL(path, window.location.origin).pathname);
35
+ return pathname.startsWith(normalizedPath);
36
+ };
37
+
38
+ /**
39
+ * 从一组 paths 中, 找到匹配当前 location 的 path, 返回序号
40
+ */
41
+ export const matchPaths = (paths = []) => {
42
+ const matched = paths.map((item, index) => ({ path: item, index })).filter((item) => matchPath(item.path));
43
+ if (!matched?.length) {
44
+ return -1;
45
+ }
46
+ // 多个 path 都匹配时, 取一个最具体 (最长的) path
47
+ const mostSpecific = matched.slice(1).reduce((prev, cur) => {
48
+ return prev.path.length >= cur.path.length ? prev : cur;
49
+ }, matched[0]);
50
+ return mostSpecific.index;
51
+ };