UNPKG

8.76 kBPlain TextView Raw
1<script>
2 import Clickoutside from 'element-ui/src/utils/clickoutside';
3 import Emitter from 'element-ui/src/mixins/emitter';
4 import Migrating from 'element-ui/src/mixins/migrating';
5 import ElButton from 'element-ui/packages/button';
6 import ElButtonGroup from 'element-ui/packages/button-group';
7 import { generateId } from 'element-ui/src/utils/util';
8
9 export default {
10 name: 'ElDropdown',
11
12 componentName: 'ElDropdown',
13
14 mixins: [Emitter, Migrating],
15
16 directives: { Clickoutside },
17
18 components: {
19 ElButton,
20 ElButtonGroup
21 },
22
23 provide() {
24 return {
25 dropdown: this
26 };
27 },
28
29 props: {
30 trigger: {
31 type: String,
32 default: 'hover'
33 },
34 type: String,
35 size: {
36 type: String,
37 default: ''
38 },
39 splitButton: Boolean,
40 hideOnClick: {
41 type: Boolean,
42 default: true
43 },
44 placement: {
45 type: String,
46 default: 'bottom-end'
47 },
48 visibleArrow: {
49 default: true
50 },
51 showTimeout: {
52 type: Number,
53 default: 250
54 },
55 hideTimeout: {
56 type: Number,
57 default: 150
58 },
59 tabindex: {
60 type: Number,
61 default: 0
62 },
63 disabled: {
64 type: Boolean,
65 default: false
66 }
67 },
68
69 data() {
70 return {
71 timeout: null,
72 visible: false,
73 triggerElm: null,
74 menuItems: null,
75 menuItemsArray: null,
76 dropdownElm: null,
77 focusing: false,
78 listId: `dropdown-menu-${generateId()}`
79 };
80 },
81
82 computed: {
83 dropdownSize() {
84 return this.size || (this.$ELEMENT || {}).size;
85 }
86 },
87
88 mounted() {
89 this.$on('menu-item-click', this.handleMenuItemClick);
90 },
91
92 watch: {
93 visible(val) {
94 this.broadcast('ElDropdownMenu', 'visible', val);
95 this.$emit('visible-change', val);
96 },
97 focusing(val) {
98 const selfDefine = this.$el.querySelector('.el-dropdown-selfdefine');
99 if (selfDefine) { // 自定义
100 if (val) {
101 selfDefine.className += ' focusing';
102 } else {
103 selfDefine.className = selfDefine.className.replace('focusing', '');
104 }
105 }
106 }
107 },
108
109 methods: {
110 getMigratingConfig() {
111 return {
112 props: {
113 'menu-align': 'menu-align is renamed to placement.'
114 }
115 };
116 },
117 show() {
118 if (this.disabled) return;
119 clearTimeout(this.timeout);
120 this.timeout = setTimeout(() => {
121 this.visible = true;
122 }, this.trigger === 'click' ? 0 : this.showTimeout);
123 },
124 hide() {
125 if (this.disabled) return;
126 this.removeTabindex();
127 if (this.tabindex >= 0) {
128 this.resetTabindex(this.triggerElm);
129 }
130 clearTimeout(this.timeout);
131 this.timeout = setTimeout(() => {
132 this.visible = false;
133 }, this.trigger === 'click' ? 0 : this.hideTimeout);
134 },
135 handleClick() {
136 if (this.disabled) return;
137 if (this.visible) {
138 this.hide();
139 } else {
140 this.show();
141 }
142 },
143 handleTriggerKeyDown(ev) {
144 const keyCode = ev.keyCode;
145 if ([38, 40].indexOf(keyCode) > -1) { // up/down
146 this.removeTabindex();
147 this.resetTabindex(this.menuItems[0]);
148 this.menuItems[0].focus();
149 ev.preventDefault();
150 ev.stopPropagation();
151 } else if (keyCode === 13) { // space enter选中
152 this.handleClick();
153 } else if ([9, 27].indexOf(keyCode) > -1) { // tab || esc
154 this.hide();
155 }
156 },
157 handleItemKeyDown(ev) {
158 const keyCode = ev.keyCode;
159 const target = ev.target;
160 const currentIndex = this.menuItemsArray.indexOf(target);
161 const max = this.menuItemsArray.length - 1;
162 let nextIndex;
163 if ([38, 40].indexOf(keyCode) > -1) { // up/down
164 if (keyCode === 38) { // up
165 nextIndex = currentIndex !== 0 ? currentIndex - 1 : 0;
166 } else { // down
167 nextIndex = currentIndex < max ? currentIndex + 1 : max;
168 }
169 this.removeTabindex();
170 this.resetTabindex(this.menuItems[nextIndex]);
171 this.menuItems[nextIndex].focus();
172 ev.preventDefault();
173 ev.stopPropagation();
174 } else if (keyCode === 13) { // enter选中
175 this.triggerElmFocus();
176 target.click();
177 if (this.hideOnClick) { // click关闭
178 this.visible = false;
179 }
180 } else if ([9, 27].indexOf(keyCode) > -1) { // tab // esc
181 this.hide();
182 this.triggerElmFocus();
183 }
184 },
185 resetTabindex(ele) { // 下次tab时组件聚焦元素
186 this.removeTabindex();
187 ele.setAttribute('tabindex', '0'); // 下次期望的聚焦元素
188 },
189 removeTabindex() {
190 this.triggerElm.setAttribute('tabindex', '-1');
191 this.menuItemsArray.forEach((item) => {
192 item.setAttribute('tabindex', '-1');
193 });
194 },
195 initAria() {
196 this.dropdownElm.setAttribute('id', this.listId);
197 this.triggerElm.setAttribute('aria-haspopup', 'list');
198 this.triggerElm.setAttribute('aria-controls', this.listId);
199
200 if (!this.splitButton) { // 自定义
201 this.triggerElm.setAttribute('role', 'button');
202 this.triggerElm.setAttribute('tabindex', this.tabindex);
203 this.triggerElm.setAttribute('class', (this.triggerElm.getAttribute('class') || '') + ' el-dropdown-selfdefine'); // 控制
204 }
205 },
206 initEvent() {
207 let { trigger, show, hide, handleClick, splitButton, handleTriggerKeyDown, handleItemKeyDown } = this;
208 this.triggerElm = splitButton
209 ? this.$refs.trigger.$el
210 : this.$slots.default[0].elm;
211
212 let dropdownElm = this.dropdownElm;
213
214 this.triggerElm.addEventListener('keydown', handleTriggerKeyDown); // triggerElm keydown
215 dropdownElm.addEventListener('keydown', handleItemKeyDown, true); // item keydown
216 // 控制自定义元素的样式
217 if (!splitButton) {
218 this.triggerElm.addEventListener('focus', () => {
219 this.focusing = true;
220 });
221 this.triggerElm.addEventListener('blur', () => {
222 this.focusing = false;
223 });
224 this.triggerElm.addEventListener('click', () => {
225 this.focusing = false;
226 });
227 }
228 if (trigger === 'hover') {
229 this.triggerElm.addEventListener('mouseenter', show);
230 this.triggerElm.addEventListener('mouseleave', hide);
231 dropdownElm.addEventListener('mouseenter', show);
232 dropdownElm.addEventListener('mouseleave', hide);
233 } else if (trigger === 'click') {
234 this.triggerElm.addEventListener('click', handleClick);
235 }
236 },
237 handleMenuItemClick(command, instance) {
238 if (this.hideOnClick) {
239 this.visible = false;
240 }
241 this.$emit('command', command, instance);
242 },
243 triggerElmFocus() {
244 this.triggerElm.focus && this.triggerElm.focus();
245 },
246 initDomOperation() {
247 this.dropdownElm = this.popperElm;
248 this.menuItems = this.dropdownElm.querySelectorAll("[tabindex='-1']");
249 this.menuItemsArray = [].slice.call(this.menuItems);
250
251 this.initEvent();
252 this.initAria();
253 }
254 },
255
256 render(h) {
257 let { hide, splitButton, type, dropdownSize, disabled } = this;
258
259 const handleMainButtonClick = (event) => {
260 this.$emit('click', event);
261 hide();
262 };
263
264 let triggerElm = null;
265 if (splitButton) {
266 triggerElm = <el-button-group>
267 <el-button type={type} size={dropdownSize} nativeOn-click={handleMainButtonClick} disabled={disabled}>
268 {this.$slots.default}
269 </el-button>
270 <el-button ref="trigger" type={type} size={dropdownSize} class="el-dropdown__caret-button" disabled={disabled}>
271 <i class="el-dropdown__icon el-icon-arrow-down"></i>
272 </el-button>
273 </el-button-group>;
274 } else {
275 triggerElm = this.$slots.default;
276 const vnodeData = triggerElm[0].data || {};
277 let { attrs = {} } = vnodeData;
278 if (disabled && !attrs.disabled) {
279 attrs.disabled = true;
280 vnodeData.attrs = attrs;
281 }
282 }
283 const menuElm = disabled ? null : this.$slots.dropdown;
284
285 return (
286 <div class="el-dropdown" v-clickoutside={hide} aria-disabled={disabled}>
287 {triggerElm}
288 {menuElm}
289 </div>
290 );
291 }
292 };
293</script>