| /* |
| * custom "selectmenu" plugin |
| */ |
| |
| //>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude); |
| //>>description: Extension to select menus to support menu styling, placeholder options, and multi-select features. |
| //>>label: Selects: Custom menus |
| //>>group: Forms |
| //>>css.structure: ../css/structure/jquery.mobile.forms.select.css |
| //>>css.theme: ../css/themes/default/jquery.mobile.theme.css |
| |
| define( [ |
| "jquery", |
| "../../jquery.mobile.buttonMarkup", |
| "../../jquery.mobile.core", |
| "../dialog", |
| "./select", |
| "../listview", |
| "../page", |
| "../popup", |
| // NOTE expects ui content in the defined page, see selector for menuPageContent definition |
| "../page.sections" ], function( jQuery ) { |
| //>>excludeEnd("jqmBuildExclude"); |
| (function( $, undefined ) { |
| var extendSelect = function( widget ) { |
| |
| var select = widget.select, |
| selectID = widget.selectID, |
| label = widget.label, |
| thisPage = widget.select.closest( ".ui-page" ), |
| selectOptions = widget._selectOptions(), |
| isMultiple = widget.isMultiple = widget.select[ 0 ].multiple, |
| buttonId = selectID + "-button", |
| menuId = selectID + "-menu", |
| menuPage = $( "<div data-" + $.mobile.ns + "role='dialog' data-" +$.mobile.ns + "theme='"+ widget.options.theme +"' data-" +$.mobile.ns + "overlay-theme='"+ widget.options.overlayTheme +"'>" + |
| "<div data-" + $.mobile.ns + "role='header'>" + |
| "<div class='ui-title'>" + label.getEncodedText() + "</div>"+ |
| "</div>"+ |
| "<div data-" + $.mobile.ns + "role='content'></div>"+ |
| "</div>" ), |
| |
| listbox = $( "<div>", { "class": "ui-selectmenu" } ).insertAfter( widget.select ).popup( { theme: "a" } ), |
| |
| list = $( "<ul>", { |
| "class": "ui-selectmenu-list", |
| "id": menuId, |
| "role": "listbox", |
| "aria-labelledby": buttonId |
| }).attr( "data-" + $.mobile.ns + "theme", widget.options.theme ).appendTo( listbox ), |
| |
| header = $( "<div>", { |
| "class": "ui-header ui-bar-" + widget.options.theme |
| }).prependTo( listbox ), |
| |
| headerTitle = $( "<h1>", { |
| "class": "ui-title" |
| }).appendTo( header ), |
| |
| menuPageContent, |
| menuPageClose, |
| headerClose; |
| |
| if ( widget.isMultiple ) { |
| headerClose = $( "<a>", { |
| "text": widget.options.closeText, |
| "href": "#", |
| "class": "ui-btn-left" |
| }).attr( "data-" + $.mobile.ns + "iconpos", "notext" ).attr( "data-" + $.mobile.ns + "icon", "delete" ).appendTo( header ).buttonMarkup(); |
| } |
| |
| $.extend( widget, { |
| select: widget.select, |
| selectID: selectID, |
| buttonId: buttonId, |
| menuId: menuId, |
| thisPage: thisPage, |
| menuPage: menuPage, |
| label: label, |
| selectOptions: selectOptions, |
| isMultiple: isMultiple, |
| theme: widget.options.theme, |
| listbox: listbox, |
| list: list, |
| header: header, |
| headerTitle: headerTitle, |
| headerClose: headerClose, |
| menuPageContent: menuPageContent, |
| menuPageClose: menuPageClose, |
| placeholder: "", |
| |
| build: function() { |
| var self = this; |
| |
| // Create list from select, update state |
| self.refresh(); |
| |
| self.select.attr( "tabindex", "-1" ).focus(function() { |
| $( this ).blur(); |
| self.button.focus(); |
| }); |
| |
| // Button events |
| self.button.bind( "vclick keydown" , function( event ) { |
| if (event.type === "vclick" || |
| event.keyCode && (event.keyCode === $.mobile.keyCode.ENTER || |
| event.keyCode === $.mobile.keyCode.SPACE)) { |
| |
| self.open(); |
| event.preventDefault(); |
| } |
| }); |
| |
| // Events for list items |
| self.list.attr( "role", "listbox" ) |
| .bind( "focusin", function( e ) { |
| $( e.target ) |
| .attr( "tabindex", "0" ) |
| .trigger( "vmouseover" ); |
| |
| }) |
| .bind( "focusout", function( e ) { |
| $( e.target ) |
| .attr( "tabindex", "-1" ) |
| .trigger( "vmouseout" ); |
| }) |
| .delegate( "li:not(.ui-disabled, .ui-li-divider)", "click", function( event ) { |
| |
| // index of option tag to be selected |
| var oldIndex = self.select[ 0 ].selectedIndex, |
| newIndex = self.list.find( "li:not(.ui-li-divider)" ).index( this ), |
| option = self._selectOptions().eq( newIndex )[ 0 ]; |
| |
| // toggle selected status on the tag for multi selects |
| option.selected = self.isMultiple ? !option.selected : true; |
| |
| // toggle checkbox class for multiple selects |
| if ( self.isMultiple ) { |
| $( this ).find( ".ui-icon" ) |
| .toggleClass( "ui-icon-checkbox-on", option.selected ) |
| .toggleClass( "ui-icon-checkbox-off", !option.selected ); |
| } |
| |
| // trigger change if value changed |
| if ( self.isMultiple || oldIndex !== newIndex ) { |
| self.select.trigger( "change" ); |
| } |
| |
| // hide custom select for single selects only - otherwise focus clicked item |
| // We need to grab the clicked item the hard way, because the list may have been rebuilt |
| if ( self.isMultiple ) { |
| self.list.find( "li:not(.ui-li-divider)" ).eq( newIndex ) |
| .addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus(); |
| } |
| else { |
| self.close(); |
| } |
| |
| event.preventDefault(); |
| }) |
| .keydown(function( event ) { //keyboard events for menu items |
| var target = $( event.target ), |
| li = target.closest( "li" ), |
| prev, next; |
| |
| // switch logic based on which key was pressed |
| switch ( event.keyCode ) { |
| // up or left arrow keys |
| case 38: |
| prev = li.prev().not( ".ui-selectmenu-placeholder" ); |
| |
| if ( prev.is( ".ui-li-divider" ) ) { |
| prev = prev.prev(); |
| } |
| |
| // if there's a previous option, focus it |
| if ( prev.length ) { |
| target |
| .blur() |
| .attr( "tabindex", "-1" ); |
| |
| prev.addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus(); |
| } |
| |
| return false; |
| // down or right arrow keys |
| case 40: |
| next = li.next(); |
| |
| if ( next.is( ".ui-li-divider" ) ) { |
| next = next.next(); |
| } |
| |
| // if there's a next option, focus it |
| if ( next.length ) { |
| target |
| .blur() |
| .attr( "tabindex", "-1" ); |
| |
| next.addClass( "ui-btn-down-" + widget.options.theme ).find( "a" ).first().focus(); |
| } |
| |
| return false; |
| // If enter or space is pressed, trigger click |
| case 13: |
| case 32: |
| target.trigger( "click" ); |
| |
| return false; |
| } |
| }); |
| |
| // button refocus ensures proper height calculation |
| // by removing the inline style and ensuring page inclusion |
| self.menuPage.bind( "pagehide", function() { |
| self.list.appendTo( self.listbox ); |
| self._focusButton(); |
| |
| // TODO centralize page removal binding / handling in the page plugin. |
| // Suggestion from @jblas to do refcounting |
| // |
| // TODO extremely confusing dependency on the open method where the pagehide.remove |
| // bindings are stripped to prevent the parent page from disappearing. The way |
| // we're keeping pages in the DOM right now sucks |
| // |
| // rebind the page remove that was unbound in the open function |
| // to allow for the parent page removal from actions other than the use |
| // of a dialog sized custom select |
| // |
| // doing this here provides for the back button on the custom select dialog |
| $.mobile._bindPageRemove.call( self.thisPage ); |
| }); |
| |
| // Events on the popup |
| self.listbox.bind( "popupafterclose", function( event ) { |
| self.close(); |
| }); |
| |
| // Close button on small overlays |
| if ( self.isMultiple ) { |
| self.headerClose.click(function() { |
| if ( self.menuType === "overlay" ) { |
| self.close(); |
| return false; |
| } |
| }); |
| } |
| |
| // track this dependency so that when the parent page |
| // is removed on pagehide it will also remove the menupage |
| self.thisPage.addDependents( this.menuPage ); |
| }, |
| |
| _isRebuildRequired: function() { |
| var list = this.list.find( "li" ), |
| options = this._selectOptions(); |
| |
| // TODO exceedingly naive method to determine difference |
| // ignores value changes etc in favor of a forcedRebuild |
| // from the user in the refresh method |
| return options.text() !== list.text(); |
| }, |
| |
| selected: function() { |
| return this._selectOptions().filter( ":selected:not( :jqmData(placeholder='true') )" ); |
| }, |
| |
| refresh: function( forceRebuild , foo ) { |
| var self = this, |
| select = this.element, |
| isMultiple = this.isMultiple, |
| indicies; |
| |
| if ( forceRebuild || this._isRebuildRequired() ) { |
| self._buildList(); |
| } |
| |
| indicies = this.selectedIndices(); |
| |
| self.setButtonText(); |
| self.setButtonCount(); |
| |
| self.list.find( "li:not(.ui-li-divider)" ) |
| .removeClass( $.mobile.activeBtnClass ) |
| .attr( "aria-selected", false ) |
| .each(function( i ) { |
| |
| if ( $.inArray( i, indicies ) > -1 ) { |
| var item = $( this ); |
| |
| // Aria selected attr |
| item.attr( "aria-selected", true ); |
| |
| // Multiple selects: add the "on" checkbox state to the icon |
| if ( self.isMultiple ) { |
| item.find( ".ui-icon" ).removeClass( "ui-icon-checkbox-off" ).addClass( "ui-icon-checkbox-on" ); |
| } else { |
| if ( item.is( ".ui-selectmenu-placeholder" ) ) { |
| item.next().addClass( $.mobile.activeBtnClass ); |
| } else { |
| item.addClass( $.mobile.activeBtnClass ); |
| } |
| } |
| } |
| }); |
| }, |
| |
| close: function() { |
| if ( this.options.disabled || !this.isOpen ) { |
| return; |
| } |
| |
| var self = this; |
| |
| if ( self.menuType === "page" ) { |
| // doesn't solve the possible issue with calling change page |
| // where the objects don't define data urls which prevents dialog key |
| // stripping - changePage has incoming refactor |
| $.mobile.back(); |
| } else { |
| self.listbox.popup( "close" ); |
| self.list.appendTo( self.listbox ); |
| self._focusButton(); |
| } |
| |
| // allow the dialog to be closed again |
| self.isOpen = false; |
| }, |
| |
| open: function() { |
| if ( this.options.disabled ) { |
| return; |
| } |
| |
| var self = this, |
| $window = $.mobile.$window, |
| selfListParent = self.list.parent(), |
| menuHeight = selfListParent.outerHeight(), |
| menuWidth = selfListParent.outerWidth(), |
| activePage = $( "." + $.mobile.activePageClass ), |
| scrollTop = $window.scrollTop(), |
| btnOffset = self.button.offset().top, |
| screenHeight = $window.height(), |
| screenWidth = $window.width(); |
| |
| //add active class to button |
| self.button.addClass( $.mobile.activeBtnClass ); |
| |
| //remove after delay |
| setTimeout( function() { |
| self.button.removeClass( $.mobile.activeBtnClass ); |
| }, 300); |
| |
| function focusMenuItem() { |
| var selector = self.list.find( "." + $.mobile.activeBtnClass + " a" ); |
| if ( selector.length === 0 ) { |
| selector = self.list.find( "li.ui-btn:not( :jqmData(placeholder='true') ) a" ); |
| } |
| selector.first().focus().closest( "li" ).addClass( "ui-btn-down-" + widget.options.theme ); |
| } |
| |
| if ( menuHeight > screenHeight - 80 || !$.support.scrollTop ) { |
| |
| self.menuPage.appendTo( $.mobile.pageContainer ).page(); |
| self.menuPageContent = menuPage.find( ".ui-content" ); |
| self.menuPageClose = menuPage.find( ".ui-header a" ); |
| |
| // prevent the parent page from being removed from the DOM, |
| // otherwise the results of selecting a list item in the dialog |
| // fall into a black hole |
| self.thisPage.unbind( "pagehide.remove" ); |
| |
| //for WebOS/Opera Mini (set lastscroll using button offset) |
| if ( scrollTop === 0 && btnOffset > screenHeight ) { |
| self.thisPage.one( "pagehide", function() { |
| $( this ).jqmData( "lastScroll", btnOffset ); |
| }); |
| } |
| |
| self.menuPage.one( "pageshow", function() { |
| focusMenuItem(); |
| self.isOpen = true; |
| }); |
| |
| self.menuType = "page"; |
| self.menuPageContent.append( self.list ); |
| self.menuPage.find("div .ui-title").text(self.label.text()); |
| $.mobile.changePage( self.menuPage, { |
| transition: $.mobile.defaultDialogTransition |
| }); |
| } else { |
| self.menuType = "overlay"; |
| |
| self.listbox |
| .one( "popupafteropen", focusMenuItem ) |
| .popup( "open", { |
| x: self.button.offset().left + self.button.outerWidth() / 2, |
| y: self.button.offset().top + self.button.outerHeight() / 2 |
| }); |
| |
| // duplicate with value set in page show for dialog sized selects |
| self.isOpen = true; |
| } |
| }, |
| |
| _buildList: function() { |
| var self = this, |
| o = this.options, |
| placeholder = this.placeholder, |
| needPlaceholder = true, |
| optgroups = [], |
| lis = [], |
| dataIcon = self.isMultiple ? "checkbox-off" : "false"; |
| |
| self.list.empty().filter( ".ui-listview" ).listview( "destroy" ); |
| |
| var $options = self.select.find( "option" ), |
| numOptions = $options.length, |
| select = this.select[ 0 ], |
| dataPrefix = 'data-' + $.mobile.ns, |
| dataIndexAttr = dataPrefix + 'option-index', |
| dataIconAttr = dataPrefix + 'icon', |
| dataRoleAttr = dataPrefix + 'role', |
| dataPlaceholderAttr = dataPrefix + 'placeholder', |
| fragment = document.createDocumentFragment(), |
| isPlaceholderItem = false, |
| optGroup; |
| |
| for (var i = 0; i < numOptions;i++, isPlaceholderItem = false) { |
| var option = $options[i], |
| $option = $( option ), |
| parent = option.parentNode, |
| text = $option.text(), |
| anchor = document.createElement( 'a' ), |
| classes = []; |
| |
| anchor.setAttribute( 'href', '#' ); |
| anchor.appendChild( document.createTextNode( text ) ); |
| |
| // Are we inside an optgroup? |
| if ( parent !== select && parent.nodeName.toLowerCase() === "optgroup" ) { |
| var optLabel = parent.getAttribute( 'label' ); |
| if ( optLabel !== optGroup ) { |
| var divider = document.createElement( 'li' ); |
| divider.setAttribute( dataRoleAttr, 'list-divider' ); |
| divider.setAttribute( 'role', 'option' ); |
| divider.setAttribute( 'tabindex', '-1' ); |
| divider.appendChild( document.createTextNode( optLabel ) ); |
| fragment.appendChild( divider ); |
| optGroup = optLabel; |
| } |
| } |
| |
| if ( needPlaceholder && ( !option.getAttribute( "value" ) || text.length === 0 || $option.jqmData( "placeholder" ) ) ) { |
| needPlaceholder = false; |
| isPlaceholderItem = true; |
| |
| // If we have identified a placeholder, mark it retroactively in the select as well |
| option.setAttribute( dataPlaceholderAttr, true ); |
| if ( o.hidePlaceholderMenuItems ) { |
| classes.push( "ui-selectmenu-placeholder" ); |
| } |
| if (!placeholder) { |
| placeholder = self.placeholder = text; |
| } |
| } |
| |
| var item = document.createElement('li'); |
| if ( option.disabled ) { |
| classes.push( "ui-disabled" ); |
| item.setAttribute('aria-disabled',true); |
| } |
| item.setAttribute( dataIndexAttr,i ); |
| item.setAttribute( dataIconAttr, dataIcon ); |
| if ( isPlaceholderItem ) { |
| item.setAttribute( dataPlaceholderAttr, true ); |
| } |
| item.className = classes.join( " " ); |
| item.setAttribute( 'role', 'option' ); |
| anchor.setAttribute( 'tabindex', '-1' ); |
| item.appendChild( anchor ); |
| fragment.appendChild( item ); |
| } |
| |
| self.list[0].appendChild( fragment ); |
| |
| // Hide header if it's not a multiselect and there's no placeholder |
| if ( !this.isMultiple && !placeholder.length ) { |
| this.header.hide(); |
| } else { |
| this.headerTitle.text( this.placeholder ); |
| } |
| |
| // Now populated, create listview |
| self.list.listview(); |
| }, |
| |
| _button: function() { |
| return $( "<a>", { |
| "href": "#", |
| "role": "button", |
| // TODO value is undefined at creation |
| "id": this.buttonId, |
| "aria-haspopup": "true", |
| |
| // TODO value is undefined at creation |
| "aria-owns": this.menuId |
| }); |
| } |
| }); |
| }; |
| |
| // issue #3894 - core doesn't trigger events on disabled delegates |
| $.mobile.$document.bind( "selectmenubeforecreate", function( event ) { |
| var selectmenuWidget = $( event.target ).data( "selectmenu" ); |
| |
| if ( !selectmenuWidget.options.nativeMenu && |
| selectmenuWidget.element.parents( ":jqmData(role='popup')" ).length === 0 ) { |
| extendSelect( selectmenuWidget ); |
| } |
| }); |
| })( jQuery ); |
| //>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude); |
| }); |
| //>>excludeEnd("jqmBuildExclude"); |