SubMenu.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. import _mergeJSXProps from 'babel-helper-vue-jsx-merge-props';
  2. import _typeof from 'babel-runtime/helpers/typeof';
  3. import _defineProperty from 'babel-runtime/helpers/defineProperty';
  4. import _extends from 'babel-runtime/helpers/extends';
  5. import omit from 'omit.js';
  6. import PropTypes from '../_util/vue-types';
  7. import Trigger from '../vc-trigger';
  8. import KeyCode from '../_util/KeyCode';
  9. import { connect } from '../_util/store';
  10. import SubPopupMenu from './SubPopupMenu';
  11. import placements from './placements';
  12. import BaseMixin from '../_util/BaseMixin';
  13. import { getComponentFromProp, filterEmpty, getListeners } from '../_util/props-util';
  14. import { requestAnimationTimeout, cancelAnimationTimeout } from '../_util/requestAnimationTimeout';
  15. import { noop, loopMenuItemRecursively, getMenuIdFromSubMenuEventKey } from './util';
  16. import getTransitionProps from '../_util/getTransitionProps';
  17. var guid = 0;
  18. var popupPlacementMap = {
  19. horizontal: 'bottomLeft',
  20. vertical: 'rightTop',
  21. 'vertical-left': 'rightTop',
  22. 'vertical-right': 'leftTop'
  23. };
  24. var updateDefaultActiveFirst = function updateDefaultActiveFirst(store, eventKey, defaultActiveFirst) {
  25. var menuId = getMenuIdFromSubMenuEventKey(eventKey);
  26. var state = store.getState();
  27. store.setState({
  28. defaultActiveFirst: _extends({}, state.defaultActiveFirst, _defineProperty({}, menuId, defaultActiveFirst))
  29. });
  30. };
  31. var SubMenu = {
  32. name: 'SubMenu',
  33. props: {
  34. parentMenu: PropTypes.object,
  35. title: PropTypes.any,
  36. selectedKeys: PropTypes.array.def([]),
  37. openKeys: PropTypes.array.def([]),
  38. openChange: PropTypes.func.def(noop),
  39. rootPrefixCls: PropTypes.string,
  40. eventKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  41. multiple: PropTypes.bool,
  42. active: PropTypes.bool, // TODO: remove
  43. isRootMenu: PropTypes.bool.def(false),
  44. index: PropTypes.number,
  45. triggerSubMenuAction: PropTypes.string,
  46. popupClassName: PropTypes.string,
  47. getPopupContainer: PropTypes.func,
  48. forceSubMenuRender: PropTypes.bool,
  49. openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
  50. disabled: PropTypes.bool,
  51. subMenuOpenDelay: PropTypes.number.def(0.1),
  52. subMenuCloseDelay: PropTypes.number.def(0.1),
  53. level: PropTypes.number.def(1),
  54. inlineIndent: PropTypes.number.def(24),
  55. openTransitionName: PropTypes.string,
  56. popupOffset: PropTypes.array,
  57. isOpen: PropTypes.bool,
  58. store: PropTypes.object,
  59. mode: PropTypes.oneOf(['horizontal', 'vertical', 'vertical-left', 'vertical-right', 'inline']).def('vertical'),
  60. manualRef: PropTypes.func.def(noop),
  61. builtinPlacements: PropTypes.object.def(function () {
  62. return {};
  63. }),
  64. itemIcon: PropTypes.any,
  65. expandIcon: PropTypes.any,
  66. subMenuKey: PropTypes.string
  67. },
  68. mixins: [BaseMixin],
  69. isSubMenu: true,
  70. data: function data() {
  71. var props = this.$props;
  72. var store = props.store;
  73. var eventKey = props.eventKey;
  74. var defaultActiveFirst = store.getState().defaultActiveFirst;
  75. var value = false;
  76. if (defaultActiveFirst) {
  77. value = defaultActiveFirst[eventKey];
  78. }
  79. updateDefaultActiveFirst(store, eventKey, value);
  80. return {
  81. // defaultActiveFirst: false,
  82. };
  83. },
  84. mounted: function mounted() {
  85. var _this = this;
  86. this.$nextTick(function () {
  87. _this.handleUpdated();
  88. });
  89. },
  90. updated: function updated() {
  91. var _this2 = this;
  92. this.$nextTick(function () {
  93. _this2.handleUpdated();
  94. });
  95. },
  96. beforeDestroy: function beforeDestroy() {
  97. var eventKey = this.eventKey;
  98. this.__emit('destroy', eventKey);
  99. /* istanbul ignore if */
  100. if (this.minWidthTimeout) {
  101. cancelAnimationTimeout(this.minWidthTimeout);
  102. this.minWidthTimeout = null;
  103. }
  104. /* istanbul ignore if */
  105. if (this.mouseenterTimeout) {
  106. cancelAnimationTimeout(this.mouseenterTimeout);
  107. this.mouseenterTimeout = null;
  108. }
  109. },
  110. methods: {
  111. handleUpdated: function handleUpdated() {
  112. var _this3 = this;
  113. var _$props = this.$props,
  114. mode = _$props.mode,
  115. parentMenu = _$props.parentMenu,
  116. manualRef = _$props.manualRef;
  117. // invoke customized ref to expose component to mixin
  118. if (manualRef) {
  119. manualRef(this);
  120. }
  121. if (mode !== 'horizontal' || !parentMenu.isRootMenu || !this.isOpen) {
  122. return;
  123. }
  124. this.minWidthTimeout = requestAnimationTimeout(function () {
  125. return _this3.adjustWidth();
  126. }, 0);
  127. },
  128. onKeyDown: function onKeyDown(e) {
  129. var keyCode = e.keyCode;
  130. var menu = this.menuInstance;
  131. var _$props2 = this.$props,
  132. store = _$props2.store,
  133. isOpen = _$props2.isOpen;
  134. if (keyCode === KeyCode.ENTER) {
  135. this.onTitleClick(e);
  136. updateDefaultActiveFirst(store, this.eventKey, true);
  137. return true;
  138. }
  139. if (keyCode === KeyCode.RIGHT) {
  140. if (isOpen) {
  141. menu.onKeyDown(e);
  142. } else {
  143. this.triggerOpenChange(true);
  144. // need to update current menu's defaultActiveFirst value
  145. updateDefaultActiveFirst(store, this.eventKey, true);
  146. }
  147. return true;
  148. }
  149. if (keyCode === KeyCode.LEFT) {
  150. var handled = void 0;
  151. if (isOpen) {
  152. handled = menu.onKeyDown(e);
  153. } else {
  154. return undefined;
  155. }
  156. if (!handled) {
  157. this.triggerOpenChange(false);
  158. handled = true;
  159. }
  160. return handled;
  161. }
  162. if (isOpen && (keyCode === KeyCode.UP || keyCode === KeyCode.DOWN)) {
  163. return menu.onKeyDown(e);
  164. }
  165. return undefined;
  166. },
  167. onPopupVisibleChange: function onPopupVisibleChange(visible) {
  168. this.triggerOpenChange(visible, visible ? 'mouseenter' : 'mouseleave');
  169. },
  170. onMouseEnter: function onMouseEnter(e) {
  171. var _$props3 = this.$props,
  172. key = _$props3.eventKey,
  173. store = _$props3.store;
  174. updateDefaultActiveFirst(store, key, false);
  175. this.__emit('mouseenter', {
  176. key: key,
  177. domEvent: e
  178. });
  179. },
  180. onMouseLeave: function onMouseLeave(e) {
  181. var eventKey = this.eventKey,
  182. parentMenu = this.parentMenu;
  183. parentMenu.subMenuInstance = this;
  184. // parentMenu.subMenuLeaveFn = () => {
  185. // // trigger mouseleave
  186. // this.__emit('mouseleave', {
  187. // key: eventKey,
  188. // domEvent: e,
  189. // })
  190. // }
  191. this.__emit('mouseleave', {
  192. key: eventKey,
  193. domEvent: e
  194. });
  195. // prevent popup menu and submenu gap
  196. // parentMenu.subMenuLeaveTimer = setTimeout(parentMenu.subMenuLeaveFn, 100)
  197. },
  198. onTitleMouseEnter: function onTitleMouseEnter(domEvent) {
  199. var key = this.$props.eventKey;
  200. // this.clearSubMenuTitleLeaveTimer()
  201. this.__emit('itemHover', {
  202. key: key,
  203. hover: true
  204. });
  205. this.__emit('titleMouseenter', {
  206. key: key,
  207. domEvent: domEvent
  208. });
  209. },
  210. onTitleMouseLeave: function onTitleMouseLeave(e) {
  211. var eventKey = this.eventKey,
  212. parentMenu = this.parentMenu;
  213. parentMenu.subMenuInstance = this;
  214. this.__emit('itemHover', {
  215. key: eventKey,
  216. hover: false
  217. });
  218. this.__emit('titleMouseleave', {
  219. key: eventKey,
  220. domEvent: e
  221. });
  222. },
  223. onTitleClick: function onTitleClick(e) {
  224. var _$props4 = this.$props,
  225. triggerSubMenuAction = _$props4.triggerSubMenuAction,
  226. eventKey = _$props4.eventKey,
  227. isOpen = _$props4.isOpen,
  228. store = _$props4.store;
  229. this.__emit('titleClick', {
  230. key: eventKey,
  231. domEvent: e
  232. });
  233. if (triggerSubMenuAction === 'hover') {
  234. return;
  235. }
  236. this.triggerOpenChange(!isOpen, 'click');
  237. updateDefaultActiveFirst(store, eventKey, false);
  238. },
  239. onSubMenuClick: function onSubMenuClick(info) {
  240. this.__emit('click', this.addKeyPath(info));
  241. },
  242. getPrefixCls: function getPrefixCls() {
  243. return this.$props.rootPrefixCls + '-submenu';
  244. },
  245. getActiveClassName: function getActiveClassName() {
  246. return this.getPrefixCls() + '-active';
  247. },
  248. getDisabledClassName: function getDisabledClassName() {
  249. return this.getPrefixCls() + '-disabled';
  250. },
  251. getSelectedClassName: function getSelectedClassName() {
  252. return this.getPrefixCls() + '-selected';
  253. },
  254. getOpenClassName: function getOpenClassName() {
  255. return this.$props.rootPrefixCls + '-submenu-open';
  256. },
  257. saveMenuInstance: function saveMenuInstance(c) {
  258. // children menu instance
  259. this.menuInstance = c;
  260. },
  261. addKeyPath: function addKeyPath(info) {
  262. return _extends({}, info, {
  263. keyPath: (info.keyPath || []).concat(this.$props.eventKey)
  264. });
  265. },
  266. // triggerOpenChange (open, type) {
  267. // const key = this.$props.eventKey
  268. // this.__emit('openChange', {
  269. // key,
  270. // item: this,
  271. // trigger: type,
  272. // open,
  273. // })
  274. // },
  275. triggerOpenChange: function triggerOpenChange(open, type) {
  276. var _this4 = this;
  277. var key = this.$props.eventKey;
  278. var openChange = function openChange() {
  279. _this4.__emit('openChange', {
  280. key: key,
  281. item: _this4,
  282. trigger: type,
  283. open: open
  284. });
  285. };
  286. if (type === 'mouseenter') {
  287. // make sure mouseenter happen after other menu item's mouseleave
  288. this.mouseenterTimeout = requestAnimationTimeout(function () {
  289. openChange();
  290. }, 0);
  291. } else {
  292. openChange();
  293. }
  294. },
  295. isChildrenSelected: function isChildrenSelected() {
  296. var ret = { find: false };
  297. loopMenuItemRecursively(this.$slots['default'], this.$props.selectedKeys, ret);
  298. return ret.find;
  299. },
  300. // isOpen () {
  301. // return this.$props.openKeys.indexOf(this.$props.eventKey) !== -1
  302. // },
  303. adjustWidth: function adjustWidth() {
  304. /* istanbul ignore if */
  305. if (!this.$refs.subMenuTitle || !this.menuInstance) {
  306. return;
  307. }
  308. var popupMenu = this.menuInstance.$el;
  309. if (popupMenu.offsetWidth >= this.$refs.subMenuTitle.offsetWidth) {
  310. return;
  311. }
  312. /* istanbul ignore next */
  313. popupMenu.style.minWidth = this.$refs.subMenuTitle.offsetWidth + 'px';
  314. },
  315. renderChildren: function renderChildren(children) {
  316. var h = this.$createElement;
  317. var props = this.$props;
  318. var _getListeners = getListeners(this),
  319. select = _getListeners.select,
  320. deselect = _getListeners.deselect,
  321. openChange = _getListeners.openChange;
  322. var subPopupMenuProps = {
  323. props: {
  324. mode: props.mode === 'horizontal' ? 'vertical' : props.mode,
  325. visible: props.isOpen,
  326. level: props.level + 1,
  327. inlineIndent: props.inlineIndent,
  328. focusable: false,
  329. selectedKeys: props.selectedKeys,
  330. eventKey: props.eventKey + '-menu-',
  331. openKeys: props.openKeys,
  332. openTransitionName: props.openTransitionName,
  333. openAnimation: props.openAnimation,
  334. subMenuOpenDelay: props.subMenuOpenDelay,
  335. parentMenu: this,
  336. subMenuCloseDelay: props.subMenuCloseDelay,
  337. forceSubMenuRender: props.forceSubMenuRender,
  338. triggerSubMenuAction: props.triggerSubMenuAction,
  339. builtinPlacements: props.builtinPlacements,
  340. defaultActiveFirst: props.store.getState().defaultActiveFirst[getMenuIdFromSubMenuEventKey(props.eventKey)],
  341. multiple: props.multiple,
  342. prefixCls: props.rootPrefixCls,
  343. manualRef: this.saveMenuInstance,
  344. itemIcon: getComponentFromProp(this, 'itemIcon'),
  345. expandIcon: getComponentFromProp(this, 'expandIcon'),
  346. children: children
  347. },
  348. on: {
  349. click: this.onSubMenuClick,
  350. select: select,
  351. deselect: deselect,
  352. openChange: openChange
  353. },
  354. id: this.internalMenuId
  355. };
  356. var baseProps = subPopupMenuProps.props;
  357. var haveRendered = this.haveRendered;
  358. this.haveRendered = true;
  359. this.haveOpened = this.haveOpened || baseProps.visible || baseProps.forceSubMenuRender;
  360. // never rendered not planning to, don't render
  361. if (!this.haveOpened) {
  362. return h('div');
  363. }
  364. // don't show transition on first rendering (no animation for opened menu)
  365. // show appear transition if it's not visible (not sure why)
  366. // show appear transition if it's not inline mode
  367. var transitionAppear = haveRendered || !baseProps.visible || !baseProps.mode === 'inline';
  368. subPopupMenuProps['class'] = ' ' + baseProps.prefixCls + '-sub';
  369. var animProps = { appear: transitionAppear, css: false };
  370. var transitionProps = {
  371. props: animProps,
  372. on: {}
  373. };
  374. if (baseProps.openTransitionName) {
  375. transitionProps = getTransitionProps(baseProps.openTransitionName, {
  376. appear: transitionAppear
  377. });
  378. } else if (_typeof(baseProps.openAnimation) === 'object') {
  379. animProps = _extends({}, animProps, baseProps.openAnimation.props || {});
  380. if (!transitionAppear) {
  381. animProps.appear = false;
  382. }
  383. } else if (typeof baseProps.openAnimation === 'string') {
  384. transitionProps = getTransitionProps(baseProps.openAnimation, { appear: transitionAppear });
  385. }
  386. if (_typeof(baseProps.openAnimation) === 'object' && baseProps.openAnimation.on) {
  387. transitionProps.on = baseProps.openAnimation.on;
  388. }
  389. return h(
  390. 'transition',
  391. transitionProps,
  392. [h(SubPopupMenu, _mergeJSXProps([{
  393. directives: [{
  394. name: 'show',
  395. value: props.isOpen
  396. }]
  397. }, subPopupMenuProps]))]
  398. );
  399. }
  400. },
  401. render: function render() {
  402. var _className, _attrs;
  403. var h = arguments[0];
  404. var props = this.$props;
  405. var rootPrefixCls = this.rootPrefixCls,
  406. parentMenu = this.parentMenu;
  407. var isOpen = props.isOpen;
  408. var prefixCls = this.getPrefixCls();
  409. var isInlineMode = props.mode === 'inline';
  410. var className = (_className = {}, _defineProperty(_className, prefixCls, true), _defineProperty(_className, prefixCls + '-' + props.mode, true), _defineProperty(_className, this.getOpenClassName(), isOpen), _defineProperty(_className, this.getActiveClassName(), props.active || isOpen && !isInlineMode), _defineProperty(_className, this.getDisabledClassName(), props.disabled), _defineProperty(_className, this.getSelectedClassName(), this.isChildrenSelected()), _className);
  411. if (!this.internalMenuId) {
  412. if (props.eventKey) {
  413. this.internalMenuId = props.eventKey + '$Menu';
  414. } else {
  415. this.internalMenuId = '$__$' + ++guid + '$Menu';
  416. }
  417. }
  418. var mouseEvents = {};
  419. var titleClickEvents = {};
  420. var titleMouseEvents = {};
  421. if (!props.disabled) {
  422. mouseEvents = {
  423. mouseleave: this.onMouseLeave,
  424. mouseenter: this.onMouseEnter
  425. };
  426. // only works in title, not outer li
  427. titleClickEvents = {
  428. click: this.onTitleClick
  429. };
  430. titleMouseEvents = {
  431. mouseenter: this.onTitleMouseEnter,
  432. mouseleave: this.onTitleMouseLeave
  433. };
  434. }
  435. var style = {};
  436. if (isInlineMode) {
  437. style.paddingLeft = props.inlineIndent * props.level + 'px';
  438. }
  439. var ariaOwns = {};
  440. // only set aria-owns when menu is open
  441. // otherwise it would be an invalid aria-owns value
  442. // since corresponding node cannot be found
  443. if (isOpen) {
  444. ariaOwns = {
  445. 'aria-owns': this.internalMenuId
  446. };
  447. }
  448. var titleProps = {
  449. attrs: _extends({
  450. 'aria-expanded': isOpen
  451. }, ariaOwns, {
  452. 'aria-haspopup': 'true',
  453. title: typeof props.title === 'string' ? props.title : undefined
  454. }),
  455. on: _extends({}, titleMouseEvents, titleClickEvents),
  456. style: style,
  457. 'class': prefixCls + '-title',
  458. ref: 'subMenuTitle'
  459. };
  460. // expand custom icon should NOT be displayed in menu with horizontal mode.
  461. var icon = null;
  462. if (props.mode !== 'horizontal') {
  463. icon = getComponentFromProp(this, 'expandIcon', props);
  464. }
  465. var title = h(
  466. 'div',
  467. titleProps,
  468. [getComponentFromProp(this, 'title'), icon || h('i', { 'class': prefixCls + '-arrow' })]
  469. );
  470. var children = this.renderChildren(filterEmpty(this.$slots['default']));
  471. var getPopupContainer = this.parentMenu.isRootMenu ? this.parentMenu.getPopupContainer : function (triggerNode) {
  472. return triggerNode.parentNode;
  473. };
  474. var popupPlacement = popupPlacementMap[props.mode];
  475. var popupAlign = props.popupOffset ? { offset: props.popupOffset } : {};
  476. var popupClassName = props.mode === 'inline' ? '' : props.popupClassName;
  477. var liProps = {
  478. on: _extends({}, omit(getListeners(this), ['click']), mouseEvents),
  479. 'class': className
  480. };
  481. return h(
  482. 'li',
  483. _mergeJSXProps([liProps, {
  484. attrs: { role: 'menuitem' }
  485. }]),
  486. [isInlineMode && title, isInlineMode && children, !isInlineMode && h(
  487. Trigger,
  488. {
  489. attrs: (_attrs = {
  490. prefixCls: prefixCls,
  491. popupClassName: prefixCls + '-popup ' + rootPrefixCls + '-' + parentMenu.theme + ' ' + (popupClassName || ''),
  492. getPopupContainer: getPopupContainer,
  493. builtinPlacements: placements
  494. }, _defineProperty(_attrs, 'builtinPlacements', _extends({}, placements, props.builtinPlacements)), _defineProperty(_attrs, 'popupPlacement', popupPlacement), _defineProperty(_attrs, 'popupVisible', isOpen), _defineProperty(_attrs, 'popupAlign', popupAlign), _defineProperty(_attrs, 'action', props.disabled ? [] : [props.triggerSubMenuAction]), _defineProperty(_attrs, 'mouseEnterDelay', props.subMenuOpenDelay), _defineProperty(_attrs, 'mouseLeaveDelay', props.subMenuCloseDelay), _defineProperty(_attrs, 'forceRender', props.forceSubMenuRender), _attrs),
  495. on: {
  496. 'popupVisibleChange': this.onPopupVisibleChange
  497. }
  498. },
  499. [h(
  500. 'template',
  501. { slot: 'popup' },
  502. [children]
  503. ), title]
  504. )]
  505. );
  506. }
  507. };
  508. var connected = connect(function (_ref, _ref2) {
  509. var openKeys = _ref.openKeys,
  510. activeKey = _ref.activeKey,
  511. selectedKeys = _ref.selectedKeys;
  512. var eventKey = _ref2.eventKey,
  513. subMenuKey = _ref2.subMenuKey;
  514. return {
  515. isOpen: openKeys.indexOf(eventKey) > -1,
  516. active: activeKey[subMenuKey] === eventKey,
  517. selectedKeys: selectedKeys
  518. };
  519. })(SubMenu);
  520. connected.isSubMenu = true;
  521. export default connected;