dropdown.vue
<script>import Clickoutside from 'element-ui/src/utils/clickoutside'; import Emitter from 'element-ui/src/mixins/emitter'; import Migrating from 'element-ui/src/mixins/migrating'; import ElButton from 'element-ui/packages/button'; import ElButtonGroup from 'element-ui/packages/button-group'; import { generateId } from 'element-ui/src/utils/util'; export default{ name: 'ElDropdown', componentName: 'ElDropdown', mixins: [Emitter, Migrating], directives: { Clickoutside }, components: { ElButton, ElButtonGroup }, provide() { return{ dropdown: this}; }, props: { //trigger 触发下拉的行为 string hover, click hover trigger: { type: String, default: 'hover'}, //菜单按钮类型,同 Button 组件(只在split-button为 true 的情况下有效) type: String, //菜单尺寸,在split-button为 true 的情况下也对触发按钮生效 size: { type: String, default: ''}, //下拉触发元素呈现为按钮组 splitButton: Boolean, //hide-on-click 是否在点击菜单项后隐藏菜单 hideOnClick: { type: Boolean, default: true}, //placement 菜单弹出位置 string top/top-start/top-end/bottom/bottom-start/bottom-end placement: { type: String, default: 'bottom-end'}, visibleArrow: { default: true}, //展开下拉菜单的延时(仅在 trigger 为 hover 时有效) showTimeout: { type: Number, default: 250}, //收起下拉菜单的延时(仅在 trigger 为 hover 时有效) hideTimeout: { type: Number, default: 150}, //Dropdown 组件的 tabindex number — 0 tabindex: { type: Number, default: 0} }, data() { return{ timeout: null, visible: false, triggerElm: null, menuItems: null, menuItemsArray: null, dropdownElm: null, focusing: false, listId: `dropdown-menu-${generateId()}` }; }, computed: { dropdownSize() { return this.size ||(this.$ELEMENT ||{}).size; } }, mounted() { //接收下拉的li点击事件 this.$on('menu-item-click', this.handleMenuItemClick); }, watch: { visible(val) { this.broadcast('ElDropdownMenu', 'visible', val); this.$emit('visible-change', val); }, focusing(val) { const selfDefine = this.$el.querySelector('.el-dropdown-selfdefine'); if(selfDefine) { //自定义 if(val) { selfDefine.className += 'focusing'; } else{ selfDefine.className =selfDefine.className.replace('focusing', ''); } } } }, methods: { getMigratingConfig() { return{ props: { 'menu-align': 'menu-align is renamed to placement.'} }; }, //展开下拉框框 show() { if(this.triggerElm.disabled) return; clearTimeout(this.timeout); this.timeout =setTimeout(() =>{ this.visible = true; //如果是点击直接触发,反之延迟传入的额数值触发 }, this.trigger === 'click' ? 0: this.showTimeout); }, //隐藏下拉框 hide() { if(this.triggerElm.disabled) return; this.removeTabindex(); if(this.tabindex >= 0) { this.resetTabindex(this.triggerElm); } clearTimeout(this.timeout); this.timeout =setTimeout(() =>{ this.visible = false; }, this.trigger === 'click' ? 0: this.hideTimeout); }, //点击展开关闭 handleClick() { if(this.triggerElm.disabled) return; if(this.visible) { this.hide(); } else{ this.show(); } }, handleTriggerKeyDown(ev) { const keyCode =ev.keyCode; if([38, 40].indexOf(keyCode) > -1) { //up/down this.removeTabindex(); this.resetTabindex(this.menuItems[0]); this.menuItems[0].focus(); ev.preventDefault(); ev.stopPropagation(); } else if(keyCode === 13) { //space enter选中 this.handleClick(); } else if([9, 27].indexOf(keyCode) > -1) { //tab || esc this.hide(); } }, handleItemKeyDown(ev) { const keyCode =ev.keyCode; const target =ev.target; const currentIndex = this.menuItemsArray.indexOf(target); const max = this.menuItemsArray.length - 1; let nextIndex; if([38, 40].indexOf(keyCode) > -1) { //up/down if(keyCode === 38) { //up nextIndex =currentIndex !== 0 ?currentIndex - 1: 0; } else{ //down nextIndex =currentIndex <max ?currentIndex + 1: max; } this.removeTabindex(); this.resetTabindex(this.menuItems[nextIndex]); this.menuItems[nextIndex].focus(); ev.preventDefault(); ev.stopPropagation(); } else if(keyCode === 13) { //enter选中 this.triggerElmFocus(); target.click(); if(this.hideOnClick) { //click关闭 this.visible = false; } } else if([9, 27].indexOf(keyCode) > -1) { //tab // esc this.hide(); this.triggerElmFocus(); } }, resetTabindex(ele) { //下次tab时组件聚焦元素 this.removeTabindex(); ele.setAttribute('tabindex', '0'); //下次期望的聚焦元素 }, removeTabindex() { this.triggerElm.setAttribute('tabindex', '-1'); this.menuItemsArray.forEach((item) =>{ item.setAttribute('tabindex', '-1'); }); }, initAria() { this.dropdownElm.setAttribute('id', this.listId); this.triggerElm.setAttribute('aria-haspopup', 'list'); this.triggerElm.setAttribute('aria-controls', this.listId); if(!this.splitButton) { //自定义 this.triggerElm.setAttribute('role', 'button'); this.triggerElm.setAttribute('tabindex', this.tabindex); this.triggerElm.setAttribute('class', (this.triggerElm.getAttribute('class') || '') + 'el-dropdown-selfdefine'); //控制 } }, //初始化事件 initEvent() { let { trigger, show, hide, handleClick, splitButton, handleTriggerKeyDown, handleItemKeyDown } = this; this.triggerElm =splitButton ? this.$refs.trigger.$el : this.$slots.default[0].elm; let dropdownElm = this.dropdownElm; this.triggerElm.addEventListener('keydown', handleTriggerKeyDown); //triggerElm keydown dropdownElm.addEventListener('keydown', handleItemKeyDown, true); //item keydown //控制自定义元素的样式 if(!splitButton) { this.triggerElm.addEventListener('focus', () =>{ this.focusing = true; }); this.triggerElm.addEventListener('blur', () =>{ this.focusing = false; }); this.triggerElm.addEventListener('click', () =>{ this.focusing = false; }); } if(trigger === 'hover') { this.triggerElm.addEventListener('mouseenter', show); this.triggerElm.addEventListener('mouseleave', hide); dropdownElm.addEventListener('mouseenter', show); dropdownElm.addEventListener('mouseleave', hide); } else if(trigger === 'click') { this.triggerElm.addEventListener('click', handleClick); } }, //点击菜单触发的回调 handleMenuItemClick(command, instance) { //如果设置了点击菜单后隐藏菜单 if(this.hideOnClick) { //隐藏下拉框 this.visible = false; } //否则触发command事件 this.$emit('command', command, instance); }, //触发获取焦点事件 triggerElmFocus() { this.triggerElm.focus && this.triggerElm.focus(); }, //初始化dom initDomOperation() { this.dropdownElm = this.popperElm; //获取所有tab键选中的元素 this.menuItems = this.dropdownElm.querySelectorAll("[tabindex='-1']"); this.menuItemsArray =[].slice.call(this.menuItems); //初始化事件 this.initEvent(); this.initAria(); } }, render(h) { let { hide, splitButton, type, dropdownSize } = this; const handleMainButtonClick =(event) =>{ this.$emit('click', event); hide(); }; let triggerElm = !splitButton ? this.$slots.default: (<el-button-group> <el-button type={type} size={dropdownSize} nativeOn-click={handleMainButtonClick}>{this.$slots.default} </el-button> <el-button ref="trigger"type={type} size={dropdownSize} class="el-dropdown__caret-button"> <i class="el-dropdown__icon el-icon-arrow-down"></i> </el-button> </el-button-group>); return( <div class="el-dropdown"v-clickoutside={hide}>{triggerElm} {this.$slots.dropdown} </div> ); } }; </script>
dropdown-menu.vue
<template> <transition name="el-zoom-in-top"@after-leave="doDestroy"> <ul class="el-dropdown-menu el-popper":class="[size && `el-dropdown-menu--${size}`]"v-show="showPopper"> <slot></slot> </ul> </transition> </template> <script>import Popper from 'element-ui/src/utils/vue-popper'; export default{ name: 'ElDropdownMenu', componentName: 'ElDropdownMenu', mixins: [Popper], props: { visibleArrow: { type: Boolean, default: true}, arrowOffset: { type: Number, default: 0} }, data() { return{ size: this.dropdown.dropdownSize }; }, //注入父组件传进的组件 inject: ['dropdown'], created() { this.$on('updatePopper', () =>{ if(this.showPopper) this.updatePopper(); }); //监听visible事件 this.$on('visible', val =>{ //改变showPoper this.showPopper =val; }); }, mounted() { //获取到popperElm元素 this.dropdown.popperElm = this.popperElm = this.$el; this.referenceElm = this.dropdown.$el; //compatible with 2.6 new v-slot syntax //issue link https://github.com/ElemeFE/element/issues/14345 //在此初始化调用 this.dropdown.initDomOperation(); }, watch: { 'dropdown.placement': { immediate: true, handler(val) { this.currentPlacement =val; } } } }; </script>
dropdown-item.vue
<template> <li class="el-dropdown-menu__item":class="{ 'is-disabled': disabled, 'el-dropdown-menu__item--divided': divided }"@click="handleClick":aria-disabled="disabled":tabindex="disabled ? null : -1" > <i :class="icon"v-if="icon"></i> <slot></slot> </li> </template> <script>import Emitter from 'element-ui/src/mixins/emitter'; export default{ name: 'ElDropdownItem', mixins: [Emitter], props: { command: {}, disabled: Boolean, divided: Boolean, icon: String }, methods: { handleClick(e) { //触发showPopper的menu-item-click事件 this.dispatch('ElDropdown', 'menu-item-click', [this.command, this]); } } }; </script>