submenu.vue 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. <script>
  2. import ElCollapseTransition from 'element-ui/src/transitions/collapse-transition';
  3. import menuMixin from './menu-mixin';
  4. import Emitter from 'element-ui/src/mixins/emitter';
  5. import Popper from 'element-ui/src/utils/vue-popper';
  6. const poperMixins = {
  7. props: {
  8. transformOrigin: {
  9. type: [Boolean, String],
  10. default: false
  11. },
  12. offset: Popper.props.offset,
  13. boundariesPadding: Popper.props.boundariesPadding,
  14. popperOptions: Popper.props.popperOptions
  15. },
  16. data: Popper.data,
  17. methods: Popper.methods,
  18. beforeDestroy: Popper.beforeDestroy,
  19. deactivated: Popper.deactivated
  20. };
  21. export default {
  22. name: 'ElSubmenu',
  23. componentName: 'ElSubmenu',
  24. mixins: [menuMixin, Emitter, poperMixins],
  25. components: { ElCollapseTransition },
  26. props: {
  27. index: {
  28. type: String,
  29. required: true
  30. },
  31. showTimeout: {
  32. type: Number,
  33. default: 300
  34. },
  35. hideTimeout: {
  36. type: Number,
  37. default: 300
  38. },
  39. popperClass: String,
  40. disabled: Boolean,
  41. popperAppendToBody: {
  42. type: Boolean,
  43. default: undefined
  44. }
  45. },
  46. data() {
  47. return {
  48. popperJS: null,
  49. timeout: null,
  50. items: {},
  51. submenus: {},
  52. mouseInChild: false
  53. };
  54. },
  55. watch: {
  56. opened(val) {
  57. if (this.isMenuPopup) {
  58. this.$nextTick(_ => {
  59. this.updatePopper();
  60. });
  61. }
  62. }
  63. },
  64. computed: {
  65. // popper option
  66. appendToBody() {
  67. return this.popperAppendToBody === undefined
  68. ? this.isFirstLevel
  69. : this.popperAppendToBody;
  70. },
  71. menuTransitionName() {
  72. return this.rootMenu.collapse ? 'el-zoom-in-left' : 'el-zoom-in-top';
  73. },
  74. opened() {
  75. return this.rootMenu.openedMenus.indexOf(this.index) > -1;
  76. },
  77. active() {
  78. let isActive = false;
  79. const submenus = this.submenus;
  80. const items = this.items;
  81. Object.keys(items).forEach(index => {
  82. if (items[index].active) {
  83. isActive = true;
  84. }
  85. });
  86. Object.keys(submenus).forEach(index => {
  87. if (submenus[index].active) {
  88. isActive = true;
  89. }
  90. });
  91. return isActive;
  92. },
  93. hoverBackground() {
  94. return this.rootMenu.hoverBackground;
  95. },
  96. backgroundColor() {
  97. return this.rootMenu.backgroundColor || '';
  98. },
  99. activeTextColor() {
  100. return this.rootMenu.activeTextColor || '';
  101. },
  102. textColor() {
  103. return this.rootMenu.textColor || '';
  104. },
  105. mode() {
  106. return this.rootMenu.mode;
  107. },
  108. isMenuPopup() {
  109. return this.rootMenu.isMenuPopup;
  110. },
  111. titleStyle() {
  112. if (this.mode !== 'horizontal') {
  113. return {
  114. color: this.textColor
  115. };
  116. }
  117. return {
  118. borderBottomColor: this.active
  119. ? (this.rootMenu.activeTextColor ? this.activeTextColor : '')
  120. : 'transparent',
  121. color: this.active
  122. ? this.activeTextColor
  123. : this.textColor
  124. };
  125. },
  126. isFirstLevel() {
  127. let isFirstLevel = true;
  128. let parent = this.$parent;
  129. while (parent && parent !== this.rootMenu) {
  130. if (['ElSubmenu', 'ElMenuItemGroup'].indexOf(parent.$options.componentName) > -1) {
  131. isFirstLevel = false;
  132. break;
  133. } else {
  134. parent = parent.$parent;
  135. }
  136. }
  137. return isFirstLevel;
  138. }
  139. },
  140. methods: {
  141. handleCollapseToggle(value) {
  142. if (value) {
  143. this.initPopper();
  144. } else {
  145. this.doDestroy();
  146. }
  147. },
  148. addItem(item) {
  149. this.$set(this.items, item.index, item);
  150. },
  151. removeItem(item) {
  152. delete this.items[item.index];
  153. },
  154. addSubmenu(item) {
  155. this.$set(this.submenus, item.index, item);
  156. },
  157. removeSubmenu(item) {
  158. delete this.submenus[item.index];
  159. },
  160. handleClick() {
  161. const { rootMenu, disabled } = this;
  162. if (
  163. (rootMenu.menuTrigger === 'hover' && rootMenu.mode === 'horizontal') ||
  164. (rootMenu.collapse && rootMenu.mode === 'vertical') ||
  165. disabled
  166. ) {
  167. return;
  168. }
  169. this.dispatch('ElMenu', 'submenu-click', this);
  170. },
  171. handleMouseenter() {
  172. const { rootMenu, disabled } = this;
  173. if (
  174. (rootMenu.menuTrigger === 'click' && rootMenu.mode === 'horizontal') ||
  175. (!rootMenu.collapse && rootMenu.mode === 'vertical') ||
  176. disabled
  177. ) {
  178. return;
  179. }
  180. this.dispatch('ElSubmenu', 'mouse-enter-child');
  181. clearTimeout(this.timeout);
  182. this.timeout = setTimeout(() => {
  183. this.rootMenu.openMenu(this.index, this.indexPath);
  184. }, this.showTimeout);
  185. },
  186. handleMouseleave() {
  187. const {rootMenu} = this;
  188. if (
  189. (rootMenu.menuTrigger === 'click' && rootMenu.mode === 'horizontal') ||
  190. (!rootMenu.collapse && rootMenu.mode === 'vertical')
  191. ) {
  192. return;
  193. }
  194. this.dispatch('ElSubmenu', 'mouse-leave-child');
  195. clearTimeout(this.timeout);
  196. this.timeout = setTimeout(() => {
  197. !this.mouseInChild && this.rootMenu.closeMenu(this.index);
  198. }, this.hideTimeout);
  199. },
  200. handleTitleMouseenter() {
  201. if (this.mode === 'horizontal' && !this.rootMenu.backgroundColor) return;
  202. const title = this.$refs['submenu-title'];
  203. title && (title.style.backgroundColor = this.rootMenu.hoverBackground);
  204. },
  205. handleTitleMouseleave() {
  206. if (this.mode === 'horizontal' && !this.rootMenu.backgroundColor) return;
  207. const title = this.$refs['submenu-title'];
  208. title && (title.style.backgroundColor = this.rootMenu.backgroundColor || '');
  209. },
  210. updatePlacement() {
  211. this.currentPlacement = this.mode === 'horizontal' && this.isFirstLevel
  212. ? 'bottom-start'
  213. : 'right-start';
  214. },
  215. initPopper() {
  216. this.referenceElm = this.$el;
  217. this.popperElm = this.$refs.menu;
  218. this.updatePlacement();
  219. }
  220. },
  221. created() {
  222. this.$on('toggle-collapse', this.handleCollapseToggle);
  223. this.$on('mouse-enter-child', () => {
  224. this.mouseInChild = true;
  225. clearTimeout(this.timeout);
  226. });
  227. this.$on('mouse-leave-child', () => {
  228. this.mouseInChild = false;
  229. clearTimeout(this.timeout);
  230. });
  231. },
  232. mounted() {
  233. this.parentMenu.addSubmenu(this);
  234. this.rootMenu.addSubmenu(this);
  235. this.initPopper();
  236. },
  237. beforeDestroy() {
  238. this.parentMenu.removeSubmenu(this);
  239. this.rootMenu.removeSubmenu(this);
  240. },
  241. render(h) {
  242. const {
  243. active,
  244. opened,
  245. paddingStyle,
  246. titleStyle,
  247. backgroundColor,
  248. rootMenu,
  249. currentPlacement,
  250. menuTransitionName,
  251. mode,
  252. disabled,
  253. popperClass,
  254. $slots,
  255. isFirstLevel
  256. } = this;
  257. const popupMenu = (
  258. <transition name={menuTransitionName}>
  259. <div
  260. ref="menu"
  261. v-show={opened}
  262. class={[`el-menu--${mode}`, popperClass]}
  263. on-mouseenter={this.handleMouseenter}
  264. on-mouseleave={this.handleMouseleave}
  265. on-focus={this.handleMouseenter}>
  266. <ul
  267. role="menu"
  268. class={['el-menu el-menu--popup', `el-menu--popup-${currentPlacement}`]}
  269. style={{ backgroundColor: rootMenu.backgroundColor || '' }}>
  270. {$slots.default}
  271. </ul>
  272. </div>
  273. </transition>
  274. );
  275. const inlineMenu = (
  276. <el-collapse-transition>
  277. <ul
  278. role="menu"
  279. class="el-menu el-menu--inline"
  280. v-show={opened}
  281. style={{ backgroundColor: rootMenu.backgroundColor || '' }}>
  282. {$slots.default}
  283. </ul>
  284. </el-collapse-transition>
  285. );
  286. const submenuTitleIcon = (
  287. rootMenu.mode === 'horizontal' && isFirstLevel ||
  288. rootMenu.mode === 'vertical' && !rootMenu.collapse
  289. ) ? 'el-icon-arrow-down' : 'el-icon-arrow-right';
  290. return (
  291. <li
  292. class={{
  293. 'el-submenu': true,
  294. 'is-active': active,
  295. 'is-opened': opened,
  296. 'is-disabled': disabled
  297. }}
  298. role="menuitem"
  299. aria-haspopup="true"
  300. aria-expanded={opened}
  301. on-mouseenter={this.handleMouseenter}
  302. on-mouseleave={this.handleMouseleave}
  303. on-focus={this.handleMouseenter}
  304. >
  305. <div
  306. class="el-submenu__title"
  307. ref="submenu-title"
  308. on-click={this.handleClick}
  309. on-mouseenter={this.handleTitleMouseenter}
  310. on-mouseleave={this.handleTitleMouseleave}
  311. style={[paddingStyle, titleStyle, { backgroundColor }]}
  312. >
  313. {$slots.title}
  314. <i class={[ 'el-submenu__icon-arrow', submenuTitleIcon ]}></i>
  315. </div>
  316. {this.isMenuPopup ? popupMenu : inlineMenu}
  317. </li>
  318. );
  319. }
  320. };
  321. </script>