element dropdown源码
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>