124c7047d607ff8eb8609a1b2ba4575b42909b84
[framework/web/web-ui-fw.git] / libs / js / jquery-mobile-1.1.0 / js / jquery.mobile.forms.select.custom.js
1 /*
2 * custom "selectmenu" plugin
3 */
4
5 //>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
6 //>>description: Extension to select menus to support menu styling, placeholder options, and multi-select features. 
7 //>>label: Selects: Custom menus
8 //>>group: Forms
9 //>>css: ../css/themes/default/jquery.mobile.theme.css, ../css/structure/jquery.mobile.forms.select.css
10
11 define( [
12         "jquery",
13         "./jquery.mobile.buttonMarkup",
14         "./jquery.mobile.core",
15         "./jquery.mobile.dialog",
16         "./jquery.mobile.forms.select",
17         "./jquery.mobile.listview",
18         "./jquery.mobile.page",
19         // NOTE expects ui content in the defined page, see selector for menuPageContent definition
20         "./jquery.mobile.page.sections" ], function( $ ) {
21 //>>excludeEnd("jqmBuildExclude");
22 (function( $, undefined ) {
23         var extendSelect = function( widget ){
24
25                 var select = widget.select,
26                         selectID  = widget.selectID,
27                         label = widget.label,
28                         thisPage = widget.select.closest( ".ui-page" ),
29                         screen = $( "<div>", {"class": "ui-selectmenu-screen ui-screen-hidden"} ).appendTo( thisPage ),
30                         selectOptions = widget._selectOptions(),
31                         isMultiple = widget.isMultiple = widget.select[ 0 ].multiple,
32                         buttonId = selectID + "-button",
33                         menuId = selectID + "-menu",
34                         menuPage = $( "<div data-" + $.mobile.ns + "role='dialog' data-" +$.mobile.ns + "theme='"+ widget.options.theme +"' data-" +$.mobile.ns + "overlay-theme='"+ widget.options.overlayTheme +"'>" +
35                                 "<div data-" + $.mobile.ns + "role='header'>" +
36                                 "<div class='ui-title'>" + label.getEncodedText() + "</div>"+
37                                 "</div>"+
38                                 "<div data-" + $.mobile.ns + "role='content'></div>"+
39                                 "</div>" ),
40
41                         listbox =  $("<div>", { "class": "ui-selectmenu ui-selectmenu-hidden ui-overlay-shadow ui-corner-all ui-body-" + widget.options.overlayTheme + " " + $.mobile.defaultDialogTransition } ).insertAfter(screen),
42
43                         list = $( "<ul>", {
44                                 "class": "ui-selectmenu-list",
45                                 "id": menuId,
46                                 "role": "listbox",
47                                 "aria-labelledby": buttonId
48                         }).attr( "data-" + $.mobile.ns + "theme", widget.options.theme ).appendTo( listbox ),
49
50                         header = $( "<div>", {
51                                 "class": "ui-header ui-bar-" + widget.options.theme
52                         }).prependTo( listbox ),
53
54                         headerTitle = $( "<h1>", {
55                                 "class": "ui-title"
56                         }).appendTo( header ),
57
58                         menuPageContent,
59                         menuPageClose,
60                         headerClose;
61
62                 if( widget.isMultiple ) {
63                         headerClose = $( "<a>", {
64                                 "text": widget.options.closeText,
65                                 "href": "#",
66                                 "class": "ui-btn-left"
67                         }).attr( "data-" + $.mobile.ns + "iconpos", "notext" ).attr( "data-" + $.mobile.ns + "icon", "delete" ).appendTo( header ).buttonMarkup();
68                 }
69
70                 $.extend( widget, {
71                         select: widget.select,
72                         selectID: selectID,
73                         buttonId: buttonId,
74                         menuId: menuId,
75                         thisPage: thisPage,
76                         menuPage: menuPage,
77                         label: label,
78                         screen: screen,
79                         selectOptions: selectOptions,
80                         isMultiple: isMultiple,
81                         theme: widget.options.theme,
82                         listbox: listbox,
83                         list: list,
84                         header: header,
85                         headerTitle: headerTitle,
86                         headerClose: headerClose,
87                         menuPageContent: menuPageContent,
88                         menuPageClose: menuPageClose,
89                         placeholder: "",
90
91                         build: function() {
92                                 var self = this;
93
94                                 // Create list from select, update state
95                                 self.refresh();
96
97                                 self.select.attr( "tabindex", "-1" ).focus(function() {
98                                         $( this ).blur();
99                                         self.button.focus();
100                                 });
101
102                                 // Button events
103                                 self.button.bind( "vclick keydown" , function( event ) {
104                                         if ( event.type == "vclick" ||
105                                                          event.keyCode && ( event.keyCode === $.mobile.keyCode.ENTER ||
106                                                                                                                                         event.keyCode === $.mobile.keyCode.SPACE ) ) {
107
108                                                 self.open();
109                                                 event.preventDefault();
110                                         }
111                                 });
112
113                                 // Events for list items
114                                 self.list.attr( "role", "listbox" )
115                                         .bind( "focusin", function( e ){
116                                                 $( e.target )
117                                                         .attr( "tabindex", "0" )
118                                                         .trigger( "vmouseover" );
119
120                                         })
121                                         .bind( "focusout", function( e ){
122                                                 $( e.target )
123                                                         .attr( "tabindex", "-1" )
124                                                         .trigger( "vmouseout" );
125                                         })
126                                         .delegate( "li:not(.ui-disabled, .ui-li-divider)", "click", function( event ) {
127
128                                                 // index of option tag to be selected
129                                                 var oldIndex = self.select[ 0 ].selectedIndex,
130                                                         newIndex = self.list.find( "li:not(.ui-li-divider)" ).index( this ),
131                                                         option = self._selectOptions().eq( newIndex )[ 0 ];
132
133                                                 // toggle selected status on the tag for multi selects
134                                                 option.selected = self.isMultiple ? !option.selected : true;
135
136                                                 // toggle checkbox class for multiple selects
137                                                 if ( self.isMultiple ) {
138                                                         $( this ).find( ".ui-icon" )
139                                                                 .toggleClass( "ui-icon-checkbox-on", option.selected )
140                                                                 .toggleClass( "ui-icon-checkbox-off", !option.selected );
141                                                 }
142
143                                                 // trigger change if value changed
144                                                 if ( self.isMultiple || oldIndex !== newIndex ) {
145                                                         self.select.trigger( "change" );
146                                                 }
147
148                                                 //hide custom select for single selects only
149                                                 if ( !self.isMultiple ) {
150                                                         self.close();
151                                                 }
152
153                                                 event.preventDefault();
154                                         })
155                                         .keydown(function( event ) {  //keyboard events for menu items
156                                                 var target = $( event.target ),
157                                                         li = target.closest( "li" ),
158                                                         prev, next;
159
160                                                 // switch logic based on which key was pressed
161                                                 switch ( event.keyCode ) {
162                                                         // up or left arrow keys
163                                                  case 38:
164                                                         prev = li.prev().not( ".ui-selectmenu-placeholder" );
165
166                                                         if( prev.is( ".ui-li-divider" ) ) {
167                                                                 prev = prev.prev();
168                                                         }
169
170                                                         // if there's a previous option, focus it
171                                                         if ( prev.length ) {
172                                                                 target
173                                                                         .blur()
174                                                                         .attr( "tabindex", "-1" );
175
176                                                                 prev.addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus();
177                                                         }
178
179                                                         return false;
180                                                         break;
181
182                                                         // down or right arrow keys
183                                                  case 40:
184                                                         next = li.next();
185
186                                                         if( next.is( ".ui-li-divider" ) ) {
187                                                                 next = next.next();
188                                                         }
189
190                                                         // if there's a next option, focus it
191                                                         if ( next.length ) {
192                                                                 target
193                                                                         .blur()
194                                                                         .attr( "tabindex", "-1" );
195
196                                                                 next.addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus();
197                                                         }
198
199                                                         return false;
200                                                         break;
201
202                                                         // If enter or space is pressed, trigger click
203                                                  case 13:
204                                                  case 32:
205                                                         target.trigger( "click" );
206
207                                                         return false;
208                                                         break;
209                                                 }
210                                         });
211
212                                 // button refocus ensures proper height calculation
213                                 // by removing the inline style and ensuring page inclusion
214                                 self.menuPage.bind( "pagehide", function() {
215                                         self.list.appendTo( self.listbox );
216                                         self._focusButton();
217
218                                         // TODO centralize page removal binding / handling in the page plugin.
219                                         // Suggestion from @jblas to do refcounting
220                                         //
221                                         // TODO extremely confusing dependency on the open method where the pagehide.remove
222                                         // bindings are stripped to prevent the parent page from disappearing. The way
223                                         // we're keeping pages in the DOM right now sucks
224                                         //
225                                         // rebind the page remove that was unbound in the open function
226                                         // to allow for the parent page removal from actions other than the use
227                                         // of a dialog sized custom select
228                                         //
229                                         // doing this here provides for the back button on the custom select dialog
230                                         $.mobile._bindPageRemove.call( self.thisPage );
231                                 });
232
233                                 // Events on "screen" overlay
234                                 self.screen.bind( "vclick", function( event ) {
235                                         self.close();
236                                 });
237
238                                 // Close button on small overlays
239                                 if( self.isMultiple ){
240                                         self.headerClose.click( function() {
241                                                 if ( self.menuType == "overlay" ) {
242                                                         self.close();
243                                                         return false;
244                                                 }
245                                         });
246                                 }
247
248                                 // track this dependency so that when the parent page
249                                 // is removed on pagehide it will also remove the menupage
250                                 self.thisPage.addDependents( this.menuPage );
251                         },
252
253                         _isRebuildRequired: function() {
254                                 var list = this.list.find( "li" ),
255                                         options = this._selectOptions();
256
257                                 // TODO exceedingly naive method to determine difference
258                                 // ignores value changes etc in favor of a forcedRebuild
259                                 // from the user in the refresh method
260                                 return options.text() !== list.text();
261                         },
262
263                         refresh: function( forceRebuild , foo ){
264                                 var self = this,
265                                 select = this.element,
266                                 isMultiple = this.isMultiple,
267                                 options = this._selectOptions(),
268                                 selected = this.selected(),
269                                 // return an array of all selected index's
270                                 indicies = this.selectedIndices();
271
272                                 if (  forceRebuild || this._isRebuildRequired() ) {
273                                         self._buildList();
274                                 }
275
276                                 self.setButtonText();
277                                 self.setButtonCount();
278
279                                 self.list.find( "li:not(.ui-li-divider)" )
280                                         .removeClass( $.mobile.activeBtnClass )
281                                         .attr( "aria-selected", false )
282                                         .each(function( i ) {
283
284                                                 if ( $.inArray( i, indicies ) > -1 ) {
285                                                         var item = $( this );
286
287                                                         // Aria selected attr
288                                                         item.attr( "aria-selected", true );
289
290                                                         // Multiple selects: add the "on" checkbox state to the icon
291                                                         if ( self.isMultiple ) {
292                                                                 item.find( ".ui-icon" ).removeClass( "ui-icon-checkbox-off" ).addClass( "ui-icon-checkbox-on" );
293                                                         } else {
294                                                                 if( item.is( ".ui-selectmenu-placeholder" ) ) {
295                                                                         item.next().addClass( $.mobile.activeBtnClass );
296                                                                 } else {
297                                                                         item.addClass( $.mobile.activeBtnClass );
298                                                                 }
299                                                         }
300                                                 }
301                                         });
302                         },
303
304                         close: function() {
305                                 if ( this.options.disabled || !this.isOpen ) {
306                                         return;
307                                 }
308
309                                 var self = this;
310
311                                 if ( self.menuType == "page" ) {
312                                         // doesn't solve the possible issue with calling change page
313                                         // where the objects don't define data urls which prevents dialog key
314                                         // stripping - changePage has incoming refactor
315                                         window.history.back();
316                                 } else {
317                                         self.screen.addClass( "ui-screen-hidden" );
318                                         self.listbox.addClass( "ui-selectmenu-hidden" ).removeAttr( "style" ).removeClass( "in" );
319                                         self.list.appendTo( self.listbox );
320                                         self._focusButton();
321                                 }
322
323                                 // allow the dialog to be closed again
324                                 self.isOpen = false;
325                         },
326
327                         open: function() {
328                                 if ( this.options.disabled ) {
329                                         return;
330                                 }
331
332                                 var self = this,
333           $window = $( window ),
334           selfListParent = self.list.parent(),
335                                         menuHeight = selfListParent.outerHeight(),
336                                         menuWidth = selfListParent.outerWidth(),
337                                         activePage = $( ".ui-page-active" ),
338                                         tScrollElem = activePage,
339                                         scrollTop = $window.scrollTop(),
340                                         btnOffset = self.button.offset().top,
341                                         screenHeight = $window.height(),
342                                         screenWidth = $window.width();
343
344                                 //add active class to button
345                                 self.button.addClass( $.mobile.activeBtnClass );
346
347                                 //remove after delay
348                                 setTimeout( function() {
349                                         self.button.removeClass( $.mobile.activeBtnClass );
350                                 }, 300);
351
352                                 function focusMenuItem() {
353                                         self.list.find( "." + $.mobile.activeBtnClass + " a" ).focus();
354                                 }
355
356                                 if ( menuHeight > screenHeight - 80 || !$.support.scrollTop ) {
357
358                                         self.menuPage.appendTo( $.mobile.pageContainer ).page();
359                                         self.menuPageContent = menuPage.find( ".ui-content" );
360                                         self.menuPageClose = menuPage.find( ".ui-header a" );
361
362                                         // prevent the parent page from being removed from the DOM,
363                                         // otherwise the results of selecting a list item in the dialog
364                                         // fall into a black hole
365                                         self.thisPage.unbind( "pagehide.remove" );
366
367                                         //for WebOS/Opera Mini (set lastscroll using button offset)
368                                         if ( scrollTop == 0 && btnOffset > screenHeight ) {
369                                                 self.thisPage.one( "pagehide", function() {
370                                                         $( this ).jqmData( "lastScroll", btnOffset );
371                                                 });
372                                         }
373
374                                         self.menuPage.one( "pageshow", function() {
375                                                 focusMenuItem();
376                                                 self.isOpen = true;
377                                         });
378
379                                         self.menuType = "page";
380                                         self.menuPageContent.append( self.list );
381                                         self.menuPage.find("div .ui-title").text(self.label.text());
382                                         $.mobile.changePage( self.menuPage, {
383                                                 transition: $.mobile.defaultDialogTransition
384                                         });
385                                 } else {
386                                         self.menuType = "overlay";
387
388                                         self.screen.height( $(document).height() )
389                                                 .removeClass( "ui-screen-hidden" );
390
391                                         // Try and center the overlay over the button
392                                         var roomtop = btnOffset - scrollTop,
393                                                 roombot = scrollTop + screenHeight - btnOffset,
394                                                 halfheight = menuHeight / 2,
395                                                 maxwidth = parseFloat( self.list.parent().css( "max-width" ) ),
396                                                 newtop, newleft;
397
398                                         if ( roomtop > menuHeight / 2 && roombot > menuHeight / 2 ) {
399                                                 newtop = btnOffset + ( self.button.outerHeight() / 2 ) - halfheight;
400                                         } else {
401                                                 // 30px tolerance off the edges
402                                                 newtop = roomtop > roombot ? scrollTop + screenHeight - menuHeight - 30 : scrollTop + 30;
403                                         }
404
405                                         // If the menuwidth is smaller than the screen center is
406                                         if ( menuWidth < maxwidth ) {
407                                                 newleft = ( screenWidth - menuWidth ) / 2;
408                                         } else {
409
410                                                 //otherwise insure a >= 30px offset from the left
411                                                 newleft = self.button.offset().left + self.button.outerWidth() / 2 - menuWidth / 2;
412
413                                                 // 30px tolerance off the edges
414                                                 if ( newleft < 30 ) {
415                                                         newleft = 30;
416                                                 } else if ( (newleft + menuWidth) > screenWidth ) {
417                                                         newleft = screenWidth - menuWidth - 30;
418                                                 }
419                                         }
420
421                                         self.listbox.append( self.list )
422                                                 .removeClass( "ui-selectmenu-hidden" )
423                                                 .css({
424                                                         top: newtop,
425                                                         left: newleft
426                                                 })
427                                                 .addClass( "in" );
428
429                                         focusMenuItem();
430
431                                         // duplicate with value set in page show for dialog sized selects
432                                         self.isOpen = true;
433                                 }
434                         },
435
436                         _buildList: function() {
437                                 var self = this,
438                                         o = this.options,
439                                         placeholder = this.placeholder,
440                                         needPlaceholder = true,
441                                         optgroups = [],
442                                         lis = [],
443                                         dataIcon = self.isMultiple ? "checkbox-off" : "false";
444
445                                 self.list.empty().filter( ".ui-listview" ).listview( "destroy" );
446
447                                 var $options = self.select.find("option"),
448                                         numOptions = $options.length,
449                                         select = this.select[ 0 ],
450                                         dataPrefix = 'data-' + $.mobile.ns,
451                                         dataIndexAttr = dataPrefix + 'option-index',
452                                         dataIconAttr = dataPrefix + 'icon',
453                                         dataRoleAttr = dataPrefix + 'role',
454                                         fragment = document.createDocumentFragment(),
455                                         optGroup;
456
457                                 for (var i = 0; i < numOptions;i++){
458                                         var option = $options[i],
459                                                 $option = $(option),
460                                                 parent = option.parentNode,
461                                                 text = $option.text(),
462                                                 anchor  = document.createElement('a'),
463                                                 classes = [];
464
465                                         anchor.setAttribute('href','#');
466                                         anchor.appendChild(document.createTextNode(text));
467
468                                         // Are we inside an optgroup?
469                                         if (parent !== select && parent.nodeName.toLowerCase() === "optgroup"){
470                                                 var optLabel = parent.getAttribute('label');
471                                                 if ( optLabel != optGroup) {
472                                                         var divider = document.createElement('li');
473                                                         divider.setAttribute(dataRoleAttr,'list-divider');
474                                                         divider.setAttribute('role','option');
475                                                         divider.setAttribute('tabindex','-1');
476                                                         divider.appendChild(document.createTextNode(optLabel));
477                                                         fragment.appendChild(divider);
478                                                         optGroup = optLabel;
479                                                 }
480                                         }
481
482                                         if (needPlaceholder && (!option.getAttribute( "value" ) || text.length == 0 || $option.jqmData( "placeholder" ))) {
483                                                 needPlaceholder = false;
484                                                 if ( o.hidePlaceholderMenuItems ) {
485                                                         classes.push( "ui-selectmenu-placeholder" );
486                                                 }
487                                                 if (!placeholder) {
488                                                         placeholder = self.placeholder = text;
489                                                 }
490                                         }
491
492                                         var item = document.createElement('li');
493                                         if ( option.disabled ) {
494                                                 classes.push( "ui-disabled" );
495                                                 item.setAttribute('aria-disabled',true);
496                                         }
497                                         item.setAttribute(dataIndexAttr,i);
498                                         item.setAttribute(dataIconAttr,dataIcon);
499                                         item.className = classes.join(" ");
500                                         item.setAttribute('role','option');
501                                         anchor.setAttribute('tabindex','-1');
502                                         item.appendChild(anchor);
503                                         fragment.appendChild(item);
504                                 }
505
506                                 self.list[0].appendChild(fragment);
507
508                                 // Hide header if it's not a multiselect and there's no placeholder
509                                 if ( !this.isMultiple && !placeholder.length ) {
510                                         this.header.hide();
511                                 } else {
512                                         this.headerTitle.text( this.placeholder );
513                                 }
514
515                                 // Now populated, create listview
516                                 self.list.listview();
517                         },
518
519                         _button: function(){
520                                 return $( "<a>", {
521                                         "href": "#",
522                                         "role": "button",
523                                         // TODO value is undefined at creation
524                                         "id": this.buttonId,
525                                         "aria-haspopup": "true",
526
527                                         // TODO value is undefined at creation
528                                         "aria-owns": this.menuId
529                                 });
530                         }
531                 });
532         };
533
534         // issue #3894 - core doesn't triggered events on disabled delegates
535         $( document ).bind( "selectmenubeforecreate", function( event ){
536                 var selectmenuWidget = $( event.target ).data( "selectmenu" );
537
538                 if( !selectmenuWidget.options.nativeMenu ){
539                         extendSelect( selectmenuWidget );
540                 }
541         });
542 })( jQuery );
543 //>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
544 });
545 //>>excludeEnd("jqmBuildExclude");