UNPKG

@b-design/ui-console-menu

Version:

React component for Alibaba Cloud.

439 lines (350 loc) 18.8 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _typeof3 = require("@babel/runtime/helpers/typeof"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties")); var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray")); var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray")); var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof")); var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _react = _interopRequireWildcard(require("react")); var _propTypes = _interopRequireDefault(require("prop-types")); var _reactRouterDom = require("react-router-dom"); var _ConsoleMenu = _interopRequireDefault(require("./ConsoleMenu")); var _utils = require("./utils"); var _excluded = ["location", "mapLocationToActiveKey", "items", "onItemClick", "activeKey", "staticContext", "openKeys", "onOpen", "defaultOpenKeys", "needOpenKeys"]; function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof3(obj) !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 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) { (0, _defineProperty2.default)(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; } /* eslint-disable @typescript-eslint/no-explicit-any, no-restricted-syntax */ var Context = /*#__PURE__*/_react.default.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 ((0, _utils.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.default.createElement(Context.Consumer, null, function (value) { if (!value) throw new Error("item is not rendered under RoutableMenu"); return /*#__PURE__*/_react.default.createElement(_reactRouterDom.Link, (0, _extends2.default)({}, 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.default.createElement(Context.Consumer, null, function (value) { if (!value) throw new Error("item is not rendered under RoutableMenu"); return /*#__PURE__*/_react.default.createElement("a", (0, _extends2.default)({}, 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 = (0, _reactRouterDom.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 (0, _typeof2.default)(descriptor) === 'object' && !(0, _utils.isNil)(descriptor) && !(0, _utils.isNil)(descriptor.key) && !(0, _utils.isNil)(descriptor.length) && !(0, _utils.isNil)(descriptor.match) && (0, _typeof2.default)(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 ((0, _utils.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((0, _toConsumableArray2.default)(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 && !(0, _utils.isNil)(descriptor.match)) return descriptor; return null; }; var useBuiltInLocationMatcher = function useBuiltInLocationMatcher(pathname, items) { return (0, _react.useMemo)(function () { return getBestMatchItem(items, pathname); }, [pathname, items]); }; var useMapLocationToActiveKey = function useMapLocationToActiveKey(location, mapLocationToActiveKey) { return (0, _react.useMemo)(function () { if (typeof mapLocationToActiveKey === 'function') { return mapLocationToActiveKey(location); } }, [mapLocationToActiveKey, location]); }; var useClickMonitor = function useClickMonitor(originalOnClick) { var _useState = (0, _react.useState)(''), _useState2 = (0, _slicedToArray2.default)(_useState, 2), lastClickedKey = _useState2[0], setLastClickedKey = _useState2[1]; var wrappedOnClick = (0, _react.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 = (0, _slicedToArray2.default)(_useClickMonitor, 2), lastClickedKey = _useClickMonitor2[0], wrappedOnClick = _useClickMonitor2[1]; var normalizedItems = (0, _react.useMemo)(function () { return normalizeItems(items); }, [items]); // 以上三个结果,取出优先级最高的有效值 var activeKey = [activeKeyFromLocation, matchedActiveKey, lastClickedKey].filter(function (value) { return !(0, _utils.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((0, _toConsumableArray2.default)(currentParents), [item]); } if (Array.isArray(item.items)) { var found = findParents(activeKey, item.items, [].concat((0, _toConsumableArray2.default)(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 = (0, _objectWithoutProperties2.default)(_ref, _excluded); var _useRoutableMenu = useRoutableMenu({ items: items, onItemClick: onItemClick, location: location, mapLocationToActiveKey: mapLocationToActiveKey }), _useRoutableMenu2 = (0, _slicedToArray2.default)(_useRoutableMenu, 4), normalizedItems = _useRoutableMenu2[0], derivedActiveKey = _useRoutableMenu2[1], routableOnItemClick = _useRoutableMenu2[2], providerValue = _useRoutableMenu2[3]; // 目的:计算出actualActiveKey以后,需要激活对应节点,同时要把其所有父节点展开 var _useState3 = (0, _react.useState)([]), _useState4 = (0, _slicedToArray2.default)(_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((0, _toConsumableArray2.default)(statefulOpenKeys), (0, _toConsumableArray2.default)(defaultOpenKeys)); } // 仅当用户没有传入defaultOpenKeys、openKeys时,使用智能计算出的statefulOpenKeys return statefulOpenKeys; }(); (0, _react.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 = (0, _react.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.default.createElement(Context.Provider, { value: providerValue }, /*#__PURE__*/_react.default.createElement(_ConsoleMenu.default, (0, _extends2.default)({ 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.default.propTypes), {}, { location: _propTypes.default.objectOf(_propTypes.default.any), mapLocationToActiveKey: _propTypes.default.func }); var _default = (0, _reactRouterDom.withRouter)(RoutableMenu); exports.default = _default;