@b-design/ui-console-menu
Version:
React component for Alibaba Cloud.
411 lines (340 loc) • 16.6 kB
JavaScript
import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties";
import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
import _toConsumableArray from "@babel/runtime/helpers/esm/toConsumableArray";
import _typeof from "@babel/runtime/helpers/esm/typeof";
import _extends from "@babel/runtime/helpers/esm/extends";
import _defineProperty from "@babel/runtime/helpers/esm/defineProperty";
var _excluded = ["location", "mapLocationToActiveKey", "items", "onItemClick", "activeKey", "staticContext", "openKeys", "onOpen", "defaultOpenKeys", "needOpenKeys"];
function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
import React, { useMemo, useState, useCallback, useLayoutEffect } from 'react';
import PropTypes from 'prop-types';
import { withRouter, Link, matchPath } from 'react-router-dom';
import ConsoleMenu from './ConsoleMenu';
import { isNil } from './utils';
/* eslint-disable @typescript-eslint/no-explicit-any, no-restricted-syntax */
var Context = /*#__PURE__*/React.createContext(null);
var normalizeItems = function normalizeItems() {
var items = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
return items.map(function (item) {
var to = item.to,
href = item.href,
_item$linkProps = item.linkProps,
linkProps = _item$linkProps === void 0 ? {} : _item$linkProps,
disabled = item.disabled,
render = item.render,
subItems = item.items;
if (subItems) {
return _objectSpread(_objectSpread({}, item), {}, {
items: normalizeItems(subItems)
});
}
if (isNil(render) && !disabled) {
// create leaf item a default renderer
if (to) {
// this is a in-app link, render `<Link>`
return _objectSpread(_objectSpread({}, item), {}, {
render: function render(renderItem) {
return /*#__PURE__*/React.createElement(Context.Consumer, null, function (value) {
if (!value) throw new Error("item is not rendered under RoutableMenu");
return /*#__PURE__*/React.createElement(Link, _extends({}, linkProps, {
// menu内部的链接不能被focus,
// 必须通过menu的键盘机制来选中
tabIndex: "-1",
to: typeof renderItem.to === 'function' ? renderItem.to(value, renderItem) : renderItem.to,
style: {
color: 'inherit'
}
}), renderItem.label);
});
}
});
}
if (href) {
// this is a web link, render `<a>`
return _objectSpread(_objectSpread({}, item), {}, {
render: function render(renderItem) {
return /*#__PURE__*/React.createElement(Context.Consumer, null, function (value) {
if (!value) throw new Error("item is not rendered under RoutableMenu");
return /*#__PURE__*/React.createElement("a", _extends({}, renderItem.linkProps, {
href: typeof renderItem.href === 'function' ? renderItem.href(value, renderItem) : renderItem.href,
style: {
color: 'inherit'
}
}), renderItem.label);
});
}
});
}
}
return item;
});
};
var getBestMatchPattern = function getBestMatchPattern(pathname, pathPatterns) {
var currentBest = {
length: Number.MAX_VALUE,
match: null
};
var _iterator = _createForOfIteratorHelper(pathPatterns),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var path = _step.value;
var match = matchPath(pathname, path); // Check if the path matches the location.pathname.
if (match) {
// The identifier that described the complexity of path matching.
// The shorter the length, the less the matching overhead,
// and the most similar between pathname and pattern.
var length = 0; // To mutate path's length by couting params.
// Every matched param will increase a bit of length.
// @woota provided the idea and original algorithm in previous version.
//
// e.g.:
// If the location.pathname is `/foo/bar/baz`:
// - To match `/foo/bar/baz`, the length is `0`
// - To match `/foo/bar/:id`, the length is `1`
// - To match `/foo/:name/:id`, the length is `2`
//
if (match.params) {
length += Object.keys(match.params).length;
} // Add a large number if the path NOT matches the location.pathname exactly.
if (!match.isExact) {
// Assuming params' count less than 1000
length += 1000;
} // Compare the length to previous length and pick up the shorter one.
if (length < currentBest.length) {
currentBest = {
length: length,
match: match
};
}
}
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
if (currentBest.match && currentBest.length < Number.MAX_VALUE) return currentBest;
return null;
};
var isValidDescriptor = function isValidDescriptor(descriptor) {
return _typeof(descriptor) === 'object' && !isNil(descriptor) && !isNil(descriptor.key) && !isNil(descriptor.length) && !isNil(descriptor.match) && _typeof(descriptor.match) === 'object';
};
var compareDescriptor = function compareDescriptor(prev, next) {
if (!isValidDescriptor(next)) {
return prev;
} // Returns newer descriptor if older descriptor is invalid,
// or it has a shorter length.
if (!isValidDescriptor(prev) || next.length < prev.length) {
return next;
}
return prev;
};
var getBestMatchItem = function getBestMatchItem(items, pathname) {
var initialDescriptor = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {
key: 'dummy',
match: null,
length: Number.MAX_VALUE
};
var descriptor = initialDescriptor;
if (isNil(items)) return null;
var _iterator2 = _createForOfIteratorHelper(items),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var item = _step2.value;
var nextDescriptor = void 0;
if (item.items) {
nextDescriptor = getBestMatchItem(item.items, pathname, descriptor);
} else {
var key = item.key,
_item$activePathPatte = item.activePathPatterns,
activePathPatterns = _item$activePathPatte === void 0 ? [] : _item$activePathPatte;
nextDescriptor = _objectSpread({
key: key,
length: Number.MAX_VALUE,
match: null
}, getBestMatchPattern(pathname, [key].concat(_toConsumableArray(activePathPatterns))));
}
descriptor = compareDescriptor(descriptor, nextDescriptor); // return descriptor with length === 0 immediately
if (descriptor && descriptor.length <= 0) {
return descriptor;
}
}
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
if (descriptor.length < Number.MAX_VALUE && !isNil(descriptor.match)) return descriptor;
return null;
};
var useBuiltInLocationMatcher = function useBuiltInLocationMatcher(pathname, items) {
return useMemo(function () {
return getBestMatchItem(items, pathname);
}, [pathname, items]);
};
var useMapLocationToActiveKey = function useMapLocationToActiveKey(location, mapLocationToActiveKey) {
return useMemo(function () {
if (typeof mapLocationToActiveKey === 'function') {
return mapLocationToActiveKey(location);
}
}, [mapLocationToActiveKey, location]);
};
var useClickMonitor = function useClickMonitor(originalOnClick) {
var _useState = useState(''),
_useState2 = _slicedToArray(_useState, 2),
lastClickedKey = _useState2[0],
setLastClickedKey = _useState2[1];
var wrappedOnClick = useCallback(function () {
if (typeof originalOnClick === 'function') {
originalOnClick.apply(void 0, arguments);
}
setLastClickedKey(arguments.length <= 0 ? undefined : arguments[0]);
}, [originalOnClick]);
return [lastClickedKey, wrappedOnClick];
};
/**
* 根据location,计算出应该active哪个item,有3种计算方式依次尝试:
* 1. 用户提供了mapLocationToActiveKey,则直接mapLocationToActiveKey(location)
* 2. 将pathname与item的key匹配,得到最接近的item
* 3. 如果上述方法都不行,则直接使用用户最近点击的item
*/
var useRoutableMenu = function useRoutableMenu(options) {
var items = options.items,
mapLocationToActiveKey = options.mapLocationToActiveKey,
onItemClick = options.onItemClick,
location = options.location;
var pathname = location.pathname; // 1. 用户提供了mapLocationToActiveKey,则直接mapLocationToActiveKey(location)
var activeKeyFromLocation = useMapLocationToActiveKey(location, mapLocationToActiveKey); // 2. 将pathname与item的key匹配,得到最接近的item
var matchResult = useBuiltInLocationMatcher(pathname, items) || {
key: undefined,
match: undefined
};
var matchedActiveKey = matchResult.key,
match = matchResult.match; // 3. 如果上述方法都不行,则直接使用用户最近点击的item
var _useClickMonitor = useClickMonitor(onItemClick),
_useClickMonitor2 = _slicedToArray(_useClickMonitor, 2),
lastClickedKey = _useClickMonitor2[0],
wrappedOnClick = _useClickMonitor2[1];
var normalizedItems = useMemo(function () {
return normalizeItems(items);
}, [items]); // 以上三个结果,取出优先级最高的有效值
var activeKey = [activeKeyFromLocation, matchedActiveKey, lastClickedKey].filter(function (value) {
return !isNil(value);
})[0];
return [normalizedItems, activeKey, wrappedOnClick, {
match: match,
location: location
}];
}; // 从一个树中,找出某个节点的所有父节点
function findParents(activeKey, items, currentParents) {
var _iterator3 = _createForOfIteratorHelper(items),
_step3;
try {
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
var item = _step3.value;
if (item.key === activeKey) {
return [].concat(_toConsumableArray(currentParents), [item]);
}
if (Array.isArray(item.items)) {
var found = findParents(activeKey, item.items, [].concat(_toConsumableArray(currentParents), [item]));
if (found) return found;
}
}
} catch (err) {
_iterator3.e(err);
} finally {
_iterator3.f();
}
return null;
}
var RoutableMenu = function RoutableMenu(_ref) {
var location = _ref.location,
mapLocationToActiveKey = _ref.mapLocationToActiveKey,
items = _ref.items,
onItemClick = _ref.onItemClick,
activeKey = _ref.activeKey,
staticContext = _ref.staticContext,
openKeys = _ref.openKeys,
onOpen = _ref.onOpen,
defaultOpenKeys = _ref.defaultOpenKeys,
_ref$needOpenKeys = _ref.needOpenKeys,
needOpenKeys = _ref$needOpenKeys === void 0 ? true : _ref$needOpenKeys,
restProps = _objectWithoutProperties(_ref, _excluded);
var _useRoutableMenu = useRoutableMenu({
items: items,
onItemClick: onItemClick,
location: location,
mapLocationToActiveKey: mapLocationToActiveKey
}),
_useRoutableMenu2 = _slicedToArray(_useRoutableMenu, 4),
normalizedItems = _useRoutableMenu2[0],
derivedActiveKey = _useRoutableMenu2[1],
routableOnItemClick = _useRoutableMenu2[2],
providerValue = _useRoutableMenu2[3]; // 目的:计算出actualActiveKey以后,需要激活对应节点,同时要把其所有父节点展开
var _useState3 = useState([]),
_useState4 = _slicedToArray(_useState3, 2),
statefulOpenKeys = _useState4[0],
setOpenKeys = _useState4[1]; // 当用户传入activeKey时,为受控模式,不使用自己计算的derivedActiveKey
var actualActiveKey = activeKey || derivedActiveKey;
var actualOpenKeys = function () {
if (!needOpenKeys) return false;
if (openKeys) return openKeys;
if (defaultOpenKeys && statefulOpenKeys) {
return [].concat(_toConsumableArray(statefulOpenKeys), _toConsumableArray(defaultOpenKeys));
} // 仅当用户没有传入defaultOpenKeys、openKeys时,使用智能计算出的statefulOpenKeys
return statefulOpenKeys;
}();
useLayoutEffect(function () {
var newOpenKeys = function () {
if (actualActiveKey) {
// 找出激活节点的所有父节点
var parentItems = findParents(actualActiveKey, normalizedItems, []);
if (Array.isArray(parentItems)) {
return parentItems.map(function (i) {
return i.key;
});
}
}
return undefined;
}();
setOpenKeys(newOpenKeys);
}, [actualActiveKey, normalizedItems]);
var actualOnOpen = useCallback(function (key, extra) {
if (typeof onOpen === 'function') {
onOpen(key, extra);
}
setOpenKeys(key);
}, [onOpen]); // openKeys传入undefined也会被认为是受控模式,因此如果想非受控,不能传入这个prop
var openKeysProp = actualOpenKeys ? {
openKeys: actualOpenKeys
} : undefined;
return /*#__PURE__*/React.createElement(Context.Provider, {
value: providerValue
}, /*#__PURE__*/React.createElement(ConsoleMenu, _extends({
items: normalizedItems,
activeKey: actualActiveKey,
defaultOpenKeys: defaultOpenKeys,
onOpen: actualOnOpen,
onItemClick: function onItemClick(key, item, event) {
if (event.type === 'keydown' && event.target) {
// 通过键盘点击菜单时,需要点击内部的链接,完成路由跳转
var linkEl = event.target.querySelector('div span a');
if (linkEl && linkEl.click) {
linkEl.click();
}
}
routableOnItemClick(key, item, event);
}
}, openKeysProp, restProps)));
};
RoutableMenu.propTypes = _objectSpread(_objectSpread({}, ConsoleMenu.propTypes), {}, {
location: PropTypes.objectOf(PropTypes.any),
mapLocationToActiveKey: PropTypes.func
});
export default withRouter(RoutableMenu);