DOMWrap.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. import _toConsumableArray from 'babel-runtime/helpers/toConsumableArray';
  2. import _extends from 'babel-runtime/helpers/extends';
  3. import _objectWithoutProperties from 'babel-runtime/helpers/objectWithoutProperties';
  4. import PropTypes from '../_util/vue-types';
  5. import ResizeObserver from 'resize-observer-polyfill';
  6. import SubMenu from './SubMenu';
  7. import BaseMixin from '../_util/BaseMixin';
  8. import { getWidth, setStyle, menuAllProps } from './util';
  9. import { cloneElement } from '../_util/vnode';
  10. import { getClass, getPropsData, getEvents, getListeners } from '../_util/props-util';
  11. var canUseDOM = !!(typeof window !== 'undefined' && window.document && window.document.createElement);
  12. var MENUITEM_OVERFLOWED_CLASSNAME = 'menuitem-overflowed';
  13. var FLOAT_PRECISION_ADJUST = 0.5;
  14. // Fix ssr
  15. if (canUseDOM) {
  16. require('mutationobserver-shim');
  17. }
  18. var DOMWrap = {
  19. name: 'DOMWrap',
  20. mixins: [BaseMixin],
  21. data: function data() {
  22. this.resizeObserver = null;
  23. this.mutationObserver = null;
  24. // original scroll size of the list
  25. this.originalTotalWidth = 0;
  26. // copy of overflowed items
  27. this.overflowedItems = [];
  28. // cache item of the original items (so we can track the size and order)
  29. this.menuItemSizes = [];
  30. return {
  31. lastVisibleIndex: undefined
  32. };
  33. },
  34. mounted: function mounted() {
  35. var _this = this;
  36. this.$nextTick(function () {
  37. _this.setChildrenWidthAndResize();
  38. if (_this.level === 1 && _this.mode === 'horizontal') {
  39. var menuUl = _this.$el;
  40. if (!menuUl) {
  41. return;
  42. }
  43. _this.resizeObserver = new ResizeObserver(function (entries) {
  44. entries.forEach(_this.setChildrenWidthAndResize);
  45. });
  46. [].slice.call(menuUl.children).concat(menuUl).forEach(function (el) {
  47. _this.resizeObserver.observe(el);
  48. });
  49. if (typeof MutationObserver !== 'undefined') {
  50. _this.mutationObserver = new MutationObserver(function () {
  51. _this.resizeObserver.disconnect();
  52. [].slice.call(menuUl.children).concat(menuUl).forEach(function (el) {
  53. _this.resizeObserver.observe(el);
  54. });
  55. _this.setChildrenWidthAndResize();
  56. });
  57. _this.mutationObserver.observe(menuUl, {
  58. attributes: false,
  59. childList: true,
  60. subTree: false
  61. });
  62. }
  63. }
  64. });
  65. },
  66. beforeDestroy: function beforeDestroy() {
  67. if (this.resizeObserver) {
  68. this.resizeObserver.disconnect();
  69. }
  70. if (this.mutationObserver) {
  71. this.mutationObserver.disconnect();
  72. }
  73. },
  74. methods: {
  75. // get all valid menuItem nodes
  76. getMenuItemNodes: function getMenuItemNodes() {
  77. var prefixCls = this.$props.prefixCls;
  78. var ul = this.$el;
  79. if (!ul) {
  80. return [];
  81. }
  82. // filter out all overflowed indicator placeholder
  83. return [].slice.call(ul.children).filter(function (node) {
  84. return node.className.split(' ').indexOf(prefixCls + '-overflowed-submenu') < 0;
  85. });
  86. },
  87. getOverflowedSubMenuItem: function getOverflowedSubMenuItem(keyPrefix, overflowedItems, renderPlaceholder) {
  88. var h = this.$createElement;
  89. var _$props = this.$props,
  90. overflowedIndicator = _$props.overflowedIndicator,
  91. level = _$props.level,
  92. mode = _$props.mode,
  93. prefixCls = _$props.prefixCls,
  94. theme = _$props.theme;
  95. if (level !== 1 || mode !== 'horizontal') {
  96. return null;
  97. }
  98. // put all the overflowed item inside a submenu
  99. // with a title of overflow indicator ('...')
  100. var copy = this.$slots['default'][0];
  101. var _getPropsData = getPropsData(copy),
  102. title = _getPropsData.title,
  103. rest = _objectWithoutProperties(_getPropsData, ['title']); // eslint-disable-line no-unused-vars
  104. var events = getEvents(copy);
  105. var style = {};
  106. var key = keyPrefix + '-overflowed-indicator';
  107. var eventKey = keyPrefix + '-overflowed-indicator';
  108. if (overflowedItems.length === 0 && renderPlaceholder !== true) {
  109. style = {
  110. display: 'none'
  111. };
  112. } else if (renderPlaceholder) {
  113. style = {
  114. visibility: 'hidden',
  115. // prevent from taking normal dom space
  116. position: 'absolute'
  117. };
  118. key = key + '-placeholder';
  119. eventKey = eventKey + '-placeholder';
  120. }
  121. var popupClassName = theme ? prefixCls + '-' + theme : '';
  122. var props = {};
  123. var on = {};
  124. menuAllProps.props.forEach(function (k) {
  125. if (rest[k] !== undefined) {
  126. props[k] = rest[k];
  127. }
  128. });
  129. menuAllProps.on.forEach(function (k) {
  130. if (events[k] !== undefined) {
  131. on[k] = events[k];
  132. }
  133. });
  134. var subMenuProps = {
  135. props: _extends({
  136. title: overflowedIndicator,
  137. popupClassName: popupClassName
  138. }, props, {
  139. eventKey: eventKey,
  140. disabled: false
  141. }),
  142. 'class': prefixCls + '-overflowed-submenu',
  143. key: key,
  144. style: style,
  145. on: on
  146. };
  147. return h(
  148. SubMenu,
  149. subMenuProps,
  150. [overflowedItems]
  151. );
  152. },
  153. // memorize rendered menuSize
  154. setChildrenWidthAndResize: function setChildrenWidthAndResize() {
  155. if (this.mode !== 'horizontal') {
  156. return;
  157. }
  158. var ul = this.$el;
  159. if (!ul) {
  160. return;
  161. }
  162. var ulChildrenNodes = ul.children;
  163. if (!ulChildrenNodes || ulChildrenNodes.length === 0) {
  164. return;
  165. }
  166. var lastOverflowedIndicatorPlaceholder = ul.children[ulChildrenNodes.length - 1];
  167. // need last overflowed indicator for calculating length;
  168. setStyle(lastOverflowedIndicatorPlaceholder, 'display', 'inline-block');
  169. var menuItemNodes = this.getMenuItemNodes();
  170. // reset display attribute for all hidden elements caused by overflow to calculate updated width
  171. // and then reset to original state after width calculation
  172. var overflowedItems = menuItemNodes.filter(function (c) {
  173. return c.className.split(' ').indexOf(MENUITEM_OVERFLOWED_CLASSNAME) >= 0;
  174. });
  175. overflowedItems.forEach(function (c) {
  176. setStyle(c, 'display', 'inline-block');
  177. });
  178. this.menuItemSizes = menuItemNodes.map(function (c) {
  179. return getWidth(c);
  180. });
  181. overflowedItems.forEach(function (c) {
  182. setStyle(c, 'display', 'none');
  183. });
  184. this.overflowedIndicatorWidth = getWidth(ul.children[ul.children.length - 1]);
  185. this.originalTotalWidth = this.menuItemSizes.reduce(function (acc, cur) {
  186. return acc + cur;
  187. }, 0);
  188. this.handleResize();
  189. // prevent the overflowed indicator from taking space;
  190. setStyle(lastOverflowedIndicatorPlaceholder, 'display', 'none');
  191. },
  192. handleResize: function handleResize() {
  193. var _this2 = this;
  194. if (this.mode !== 'horizontal') {
  195. return;
  196. }
  197. var ul = this.$el;
  198. if (!ul) {
  199. return;
  200. }
  201. var width = getWidth(ul);
  202. this.overflowedItems = [];
  203. var currentSumWidth = 0;
  204. // index for last visible child in horizontal mode
  205. var lastVisibleIndex = void 0;
  206. // float number comparison could be problematic
  207. // e.g. 0.1 + 0.2 > 0.3 =====> true
  208. // thus using FLOAT_PRECISION_ADJUST as buffer to help the situation
  209. if (this.originalTotalWidth > width + FLOAT_PRECISION_ADJUST) {
  210. lastVisibleIndex = -1;
  211. this.menuItemSizes.forEach(function (liWidth) {
  212. currentSumWidth += liWidth;
  213. if (currentSumWidth + _this2.overflowedIndicatorWidth <= width) {
  214. lastVisibleIndex += 1;
  215. }
  216. });
  217. }
  218. this.setState({ lastVisibleIndex: lastVisibleIndex });
  219. },
  220. renderChildren: function renderChildren(children) {
  221. var _this3 = this;
  222. // need to take care of overflowed items in horizontal mode
  223. var lastVisibleIndex = this.$data.lastVisibleIndex;
  224. var className = getClass(this);
  225. return (children || []).reduce(function (acc, childNode, index) {
  226. var item = childNode;
  227. var eventKey = getPropsData(childNode).eventKey;
  228. if (_this3.mode === 'horizontal') {
  229. var overflowed = _this3.getOverflowedSubMenuItem(eventKey, []);
  230. if (lastVisibleIndex !== undefined && className[_this3.prefixCls + '-root'] !== -1) {
  231. if (index > lastVisibleIndex) {
  232. item = cloneElement(childNode,
  233. // 这里修改 eventKey 是为了防止隐藏状态下还会触发 openkeys 事件
  234. {
  235. style: { display: 'none' },
  236. props: { eventKey: eventKey + '-hidden' },
  237. 'class': MENUITEM_OVERFLOWED_CLASSNAME
  238. });
  239. }
  240. if (index === lastVisibleIndex + 1) {
  241. _this3.overflowedItems = children.slice(lastVisibleIndex + 1).map(function (c) {
  242. return cloneElement(c,
  243. // children[index].key will become '.$key' in clone by default,
  244. // we have to overwrite with the correct key explicitly
  245. {
  246. key: getPropsData(c).eventKey,
  247. props: { mode: 'vertical-left' }
  248. });
  249. });
  250. overflowed = _this3.getOverflowedSubMenuItem(eventKey, _this3.overflowedItems);
  251. }
  252. }
  253. var ret = [].concat(_toConsumableArray(acc), [overflowed, item]);
  254. if (index === children.length - 1) {
  255. // need a placeholder for calculating overflowed indicator width
  256. ret.push(_this3.getOverflowedSubMenuItem(eventKey, [], true));
  257. }
  258. return ret;
  259. }
  260. return [].concat(_toConsumableArray(acc), [item]);
  261. }, []);
  262. }
  263. },
  264. render: function render() {
  265. var h = arguments[0];
  266. var Tag = this.$props.tag;
  267. var tagProps = {
  268. on: getListeners(this)
  269. };
  270. return h(
  271. Tag,
  272. tagProps,
  273. [this.renderChildren(this.$slots['default'])]
  274. );
  275. }
  276. };
  277. DOMWrap.props = {
  278. mode: PropTypes.oneOf(['horizontal', 'vertical', 'vertical-left', 'vertical-right', 'inline']),
  279. prefixCls: PropTypes.string,
  280. level: PropTypes.number,
  281. theme: PropTypes.string,
  282. overflowedIndicator: PropTypes.node,
  283. visible: PropTypes.bool,
  284. hiddenClassName: PropTypes.string,
  285. tag: PropTypes.string.def('div')
  286. };
  287. export default DOMWrap;