blob: 9a56d4b5a5ec5efae4940eabc5e0384d033886a3 [file] [log] [blame]
//>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
//>>description: Applies listview styling of various types (standard, numbered, split button, etc.)
//>>label: Listview
//>>group: Widgets
//>>css.structure: ../css/structure/jquery.mobile.listview.css
//>>css.theme: ../css/themes/default/jquery.mobile.theme.css
define( [ "jquery", "../jquery.mobile.widget", "../jquery.mobile.buttonMarkup", "./page", "./page.sections" ], function( jQuery ) {
//>>excludeEnd("jqmBuildExclude");
(function( $, undefined ) {
//Keeps track of the number of lists per page UID
//This allows support for multiple nested list in the same page
//https://github.com/jquery/jquery-mobile/issues/1617
var listCountPerPage = {};
$.widget( "mobile.listview", $.mobile.widget, {
options: {
theme: null,
countTheme: "c",
headerTheme: "b",
dividerTheme: "b",
splitIcon: "arrow-r",
splitTheme: "b",
inset: false,
initSelector: ":jqmData(role='listview')"
},
_create: function() {
var t = this,
listviewClasses = "";
listviewClasses += t.options.inset ? " ui-listview-inset ui-corner-all ui-shadow " : "";
// create listview markup
t.element.addClass(function( i, orig ) {
return orig + " ui-listview " + listviewClasses;
});
t.refresh( true );
},
_removeCorners: function( li, which ) {
var top = "ui-corner-top ui-corner-tr ui-corner-tl",
bot = "ui-corner-bottom ui-corner-br ui-corner-bl";
li = li.add( li.find( ".ui-btn-inner, .ui-li-link-alt, .ui-li-thumb" ) );
if ( which === "top" ) {
li.removeClass( top );
} else if ( which === "bottom" ) {
li.removeClass( bot );
} else {
li.removeClass( top + " " + bot );
}
},
_refreshCorners: function( create ) {
var $li,
$visibleli,
$topli,
$bottomli;
$li = this.element.children( "li" );
// At create time and when autodividers calls refresh the li are not visible yet so we need to rely on .ui-screen-hidden
$visibleli = create || $li.filter( ":visible" ).length === 0 ? $li.not( ".ui-screen-hidden" ) : $li.filter( ":visible" );
// ui-li-last is used for setting border-bottom on the last li
$li.filter( ".ui-li-last" ).removeClass( "ui-li-last" );
if ( this.options.inset ) {
this._removeCorners( $li );
// Select the first visible li element
$topli = $visibleli.first()
.addClass( "ui-corner-top" );
$topli.add( $topli.find( ".ui-btn-inner" )
.not( ".ui-li-link-alt span:first-child" ) )
.addClass( "ui-corner-top" )
.end()
.find( ".ui-li-link-alt, .ui-li-link-alt span:first-child" )
.addClass( "ui-corner-tr" )
.end()
.find( ".ui-li-thumb" )
.not( ".ui-li-icon" )
.addClass( "ui-corner-tl" );
// Select the last visible li element
$bottomli = $visibleli.last()
.addClass( "ui-corner-bottom ui-li-last" );
$bottomli.add( $bottomli.find( ".ui-btn-inner" ) )
.find( ".ui-li-link-alt" )
.addClass( "ui-corner-br" )
.end()
.find( ".ui-li-thumb" )
.not( ".ui-li-icon" )
.addClass( "ui-corner-bl" );
} else {
$visibleli.last().addClass( "ui-li-last" );
}
if ( !create ) {
this.element.trigger( "updatelayout" );
}
},
// This is a generic utility method for finding the first
// node with a given nodeName. It uses basic DOM traversal
// to be fast and is meant to be a substitute for simple
// $.fn.closest() and $.fn.children() calls on a single
// element. Note that callers must pass both the lowerCase
// and upperCase version of the nodeName they are looking for.
// The main reason for this is that this function will be
// called many times and we want to avoid having to lowercase
// the nodeName from the element every time to ensure we have
// a match. Note that this function lives here for now, but may
// be moved into $.mobile if other components need a similar method.
_findFirstElementByTagName: function( ele, nextProp, lcName, ucName ) {
var dict = {};
dict[ lcName ] = dict[ ucName ] = true;
while ( ele ) {
if ( dict[ ele.nodeName ] ) {
return ele;
}
ele = ele[ nextProp ];
}
return null;
},
_getChildrenByTagName: function( ele, lcName, ucName ) {
var results = [],
dict = {};
dict[ lcName ] = dict[ ucName ] = true;
ele = ele.firstChild;
while ( ele ) {
if ( dict[ ele.nodeName ] ) {
results.push( ele );
}
ele = ele.nextSibling;
}
return $( results );
},
_addThumbClasses: function( containers ) {
var i, img, len = containers.length;
for ( i = 0; i < len; i++ ) {
img = $( this._findFirstElementByTagName( containers[ i ].firstChild, "nextSibling", "img", "IMG" ) );
if ( img.length ) {
img.addClass( "ui-li-thumb" ).attr( {
"role" : "",
"aria-label" : "icon"
});
$( this._findFirstElementByTagName( img[ 0 ].parentNode, "parentNode", "li", "LI" ) ).addClass( img.is( ".ui-li-icon" ) ? "ui-li-has-icon" : "ui-li-has-thumb" );
}
}
},
_addCheckboxRadioClasses: function( containers )
{
var i, inputAttr, len = containers.length;
for ( i = 0; i < len; i++ ) {
inputAttr = $( containers[ i ] ).find( "input" );
if ( inputAttr.attr( "type" ) == "checkbox" ) {
$( containers[ i ] ).addClass( "ui-li-has-checkbox" );
} else if ( inputAttr.attr( "type" ) == "radio" ) {
$( containers[ i ] ).addClass( "ui-li-has-radio" );
}
}
},
_addRightBtnClasses: function( containers )
{
var i, btnAttr, len = containers.length;
for ( i = 0; i < len; i++ ) {
btnAttr = $( containers[ i ] ).find( ":jqmData(role='button'),input[type='button'],select:jqmData(role='slider')" );
if ( btnAttr.length ) {
if ( btnAttr.jqmData( "style" ) == "circle" ) {
$( containers[ i ] ).addClass( "ui-li-has-right-circle-btn" );
} else {
$( containers[ i ] ).addClass( "ui-li-has-right-btn" );
}
}
}
},
refresh: function( create ) {
this.parentPage = this.element.closest( ".ui-page" );
this._createSubPages();
var o = this.options,
$list = this.element,
self = this,
dividertheme = $.mobile.getAttrFixed( $list[0], "data-" + $.mobile.ns + "dividertheme" ) || o.dividerTheme,
listsplittheme = $.mobile.getAttrFixed( $list[0], "data-" + $.mobile.ns + "splittheme" ),
listspliticon = $.mobile.getAttrFixed( $list[0], "data-" + $.mobile.ns + "spliticon" ),
li = this._getChildrenByTagName( $list[ 0 ], "li", "LI" ),
ol = !!$.nodeName( $list[ 0 ], "ol" ),
jsCount = !$.support.cssPseudoElement,
start = $list.attr( "start" ),
itemClassDict = {},
item, itemClass, itemTheme,
a, last, splittheme, counter, startCount, newStartCount, countParent, icon, imgParents, img, linkIcon;
if ( ol && jsCount ) {
$list.find( ".ui-li-dec" ).remove();
}
if ( ol ) {
// Check if a start attribute has been set while taking a value of 0 into account
if ( start || start === 0 ) {
if ( !jsCount ) {
startCount = parseFloat( start ) - 1;
$list.css( "counter-reset", "listnumbering " + startCount );
} else {
counter = parseFloat( start );
}
} else if ( jsCount ) {
counter = 1;
}
}
if ( !o.theme ) {
o.theme = $.mobile.getInheritedTheme( this.element, "c" );
}
for ( var pos = 0, numli = li.length; pos < numli; pos++ ) {
item = li.eq( pos );
itemClass = "ui-li";
// If we're creating the element, we update it regardless
if ( create || !item.hasClass( "ui-li" ) ) {
itemTheme = $.mobile.getAttrFixed( item[0], "data-" + $.mobile.ns + "theme" ) || o.theme;
a = this._getChildrenByTagName( item[ 0 ], "a", "A" ).attr( {
"role": "",
"tabindex": "0"
});
var isDivider = ( $.mobile.getAttrFixed( item[0], "data-" + $.mobile.ns + "role" ) === "list-divider" );
if ( item.hasClass( "ui-li-has-checkbox" ) || item.hasClass( "ui-li-has-radio" ) ) {
item.on( "vclick", function ( e ) {
var targetItem = $( e.target );
var checkboxradio = targetItem.find( ".ui-checkbox" );
if ( !checkboxradio.length ) {
checkboxradio = targetItem.find( ".ui-radio" );
}
if ( checkboxradio.length ) {
checkboxradio.children( "label" ).trigger( "vclick" );
}
});
}
if ( a.length && !isDivider ) {
icon = $.mobile.getAttrFixed( item[0], "data-" + $.mobile.ns + "icon" );
/* Remove auto populated right-arrow button. */
if ( icon === undefined ) {
icon = false;
}
item.buttonMarkup({
wrapperEls: "div",
shadow: false,
corners: false,
iconpos: "right",
icon: a.length > 1 || icon === false ? false : icon || "arrow-r",
theme: itemTheme
});
if ( ( icon !== false ) && ( a.length === 1 ) ) {
item.addClass( "ui-li-has-arrow" );
}
a.first().removeClass( "ui-link" ).addClass( "ui-link-inherit" );
if ( a.length > 1 ) {
itemClass += " ui-li-has-alt";
last = a.last();
splittheme = listsplittheme || $.mobile.getAttrFixed( last[0], "data-" + $.mobile.ns + "theme" ) || o.splitTheme;
linkIcon = $.mobile.getAttrFixed( last[0], "data-" + $.mobile.ns + "icon" );
last.appendTo( item )
.attr( "title", last.getEncodedText() )
.addClass( "ui-li-link-alt" )
.empty()
.buttonMarkup({
shadow: false,
corners: false,
theme: itemTheme,
icon: false,
iconpos: "notext"
})
.find( ".ui-btn-inner" )
.append(
$( document.createElement( "span" ) ).buttonMarkup({
shadow: true,
corners: true,
theme: splittheme,
iconpos: "notext",
// link icon overrides list item icon overrides ul element overrides options
icon: linkIcon || icon || listspliticon || o.splitIcon
})
);
}
} else if ( isDivider ) {
itemClass += " ui-li-divider ui-bar-" + dividertheme;
item.attr( { "role": "heading", "tabindex": "0" } );
if ( ol ) {
//reset counter when a divider heading is encountered
if ( start || start === 0 ) {
if ( !jsCount ) {
newStartCount = parseFloat( start ) - 1;
item.css( "counter-reset", "listnumbering " + newStartCount );
} else {
counter = parseFloat( start );
}
} else if ( jsCount ) {
counter = 1;
}
}
} else {
itemClass += " ui-li-static ui-btn-up-" + itemTheme;
item.attr( "tabindex", "0" );
}
}
if ( ol && jsCount && itemClass.indexOf( "ui-li-divider" ) < 0 ) {
countParent = itemClass.indexOf( "ui-li-static" ) > 0 ? item : item.find( ".ui-link-inherit" );
countParent.addClass( "ui-li-jsnumbering" )
.prepend( "<span class='ui-li-dec'>" + ( counter++ ) + ". </span>" );
}
// Instead of setting item class directly on the list item and its
// btn-inner at this point in time, push the item into a dictionary
// that tells us what class to set on it so we can do this after this
// processing loop is finished.
if ( !itemClassDict[ itemClass ] ) {
itemClassDict[ itemClass ] = [];
}
itemClassDict[ itemClass ].push( item[ 0 ] );
}
// Set the appropriate listview item classes on each list item
// and their btn-inner elements. The main reason we didn't do this
// in the for-loop above is because we can eliminate per-item function overhead
// by calling addClass() and children() once or twice afterwards. This
// can give us a significant boost on platforms like WP7.5.
for ( itemClass in itemClassDict ) {
$( itemClassDict[ itemClass ] ).addClass( itemClass ).children( ".ui-btn-inner" ).addClass( itemClass );
}
$list.find( "h1, h2, h3, h4, h5, h6" ).addClass( "ui-li-heading" )
.end()
.find( "p, dl" ).addClass( "ui-li-desc" )
.end()
.find( ".ui-li-aside" ).each(function() {
var $this = $( this );
$this.prependTo( $this.parent() ); //shift aside to front for css float
})
.end()
.find( ".ui-li-count" ).each(function() {
$( this ).closest( "li" ).addClass( "ui-li-has-count" );
}).addClass( "ui-btn-up-" + ( $.mobile.getAttrFixed( $list[0], "data-" + $.mobile.ns + "counttheme" ) || this.options.countTheme) + " ui-btn-corner-all" );
// The idea here is to look at the first image in the list item
// itself, and any .ui-link-inherit element it may contain, so we
// can place the appropriate classes on the image and list item.
// Note that we used to use something like:
//
// li.find(">img:eq(0), .ui-link-inherit>img:eq(0)").each( ... );
//
// But executing a find() like that on Windows Phone 7.5 took a
// really long time. Walking things manually with the code below
// allows the 400 listview item page to load in about 3 seconds as
// opposed to 30 seconds.
this._addThumbClasses( li );
this._addThumbClasses( $list.find( ".ui-link-inherit" ) );
this._addCheckboxRadioClasses( li );
this._addCheckboxRadioClasses( $list.find( ".ui-link-inherit" ) );
this._addRightBtnClasses( li );
this._addRightBtnClasses( $list.find( ".ui-link-inherit" ) );
this._refreshCorners( create );
// autodividers binds to this to redraw dividers after the listview refresh
this._trigger( "afterrefresh" );
},
//create a string for ID/subpage url creation
_idStringEscape: function( str ) {
return str.replace(/[^a-zA-Z0-9]/g, '-');
},
_createSubPages: function() {
var parentList = this.element,
parentPage = parentList.closest( ".ui-page" ),
parentUrl = parentPage.jqmData( "url" ),
parentId = parentUrl || parentPage[ 0 ][ $.expando ],
parentListId = parentList.attr( "id" ),
o = this.options,
dns = "data-" + $.mobile.ns,
self = this,
persistentFooterID = parentPage.find( ":jqmData(role='footer')" ).jqmData( "id" ),
hasSubPages;
if ( typeof listCountPerPage[ parentId ] === "undefined" ) {
listCountPerPage[ parentId ] = -1;
}
parentListId = parentListId || ++listCountPerPage[ parentId ];
$( parentList.find( "li>ul, li>ol" ).toArray().reverse() ).each(function( i ) {
var self = this,
list = $( this ),
listId = list.attr( "id" ) || parentListId + "-" + i,
parent = list.parent(),
nodeElsFull = $( list.prevAll().toArray().reverse() ),
nodeEls = nodeElsFull.length ? nodeElsFull : $( "<span>" + $.trim(parent.contents()[ 0 ].nodeValue) + "</span>" ),
title = nodeEls.first().getEncodedText(),//url limits to first 30 chars of text
id = ( parentUrl || "" ) + "&" + $.mobile.subPageUrlKey + "=" + listId,
theme = $.mobile.getAttrFixed( list[0], "data-" + $.mobile.ns + "theme" ) || o.theme,
countTheme = $.mobile.getAttrFixed( list[0], "data-" + $.mobile.ns + "counttheme" ) || $.mobile.getAttrFixed( parentList[0], "data-" + $.mobile.ns + "counttheme" ) || o.countTheme,
newPage, anchor;
//define hasSubPages for use in later removal
hasSubPages = true;
newPage = list.detach()
.wrap( "<div " + dns + "role='page' " + dns + "url='" + id + "' " + dns + "theme='" + theme + "' " + dns + "count-theme='" + countTheme + "'><div " + dns + "role='content'></div></div>" )
.parent()
.before( "<div " + dns + "role='header' " + dns + "theme='" + o.headerTheme + "'><div class='ui-title'>" + title + "</div></div>" )
.after( persistentFooterID ? $( "<div " + dns + "role='footer' " + dns + "id='"+ persistentFooterID +"'>" ) : "" )
.parent()
.appendTo( $.mobile.pageContainer );
newPage.page();
anchor = parent.find( 'a:first' );
if ( !anchor.length ) {
anchor = $( "<a/>" ).html( nodeEls || title ).prependTo( parent.empty() );
}
anchor.attr( "href", "#" + id );
}).listview();
// on pagehide, remove any nested pages along with the parent page, as long as they aren't active
// and aren't embedded
if ( hasSubPages &&
parentPage.is( ":jqmData(external-page='true')" ) &&
parentPage.data( "page" ).options.domCache === false ) {
var newRemove = function( e, ui ) {
var nextPage = ui.nextPage, npURL,
prEvent = new $.Event( "pageremove" );
if ( ui.nextPage ) {
npURL = nextPage.jqmData( "url" );
if ( npURL.indexOf( parentUrl + "&" + $.mobile.subPageUrlKey ) !== 0 ) {
self.childPages().remove();
parentPage.trigger( prEvent );
if ( !prEvent.isDefaultPrevented() ) {
parentPage.removeWithDependents();
}
}
}
};
// unbind the original page remove and replace with our specialized version
parentPage
.unbind( "pagehide.remove" )
.bind( "pagehide.remove", newRemove);
}
},
addItem : function( listitem , idx ) {
var $item = $(listitem),
$li,
_self = this;
$li = _self.element.children( 'li' );
$item.css( { 'opacity' : 0,
'display' : 'none' } );
if( $li.length == 0
|| $li.length <= idx)
{
$( _self.element ).append( $item );
} else {
$( $li.get( idx ) ).before( $item );
}
$(_self.element).trigger("create")
.listview( 'refresh' );
$item.css( 'min-height' , '0px' );
$item.slideDown( 'fast' , function( ){
$item.addClass("addli");
$item.css( { 'opacity' : 1 } );
} );
},
removeItem : function( idx ) {
var $item,
$li,
_self = this;
$li = _self.element.children( 'li' );
if( $li.length <= 0 ||
$li.length < idx ) {
return ;
}
$item = $( $li.get( idx ) );
$item.addClass("removeli");
$item.slideUp('normal',
function( ) {
$(this).remove();
});
},
// TODO sort out a better way to track sub pages of the listview this is brittle
childPages: function() {
var parentUrl = this.parentPage.jqmData( "url" );
return $( ":jqmData(url^='"+ parentUrl + "&" + $.mobile.subPageUrlKey + "')" );
}
});
//delegate auto self-init widgets
$.delegateSelfInitWithSingleSelector( $.mobile.listview );
})( jQuery );
//>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
});
//>>excludeEnd("jqmBuildExclude");