100 clearTimeout( this.timer ); |
106 clearTimeout( this.timer ); |
101 } |
107 } |
102 } |
108 } |
103 } |
109 } |
104 }, |
110 }, |
105 "mouseenter .ui-menu-item": function( event ) { |
111 "mouseenter .ui-menu-item": "_activateItem", |
106 |
112 "mousemove .ui-menu-item": "_activateItem", |
107 // Ignore mouse events while typeahead is active, see #10458. |
|
108 // Prevents focusing the wrong item when typeahead causes a scroll while the mouse |
|
109 // is over an item in the menu |
|
110 if ( this.previousFilter ) { |
|
111 return; |
|
112 } |
|
113 |
|
114 var actualTarget = $( event.target ).closest( ".ui-menu-item" ), |
|
115 target = $( event.currentTarget ); |
|
116 |
|
117 // Ignore bubbled events on parent items, see #11641 |
|
118 if ( actualTarget[ 0 ] !== target[ 0 ] ) { |
|
119 return; |
|
120 } |
|
121 |
|
122 // Remove ui-state-active class from siblings of the newly focused menu item |
|
123 // to avoid a jump caused by adjacent elements both having a class with a border |
|
124 this._removeClass( target.siblings().children( ".ui-state-active" ), |
|
125 null, "ui-state-active" ); |
|
126 this.focus( event, target ); |
|
127 }, |
|
128 mouseleave: "collapseAll", |
113 mouseleave: "collapseAll", |
129 "mouseleave .ui-menu": "collapseAll", |
114 "mouseleave .ui-menu": "collapseAll", |
130 focus: function( event, keepActiveItem ) { |
115 focus: function( event, keepActiveItem ) { |
131 |
116 |
132 // If there's already an active item, keep it active |
117 // If there's already an active item, keep it active |
133 // If not, activate the first item |
118 // If not, activate the first item |
134 var item = this.active || this.element.find( this.options.items ).eq( 0 ); |
119 var item = this.active || this._menuItems().first(); |
135 |
120 |
136 if ( !keepActiveItem ) { |
121 if ( !keepActiveItem ) { |
137 this.focus( event, item ); |
122 this.focus( event, item ); |
138 } |
123 } |
139 }, |
124 }, |
155 |
140 |
156 // Clicks outside of a menu collapse any open menus |
141 // Clicks outside of a menu collapse any open menus |
157 this._on( this.document, { |
142 this._on( this.document, { |
158 click: function( event ) { |
143 click: function( event ) { |
159 if ( this._closeOnDocumentClick( event ) ) { |
144 if ( this._closeOnDocumentClick( event ) ) { |
160 this.collapseAll( event ); |
145 this.collapseAll( event, true ); |
161 } |
146 } |
162 |
147 |
163 // Reset the mouseHandled flag |
148 // Reset the mouseHandled flag |
164 this.mouseHandled = false; |
149 this.mouseHandled = false; |
165 } |
150 } |
166 } ); |
151 } ); |
|
152 }, |
|
153 |
|
154 _activateItem: function( event ) { |
|
155 |
|
156 // Ignore mouse events while typeahead is active, see #10458. |
|
157 // Prevents focusing the wrong item when typeahead causes a scroll while the mouse |
|
158 // is over an item in the menu |
|
159 if ( this.previousFilter ) { |
|
160 return; |
|
161 } |
|
162 |
|
163 // If the mouse didn't actually move, but the page was scrolled, ignore the event (#9356) |
|
164 if ( event.clientX === this.lastMousePosition.x && |
|
165 event.clientY === this.lastMousePosition.y ) { |
|
166 return; |
|
167 } |
|
168 |
|
169 this.lastMousePosition = { |
|
170 x: event.clientX, |
|
171 y: event.clientY |
|
172 }; |
|
173 |
|
174 var actualTarget = $( event.target ).closest( ".ui-menu-item" ), |
|
175 target = $( event.currentTarget ); |
|
176 |
|
177 // Ignore bubbled events on parent items, see #11641 |
|
178 if ( actualTarget[ 0 ] !== target[ 0 ] ) { |
|
179 return; |
|
180 } |
|
181 |
|
182 // If the item is already active, there's nothing to do |
|
183 if ( target.is( ".ui-state-active" ) ) { |
|
184 return; |
|
185 } |
|
186 |
|
187 // Remove ui-state-active class from siblings of the newly focused menu item |
|
188 // to avoid a jump caused by adjacent elements both having a class with a border |
|
189 this._removeClass( target.siblings().children( ".ui-state-active" ), |
|
190 null, "ui-state-active" ); |
|
191 this.focus( event, target ); |
167 }, |
192 }, |
168 |
193 |
169 _destroy: function() { |
194 _destroy: function() { |
170 var items = this.element.find( ".ui-menu-item" ) |
195 var items = this.element.find( ".ui-menu-item" ) |
171 .removeAttr( "role aria-disabled" ), |
196 .removeAttr( "role aria-disabled" ), |
193 _keydown: function( event ) { |
218 _keydown: function( event ) { |
194 var match, prev, character, skip, |
219 var match, prev, character, skip, |
195 preventDefault = true; |
220 preventDefault = true; |
196 |
221 |
197 switch ( event.keyCode ) { |
222 switch ( event.keyCode ) { |
198 case $.ui.keyCode.PAGE_UP: |
223 case $.ui.keyCode.PAGE_UP: |
199 this.previousPage( event ); |
224 this.previousPage( event ); |
200 break; |
225 break; |
201 case $.ui.keyCode.PAGE_DOWN: |
226 case $.ui.keyCode.PAGE_DOWN: |
202 this.nextPage( event ); |
227 this.nextPage( event ); |
203 break; |
228 break; |
204 case $.ui.keyCode.HOME: |
229 case $.ui.keyCode.HOME: |
205 this._move( "first", "first", event ); |
230 this._move( "first", "first", event ); |
206 break; |
231 break; |
207 case $.ui.keyCode.END: |
232 case $.ui.keyCode.END: |
208 this._move( "last", "last", event ); |
233 this._move( "last", "last", event ); |
209 break; |
234 break; |
210 case $.ui.keyCode.UP: |
235 case $.ui.keyCode.UP: |
211 this.previous( event ); |
236 this.previous( event ); |
212 break; |
237 break; |
213 case $.ui.keyCode.DOWN: |
238 case $.ui.keyCode.DOWN: |
214 this.next( event ); |
239 this.next( event ); |
215 break; |
240 break; |
216 case $.ui.keyCode.LEFT: |
241 case $.ui.keyCode.LEFT: |
217 this.collapse( event ); |
242 this.collapse( event ); |
218 break; |
243 break; |
219 case $.ui.keyCode.RIGHT: |
244 case $.ui.keyCode.RIGHT: |
220 if ( this.active && !this.active.is( ".ui-state-disabled" ) ) { |
245 if ( this.active && !this.active.is( ".ui-state-disabled" ) ) { |
221 this.expand( event ); |
246 this.expand( event ); |
222 } |
247 } |
223 break; |
248 break; |
224 case $.ui.keyCode.ENTER: |
249 case $.ui.keyCode.ENTER: |
225 case $.ui.keyCode.SPACE: |
250 case $.ui.keyCode.SPACE: |
226 this._activate( event ); |
251 this._activate( event ); |
227 break; |
252 break; |
228 case $.ui.keyCode.ESCAPE: |
253 case $.ui.keyCode.ESCAPE: |
229 this.collapse( event ); |
254 this.collapse( event ); |
230 break; |
255 break; |
231 default: |
256 default: |
232 preventDefault = false; |
257 preventDefault = false; |
233 prev = this.previousFilter || ""; |
258 prev = this.previousFilter || ""; |
234 skip = false; |
259 skip = false; |
235 |
260 |
236 // Support number pad values |
261 // Support number pad values |
237 character = event.keyCode >= 96 && event.keyCode <= 105 ? |
262 character = event.keyCode >= 96 && event.keyCode <= 105 ? |
238 ( event.keyCode - 96 ).toString() : String.fromCharCode( event.keyCode ); |
263 ( event.keyCode - 96 ).toString() : String.fromCharCode( event.keyCode ); |
239 |
264 |
240 clearTimeout( this.filterTimer ); |
265 clearTimeout( this.filterTimer ); |
241 |
266 |
242 if ( character === prev ) { |
267 if ( character === prev ) { |
243 skip = true; |
268 skip = true; |
244 } else { |
269 } else { |
245 character = prev + character; |
270 character = prev + character; |
246 } |
271 } |
247 |
272 |
248 match = this._filterMenuItems( character ); |
|
249 match = skip && match.index( this.active.next() ) !== -1 ? |
|
250 this.active.nextAll( ".ui-menu-item" ) : |
|
251 match; |
|
252 |
|
253 // If no matches on the current filter, reset to the last character pressed |
|
254 // to move down the menu to the first item that starts with that character |
|
255 if ( !match.length ) { |
|
256 character = String.fromCharCode( event.keyCode ); |
|
257 match = this._filterMenuItems( character ); |
273 match = this._filterMenuItems( character ); |
258 } |
274 match = skip && match.index( this.active.next() ) !== -1 ? |
259 |
275 this.active.nextAll( ".ui-menu-item" ) : |
260 if ( match.length ) { |
276 match; |
261 this.focus( event, match ); |
277 |
262 this.previousFilter = character; |
278 // If no matches on the current filter, reset to the last character pressed |
263 this.filterTimer = this._delay( function() { |
279 // to move down the menu to the first item that starts with that character |
|
280 if ( !match.length ) { |
|
281 character = String.fromCharCode( event.keyCode ); |
|
282 match = this._filterMenuItems( character ); |
|
283 } |
|
284 |
|
285 if ( match.length ) { |
|
286 this.focus( event, match ); |
|
287 this.previousFilter = character; |
|
288 this.filterTimer = this._delay( function() { |
|
289 delete this.previousFilter; |
|
290 }, 1000 ); |
|
291 } else { |
264 delete this.previousFilter; |
292 delete this.previousFilter; |
265 }, 1000 ); |
293 } |
266 } else { |
|
267 delete this.previousFilter; |
|
268 } |
|
269 } |
294 } |
270 |
295 |
271 if ( preventDefault ) { |
296 if ( preventDefault ) { |
272 event.preventDefault(); |
297 event.preventDefault(); |
273 } |
298 } |
561 return this.active && !this.active.prevAll( ".ui-menu-item" ).length; |
582 return this.active && !this.active.prevAll( ".ui-menu-item" ).length; |
562 }, |
583 }, |
563 |
584 |
564 isLastItem: function() { |
585 isLastItem: function() { |
565 return this.active && !this.active.nextAll( ".ui-menu-item" ).length; |
586 return this.active && !this.active.nextAll( ".ui-menu-item" ).length; |
|
587 }, |
|
588 |
|
589 _menuItems: function( menu ) { |
|
590 return ( menu || this.element ) |
|
591 .find( this.options.items ) |
|
592 .filter( ".ui-menu-item" ); |
566 }, |
593 }, |
567 |
594 |
568 _move: function( direction, filter, event ) { |
595 _move: function( direction, filter, event ) { |
569 var next; |
596 var next; |
570 if ( this.active ) { |
597 if ( this.active ) { |
571 if ( direction === "first" || direction === "last" ) { |
598 if ( direction === "first" || direction === "last" ) { |
572 next = this.active |
599 next = this.active |
573 [ direction === "first" ? "prevAll" : "nextAll" ]( ".ui-menu-item" ) |
600 [ direction === "first" ? "prevAll" : "nextAll" ]( ".ui-menu-item" ) |
574 .eq( -1 ); |
601 .last(); |
575 } else { |
602 } else { |
576 next = this.active |
603 next = this.active |
577 [ direction + "All" ]( ".ui-menu-item" ) |
604 [ direction + "All" ]( ".ui-menu-item" ) |
578 .eq( 0 ); |
605 .first(); |
579 } |
606 } |
580 } |
607 } |
581 if ( !next || !next.length || !this.active ) { |
608 if ( !next || !next.length || !this.active ) { |
582 next = this.activeMenu.find( this.options.items )[ filter ](); |
609 next = this._menuItems( this.activeMenu )[ filter ](); |
583 } |
610 } |
584 |
611 |
585 this.focus( event, next ); |
612 this.focus( event, next ); |
586 }, |
613 }, |
587 |
614 |
595 if ( this.isLastItem() ) { |
622 if ( this.isLastItem() ) { |
596 return; |
623 return; |
597 } |
624 } |
598 if ( this._hasScroll() ) { |
625 if ( this._hasScroll() ) { |
599 base = this.active.offset().top; |
626 base = this.active.offset().top; |
600 height = this.element.height(); |
627 height = this.element.innerHeight(); |
|
628 |
|
629 // jQuery 3.2 doesn't include scrollbars in innerHeight, add it back. |
|
630 if ( $.fn.jquery.indexOf( "3.2." ) === 0 ) { |
|
631 height += this.element[ 0 ].offsetHeight - this.element.outerHeight(); |
|
632 } |
|
633 |
601 this.active.nextAll( ".ui-menu-item" ).each( function() { |
634 this.active.nextAll( ".ui-menu-item" ).each( function() { |
602 item = $( this ); |
635 item = $( this ); |
603 return item.offset().top - base - height < 0; |
636 return item.offset().top - base - height < 0; |
604 } ); |
637 } ); |
605 |
638 |
606 this.focus( event, item ); |
639 this.focus( event, item ); |
607 } else { |
640 } else { |
608 this.focus( event, this.activeMenu.find( this.options.items ) |
641 this.focus( event, this._menuItems( this.activeMenu ) |
609 [ !this.active ? "first" : "last" ]() ); |
642 [ !this.active ? "first" : "last" ]() ); |
610 } |
643 } |
611 }, |
644 }, |
612 |
645 |
613 previousPage: function( event ) { |
646 previousPage: function( event ) { |
619 if ( this.isFirstItem() ) { |
652 if ( this.isFirstItem() ) { |
620 return; |
653 return; |
621 } |
654 } |
622 if ( this._hasScroll() ) { |
655 if ( this._hasScroll() ) { |
623 base = this.active.offset().top; |
656 base = this.active.offset().top; |
624 height = this.element.height(); |
657 height = this.element.innerHeight(); |
|
658 |
|
659 // jQuery 3.2 doesn't include scrollbars in innerHeight, add it back. |
|
660 if ( $.fn.jquery.indexOf( "3.2." ) === 0 ) { |
|
661 height += this.element[ 0 ].offsetHeight - this.element.outerHeight(); |
|
662 } |
|
663 |
625 this.active.prevAll( ".ui-menu-item" ).each( function() { |
664 this.active.prevAll( ".ui-menu-item" ).each( function() { |
626 item = $( this ); |
665 item = $( this ); |
627 return item.offset().top - base + height > 0; |
666 return item.offset().top - base + height > 0; |
628 } ); |
667 } ); |
629 |
668 |
630 this.focus( event, item ); |
669 this.focus( event, item ); |
631 } else { |
670 } else { |
632 this.focus( event, this.activeMenu.find( this.options.items ).first() ); |
671 this.focus( event, this._menuItems( this.activeMenu ).first() ); |
633 } |
672 } |
634 }, |
673 }, |
635 |
674 |
636 _hasScroll: function() { |
675 _hasScroll: function() { |
637 return this.element.outerHeight() < this.element.prop( "scrollHeight" ); |
676 return this.element.outerHeight() < this.element.prop( "scrollHeight" ); |