diff --git a/js/jquery.mobile.forms.select.custom.js b/js/jquery.mobile.forms.select.custom.js index a7c90d57..2c057bd3 100644 --- a/js/jquery.mobile.forms.select.custom.js +++ b/js/jquery.mobile.forms.select.custom.js @@ -25,408 +25,397 @@ $.widget( "mobile.customselect", $.mobile.widget, { initSelector: "select:not(:jqmData(role='slider'))" }, - _common: $.mobile.selectShared, + _shared: $.mobile.selectShared, - _custom: function() { - var widget = this, common = this._common(); + _create: function() { + var widget = this, shared = this._shared(); - return $.extend(common, { + $.extend( widget, shared, { typeName: 'custom', button: $( "", { "href": "#", "role": "button", // TODO value is undefined at creation - "id": common.buttonId, + "id": shared.buttonId, "aria-haspopup": "true", // TODO value is undefined at creation - "aria-owns": common.menuId - }), + "aria-owns": shared.menuId + }) + }); + }, - build: function() { - var self = this, parentWidget = widget; + build: function() { + var self = this, widget = this; - // Create list from select, update state - widget.refresh(); + // Create list from select, update state + widget.refresh(); - self.select.attr( "tabindex", "-1" ) - .focus(function() { - $(this).blur(); - button.focus(); - }); + self.select.attr( "tabindex", "-1" ) + .focus(function() { + $(this).blur(); + 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 ) ) { + // 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 ) ) { - widget.open(); - event.preventDefault(); - } - }); + widget.open(); + event.preventDefault(); + } + }); - // Events for list items - self.list.attr( "role", "listbox" ) - .delegate( ".ui-li>a", "focusin", function() { - $( this ).attr( "tabindex", "0" ); - }) - .delegate( ".ui-li>a", "focusout", function() { - $( this ).attr( "tabindex", "-1" ); - }) - .delegate( "li:not(.ui-disabled, .ui-li-divider)", "vclick", function( event ) { + // Events for list items + self.list.attr( "role", "listbox" ) + .delegate( ".ui-li>a", "focusin", function() { + $( this ).attr( "tabindex", "0" ); + }) + .delegate( ".ui-li>a", "focusout", function() { + $( this ).attr( "tabindex", "-1" ); + }) + .delegate( "li:not(.ui-disabled, .ui-li-divider)", "vclick", 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 = widget.optionElems.eq( newIndex )[ 0 ]; + // 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 = widget.optionElems.eq( newIndex )[ 0 ]; - // toggle selected status on the tag for multi selects - option.selected = self.isMultiple ? !option.selected : true; + // 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 ); - } + // 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" ); - } + // trigger change if value changed + if ( self.isMultiple || oldIndex !== newIndex ) { + self.select.trigger( "change" ); + } - //hide custom select for single selects only - if ( !self.isMultiple ) { - widget.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(); - - // if there's a previous option, focus it - if ( prev.length ) { - target - .blur() - .attr( "tabindex", "-1" ); - - prev.find( "a" ).first().focus(); - } - - return false; - break; - - // down or right arrow keys - case 40: - next = li.next(); - - // if there's a next option, focus it - if ( next.length ) { - target - .blur() - .attr( "tabindex", "-1" ); - - next.find( "a" ).first().focus(); - } - - return false; - break; - - // If enter or space is pressed, trigger click - case 13: - case 32: - target.trigger( "vclick" ); - - return false; - break; - } - }); - - // 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(); - }); - - // Events on "screen" overlay - self.screen.bind( "vclick", function( event ) { + //hide custom select for single selects only + if ( !self.isMultiple ) { widget.close(); - }); - - // Close button on small overlays - self.headerClose.click( function() { - if ( self.menuType == "overlay" ) { - widget.close(); - return false; - } - }); - }, - - refresh: function( forceRebuild ){ - var self = this, - select = this.element, - isMultiple = this.isMultiple, - options = this.optionElems = select.find( "option" ), - selected = this.selected(), - // return an array of all selected index's - indicies = this.selectedIndices(); - - if ( forceRebuild || select[0].options.length != self.list.find( "li" ).length ) { - self._buildList(); } - self.setButtonText(); - self.setButtonCount(); + event.preventDefault(); + }) + .keydown(function( event ) { //keyboard events for menu items + var target = $( event.target ), + li = target.closest( "li" ), + prev, next; - self.list.find( "li:not(.ui-li-divider)" ) - .removeClass( $.mobile.activeBtnClass ) - .attr( "aria-selected", false ) - .each(function( i ) { + // switch logic based on which key was pressed + switch ( event.keyCode ) { + // up or left arrow keys + case 38: + prev = li.prev(); - if ( $.inArray( i, indicies ) > -1 ) { - var item = $( this ).addClass( $.mobile.activeBtnClass ); + // if there's a previous option, focus it + if ( prev.length ) { + target + .blur() + .attr( "tabindex", "-1" ); - // Aria selected attr - item.find( "a" ).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" ); - } - } - }); - }, - - close: function() { - if ( this.options.disabled || !this.isOpen ) { - return; - } - - var self = this; - - if ( self.menuType == "page" ) { - // 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 - self.thisPage.bind( "pagehide.remove", function() { - $(this).remove(); - }); - - // 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 - window.history.back(); - } else { - self.screen.addClass( "ui-screen-hidden" ); - self.listbox.addClass( "ui-selectmenu-hidden" ).removeAttr( "style" ).removeClass( "in" ); - self.list.appendTo( self.listbox ); - self._focusButton(); - } - - // allow the dialog to be closed again - this.isOpen = false; - }, - - open: function() { - if ( this.options.disabled ) { - return; - } - - var self = this, - menuHeight = self.list.parent().outerHeight(), - menuWidth = self.list.parent().outerWidth(), - scrollTop = $( window ).scrollTop(), - btnOffset = self.button.offset().top, - screenHeight = window.innerHeight, - screenWidth = window.innerWidth; - - //add active class to button - self.button.addClass( $.mobile.activeBtnClass ); - - //remove after delay - setTimeout( function() { - self.button.removeClass( $.mobile.activeBtnClass ); - }, 300); - - function focusMenuItem() { - self.list.find( ".ui-btn-active" ).focus(); - } - - if ( menuHeight > screenHeight - 80 || !$.support.scrollTop ) { - // 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 (set lastscroll using button offset) - if ( scrollTop == 0 && btnOffset > screenHeight ) { - self.thisPage.one( "pagehide", function() { - $( this ).jqmData( "lastScroll", btnOffset ); - }); + prev.find( "a" ).first().focus(); } - self.menuPage.one( "pageshow", function() { - // silentScroll() is called whenever a page is shown to restore - // any previous scroll position the page may have had. We need to - // wait for the "silentscroll" event before setting focus to avoid - // the browser"s "feature" which offsets rendering to make sure - // whatever has focus is in view. - $( window ).one( "silentscroll", function() { - focusMenuItem(); - }); + return false; + break; - self.isOpen = true; - }); + // down or right arrow keys + case 40: + next = li.next(); - self.menuType = "page"; - self.menuPageContent.append( self.list ); - $.mobile.changePage( self.menuPage, { - transition: $.mobile.defaultDialogTransition - }); - } else { - self.menuType = "overlay"; + // if there's a next option, focus it + if ( next.length ) { + target + .blur() + .attr( "tabindex", "-1" ); - self.screen.height( $(document).height() ) - .removeClass( "ui-screen-hidden" ); - - // Try and center the overlay over the button - var roomtop = btnOffset - scrollTop, - roombot = scrollTop + screenHeight - btnOffset, - halfheight = menuHeight / 2, - maxwidth = parseFloat( self.list.parent().css( "max-width" ) ), - newtop, newleft; - - if ( roomtop > menuHeight / 2 && roombot > menuHeight / 2 ) { - newtop = btnOffset + ( self.button.outerHeight() / 2 ) - halfheight; - } else { - // 30px tolerance off the edges - newtop = roomtop > roombot ? scrollTop + screenHeight - menuHeight - 30 : scrollTop + 30; + next.find( "a" ).first().focus(); } - // If the menuwidth is smaller than the screen center is - if ( menuWidth < maxwidth ) { - newleft = ( screenWidth - menuWidth ) / 2; - } else { + return false; + break; - //otherwise insure a >= 30px offset from the left - newleft = self.button.offset().left + self.button.outerWidth() / 2 - menuWidth / 2; + // If enter or space is pressed, trigger click + case 13: + case 32: + target.trigger( "vclick" ); - // 30px tolerance off the edges - if ( newleft < 30 ) { - newleft = 30; - } else if ( (newleft + menuWidth) > screenWidth ) { - newleft = screenWidth - menuWidth - 30; - } - } - - self.listbox.append( self.list ) - .removeClass( "ui-selectmenu-hidden" ) - .css({ - top: newtop, - left: newleft - }) - .addClass( "in" ); - - focusMenuItem(); - - // duplicate with value set in page show for dialog sized selects - self.isOpen = true; + return false; + break; } - }, + }); - _buildList: function() { - var self = this, - o = this.options, - placeholder = this.placeholder, - optgroups = [], - lis = [], - dataIcon = self.isMultiple ? "checkbox-off" : "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(); + }); - self.list.empty().filter( ".ui-listview" ).listview( "destroy" ); + // Events on "screen" overlay + self.screen.bind( "vclick", function( event ) { + widget.close(); + }); - // Populate menu with options from select element - self.select.find( "option" ).each( function( i ) { - var $this = $( this ), - $parent = $this.parent(), - text = $this.text(), - anchor = ""+ text +"", - classes = [], - extraAttrs = []; - - // Are we inside an optgroup? - if ( $parent.is( "optgroup" ) ) { - var optLabel = $parent.attr( "label" ); - - // has this optgroup already been built yet? - if ( $.inArray( optLabel, optgroups ) === -1 ) { - lis.push( "
  • "+ optLabel +"
  • " ); - optgroups.push( optLabel ); - } - } - - // Find placeholder text - // TODO: Are you sure you want to use getAttribute? ^RW - if ( !this.getAttribute( "value" ) || text.length == 0 || $this.jqmData( "placeholder" ) ) { - if ( o.hidePlaceholderMenuItems ) { - classes.push( "ui-selectmenu-placeholder" ); - } - placeholder = self.placeholder = text; - } - - // support disabled option tags - if ( this.disabled ) { - classes.push( "ui-disabled" ); - extraAttrs.push( "aria-disabled='true'" ); - } - - lis.push( "
  • "+ anchor +"
  • " ); - }); - - self.list.html( lis.join(" ") ); - - self.list.find( "li" ) - .attr({ "role": "option", "tabindex": "-1" }) - .first().attr( "tabindex", "0" ); - - // Hide header close link for single selects - if ( !this.isMultiple ) { - this.headerClose.hide(); - } - - // 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(); + // Close button on small overlays + self.headerClose.click( function() { + if ( self.menuType == "overlay" ) { + widget.close(); + return false; } }); }, - _create: function() { + refresh: function( forceRebuild ){ var self = this, + select = this.element, + isMultiple = this.isMultiple, + options = this.optionElems = select.find( "option" ), + selected = this.selected(), + // return an array of all selected index's + indicies = this.selectedIndices(); - o = this.options, + if ( forceRebuild || select[0].options.length != self.list.find( "li" ).length ) { + self._buildList(); + } - menu = this._custom(); + self.setButtonText(); + self.setButtonCount(); - // Expose to other methods - $.extend( self, menu ); + 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 ).addClass( $.mobile.activeBtnClass ); + + // Aria selected attr + item.find( "a" ).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" ); + } + } + }); + }, + + close: function() { + if ( this.options.disabled || !this.isOpen ) { + return; + } + + var self = this; + + if ( self.menuType == "page" ) { + // 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 + self.thisPage.bind( "pagehide.remove", function() { + $(this).remove(); + }); + + // 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 + window.history.back(); + } else { + self.screen.addClass( "ui-screen-hidden" ); + self.listbox.addClass( "ui-selectmenu-hidden" ).removeAttr( "style" ).removeClass( "in" ); + self.list.appendTo( self.listbox ); + self._focusButton(); + } + + // allow the dialog to be closed again + this.isOpen = false; + }, + + open: function() { + if ( this.options.disabled ) { + return; + } + + var self = this, + menuHeight = self.list.parent().outerHeight(), + menuWidth = self.list.parent().outerWidth(), + scrollTop = $( window ).scrollTop(), + btnOffset = self.button.offset().top, + screenHeight = window.innerHeight, + screenWidth = window.innerWidth; + + //add active class to button + self.button.addClass( $.mobile.activeBtnClass ); + + //remove after delay + setTimeout( function() { + self.button.removeClass( $.mobile.activeBtnClass ); + }, 300); + + function focusMenuItem() { + self.list.find( ".ui-btn-active" ).focus(); + } + + if ( menuHeight > screenHeight - 80 || !$.support.scrollTop ) { + // 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 (set lastscroll using button offset) + if ( scrollTop == 0 && btnOffset > screenHeight ) { + self.thisPage.one( "pagehide", function() { + $( this ).jqmData( "lastScroll", btnOffset ); + }); + } + + self.menuPage.one( "pageshow", function() { + // silentScroll() is called whenever a page is shown to restore + // any previous scroll position the page may have had. We need to + // wait for the "silentscroll" event before setting focus to avoid + // the browser"s "feature" which offsets rendering to make sure + // whatever has focus is in view. + $( window ).one( "silentscroll", function() { + focusMenuItem(); + }); + + self.isOpen = true; + }); + + self.menuType = "page"; + self.menuPageContent.append( self.list ); + $.mobile.changePage( self.menuPage, { + transition: $.mobile.defaultDialogTransition + }); + } else { + self.menuType = "overlay"; + + self.screen.height( $(document).height() ) + .removeClass( "ui-screen-hidden" ); + + // Try and center the overlay over the button + var roomtop = btnOffset - scrollTop, + roombot = scrollTop + screenHeight - btnOffset, + halfheight = menuHeight / 2, + maxwidth = parseFloat( self.list.parent().css( "max-width" ) ), + newtop, newleft; + + if ( roomtop > menuHeight / 2 && roombot > menuHeight / 2 ) { + newtop = btnOffset + ( self.button.outerHeight() / 2 ) - halfheight; + } else { + // 30px tolerance off the edges + newtop = roomtop > roombot ? scrollTop + screenHeight - menuHeight - 30 : scrollTop + 30; + } + + // If the menuwidth is smaller than the screen center is + if ( menuWidth < maxwidth ) { + newleft = ( screenWidth - menuWidth ) / 2; + } else { + + //otherwise insure a >= 30px offset from the left + newleft = self.button.offset().left + self.button.outerWidth() / 2 - menuWidth / 2; + + // 30px tolerance off the edges + if ( newleft < 30 ) { + newleft = 30; + } else if ( (newleft + menuWidth) > screenWidth ) { + newleft = screenWidth - menuWidth - 30; + } + } + + self.listbox.append( self.list ) + .removeClass( "ui-selectmenu-hidden" ) + .css({ + top: newtop, + left: newleft + }) + .addClass( "in" ); + + focusMenuItem(); + + // 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, + optgroups = [], + lis = [], + dataIcon = self.isMultiple ? "checkbox-off" : "false"; + + self.list.empty().filter( ".ui-listview" ).listview( "destroy" ); + + // Populate menu with options from select element + self.select.find( "option" ).each( function( i ) { + var $this = $( this ), + $parent = $this.parent(), + text = $this.text(), + anchor = ""+ text +"", + classes = [], + extraAttrs = []; + + // Are we inside an optgroup? + if ( $parent.is( "optgroup" ) ) { + var optLabel = $parent.attr( "label" ); + + // has this optgroup already been built yet? + if ( $.inArray( optLabel, optgroups ) === -1 ) { + lis.push( "
  • "+ optLabel +"
  • " ); + optgroups.push( optLabel ); + } + } + + // Find placeholder text + // TODO: Are you sure you want to use getAttribute? ^RW + if ( !this.getAttribute( "value" ) || text.length == 0 || $this.jqmData( "placeholder" ) ) { + if ( o.hidePlaceholderMenuItems ) { + classes.push( "ui-selectmenu-placeholder" ); + } + placeholder = self.placeholder = text; + } + + // support disabled option tags + if ( this.disabled ) { + classes.push( "ui-disabled" ); + extraAttrs.push( "aria-disabled='true'" ); + } + + lis.push( "
  • "+ anchor +"
  • " ); + }); + + self.list.html( lis.join(" ") ); + + self.list.find( "li" ) + .attr({ "role": "option", "tabindex": "-1" }) + .first().attr( "tabindex", "0" ); + + // Hide header close link for single selects + if ( !this.isMultiple ) { + this.headerClose.hide(); + } + + // 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(); } }); })( jQuery ); diff --git a/js/jquery.mobile.forms.select.js b/js/jquery.mobile.forms.select.js index bf2ae838..37ccfc91 100644 --- a/js/jquery.mobile.forms.select.js +++ b/js/jquery.mobile.forms.select.js @@ -98,5 +98,4 @@ $( document ).bind( "pagecreate create", function( e ){ .not( ":jqmData(role='none'), :jqmData(role='nojs')" ) .selectmenu(); }); - })( jQuery ); diff --git a/js/jquery.mobile.forms.select.native.js b/js/jquery.mobile.forms.select.native.js index 2e901a0e..180ec11e 100644 --- a/js/jquery.mobile.forms.select.native.js +++ b/js/jquery.mobile.forms.select.native.js @@ -27,58 +27,47 @@ $.widget( "mobile.nativeselect", $.mobile.widget, { _shared: $.mobile.selectShared, - _native: function() { + _create: function() { var widget = this; - return $.extend( this._shared(), { + $.extend( widget, widget._shared(), { typgeName: 'native', - button: $( "
    " ), - - build: function() { - var self = this; - - this.select - .appendTo( self.button ) - .bind( "vmousedown", function() { - // Add active class to button - self.button.addClass( $.mobile.activeBtnClass ); - }) - .bind( "focus vmouseover", function() { - self.button.trigger( "vmouseover" ); - }) - .bind( "vmousemove", function() { - // Remove active class on scroll/touchmove - self.button.removeClass( $.mobile.activeBtnClass ); - }) - .bind( "change blur vmouseout", function() { - self.button.trigger( "vmouseout" ) - .removeClass( $.mobile.activeBtnClass ); - }) - .bind( "change blur", function() { - self.button.removeClass( "ui-btn-down-" + widget.options.theme ); - }); - }, - - refresh: function() { - var self = this, - selected = this.selected(); - - self.setButtonText(); - self.setButtonCount(); - } + button: $( "
    " ) }); }, - _create: function() { + build: function() { + var self = this; + + this.select + .appendTo( self.button ) + .bind( "vmousedown", function() { + // Add active class to button + self.button.addClass( $.mobile.activeBtnClass ); + }) + .bind( "focus vmouseover", function() { + self.button.trigger( "vmouseover" ); + }) + .bind( "vmousemove", function() { + // Remove active class on scroll/touchmove + self.button.removeClass( $.mobile.activeBtnClass ); + }) + .bind( "change blur vmouseout", function() { + self.button.trigger( "vmouseout" ) + .removeClass( $.mobile.activeBtnClass ); + }) + .bind( "change blur", function() { + self.button.removeClass( "ui-btn-down-" + self.options.theme ); + }); + }, + + refresh: function() { var self = this, + selected = this.selected(); - o = this.options, - - menu = this._native(); - - // Expose to other methods - $.extend( self, menu ); + self.setButtonText(); + self.setButtonCount(); } }); })( jQuery );