1 //>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
2 //>>description: Applies listview styling of various types (standard, numbered, split button, etc.)
5 //>>css: ../css/themes/default/jquery.mobile.theme.css, ../css/structure/jquery.mobile.listview.css
7 define( [ "jquery", "./jquery.mobile.widget", "./jquery.mobile.buttonMarkup", "./jquery.mobile.page", "./jquery.mobile.page.sections" ], function( $ ) {
8 //>>excludeEnd("jqmBuildExclude");
9 (function( $, undefined ) {
11 //Keeps track of the number of lists per page UID
12 //This allows support for multiple nested list in the same page
13 //https://github.com/jquery/jquery-mobile/issues/1617
14 var listCountPerPage = {};
16 $.widget( "mobile.listview", $.mobile.widget, {
27 initSelector: ":jqmData(role='listview')"
34 listviewClasses += t.options.inset ? " ui-listview-inset ui-corner-all ui-shadow " : "";
35 listviewClasses += t.element.jqmData( "mini" ) || t.options.mini === true ? " ui-mini" : "";
37 // create listview markup
38 t.element.addClass(function( i, orig ) {
39 return orig + " ui-listview " + listviewClasses;
45 _removeCorners: function( li, which ) {
46 var top = "ui-corner-top ui-corner-tr ui-corner-tl",
47 bot = "ui-corner-bottom ui-corner-br ui-corner-bl";
49 li = li.add( li.find( ".ui-btn-inner, .ui-li-link-alt, .ui-li-thumb" ) );
51 if ( which === "top" ) {
52 li.removeClass( top );
53 } else if ( which === "bottom" ) {
54 li.removeClass( bot );
56 li.removeClass( top + " " + bot );
60 _refreshCorners: function( create ) {
66 if ( this.options.inset ) {
67 $li = this.element.children( "li" );
68 // at create time the li are not visible yet so we need to rely on .ui-screen-hidden
69 $visibleli = create?$li.not( ".ui-screen-hidden" ):$li.filter( ":visible" );
71 this._removeCorners( $li );
73 // Select the first visible li element
74 $topli = $visibleli.first()
75 .addClass( "ui-corner-top" );
77 $topli.add( $topli.find( ".ui-btn-inner" )
78 .not( ".ui-li-link-alt span:first-child" ) )
79 .addClass( "ui-corner-top" )
81 .find( ".ui-li-link-alt, .ui-li-link-alt span:first-child" )
82 .addClass( "ui-corner-tr" )
84 .find( ".ui-li-thumb" )
86 .addClass( "ui-corner-tl" );
88 // Select the last visible li element
89 $bottomli = $visibleli.last()
90 .addClass( "ui-corner-bottom" );
92 $bottomli.add( $bottomli.find( ".ui-btn-inner" ) )
93 .find( ".ui-li-link-alt" )
94 .addClass( "ui-corner-br" )
96 .find( ".ui-li-thumb" )
98 .addClass( "ui-corner-bl" );
101 this.element.trigger( "updatelayout" );
105 // This is a generic utility method for finding the first
106 // node with a given nodeName. It uses basic DOM traversal
107 // to be fast and is meant to be a substitute for simple
108 // $.fn.closest() and $.fn.children() calls on a single
109 // element. Note that callers must pass both the lowerCase
110 // and upperCase version of the nodeName they are looking for.
111 // The main reason for this is that this function will be
112 // called many times and we want to avoid having to lowercase
113 // the nodeName from the element every time to ensure we have
114 // a match. Note that this function lives here for now, but may
115 // be moved into $.mobile if other components need a similar method.
116 _findFirstElementByTagName: function( ele, nextProp, lcName, ucName )
119 dict[ lcName ] = dict[ ucName ] = true;
121 if ( dict[ ele.nodeName ] ) {
124 ele = ele[ nextProp ];
128 _getChildrenByTagName: function( ele, lcName, ucName )
132 dict[ lcName ] = dict[ ucName ] = true;
133 ele = ele.firstChild;
135 if ( dict[ ele.nodeName ] ) {
138 ele = ele.nextSibling;
143 _addThumbClasses: function( containers )
145 var i, img, len = containers.length;
146 for ( i = 0; i < len; i++ ) {
147 img = $( this._findFirstElementByTagName( containers[ i ].firstChild, "nextSibling", "img", "IMG" ) );
149 img.addClass( "ui-li-thumb" );
150 $( this._findFirstElementByTagName( img[ 0 ].parentNode, "parentNode", "li", "LI" ) ).addClass( img.is( ".ui-li-icon" ) ? "ui-li-has-icon" : "ui-li-has-thumb" );
155 refresh: function( create ) {
156 this.parentPage = this.element.closest( ".ui-page" );
157 this._createSubPages();
159 var o = this.options,
160 $list = this.element,
162 dividertheme = $list.jqmData( "dividertheme" ) || o.dividerTheme,
163 listsplittheme = $list.jqmData( "splittheme" ),
164 listspliticon = $list.jqmData( "spliticon" ),
165 li = this._getChildrenByTagName( $list[ 0 ], "li", "LI" ),
166 counter = $.support.cssPseudoElement || !$.nodeName( $list[ 0 ], "ol" ) ? 0 : 1,
168 item, itemClass, itemTheme,
169 a, last, splittheme, countParent, icon, imgParents, img, linkIcon;
172 $list.find( ".ui-li-dec" ).remove();
176 o.theme = $.mobile.getInheritedTheme( this.element, "c" );
179 for ( var pos = 0, numli = li.length; pos < numli; pos++ ) {
183 // If we're creating the element, we update it regardless
184 if ( create || !item.hasClass( "ui-li" ) ) {
185 itemTheme = item.jqmData("theme") || o.theme;
186 a = this._getChildrenByTagName( item[ 0 ], "a", "A" );
189 icon = item.jqmData("icon");
196 icon: a.length > 1 || icon === false ? false : icon || "arrow-r",
200 if ( ( icon != false ) && ( a.length == 1 ) ) {
201 item.addClass( "ui-li-has-arrow" );
204 a.first().removeClass( "ui-link" ).addClass( "ui-link-inherit" );
206 if ( a.length > 1 ) {
207 itemClass += " ui-li-has-alt";
210 splittheme = listsplittheme || last.jqmData( "theme" ) || o.splitTheme;
211 linkIcon = last.jqmData("icon");
214 .attr( "title", last.getEncodedText() )
215 .addClass( "ui-li-link-alt" )
224 .find( ".ui-btn-inner" )
226 $( document.createElement( "span" ) ).buttonMarkup({
231 // link icon overrides list item icon overrides ul element overrides options
232 icon: linkIcon || icon || listspliticon || o.splitIcon
236 } else if ( item.jqmData( "role" ) === "list-divider" ) {
238 itemClass += " ui-li-divider ui-bar-" + dividertheme;
239 item.attr( "role", "heading" );
241 //reset counter when a divider heading is encountered
247 itemClass += " ui-li-static ui-body-" + itemTheme;
251 if ( counter && itemClass.indexOf( "ui-li-divider" ) < 0 ) {
252 countParent = item.is( ".ui-li-static:first" ) ? item : item.find( ".ui-link-inherit" );
254 countParent.addClass( "ui-li-jsnumbering" )
255 .prepend( "<span class='ui-li-dec'>" + (counter++) + ". </span>" );
258 // Instead of setting item class directly on the list item and its
259 // btn-inner at this point in time, push the item into a dictionary
260 // that tells us what class to set on it so we can do this after this
261 // processing loop is finished.
263 if ( !itemClassDict[ itemClass ] ) {
264 itemClassDict[ itemClass ] = [];
267 itemClassDict[ itemClass ].push( item[ 0 ] );
270 // Set the appropriate listview item classes on each list item
271 // and their btn-inner elements. The main reason we didn't do this
272 // in the for-loop above is because we can eliminate per-item function overhead
273 // by calling addClass() and children() once or twice afterwards. This
274 // can give us a significant boost on platforms like WP7.5.
276 for ( itemClass in itemClassDict ) {
277 $( itemClassDict[ itemClass ] ).addClass( itemClass ).children( ".ui-btn-inner" ).addClass( itemClass );
280 $list.find( "h1, h2, h3, h4, h5, h6" ).addClass( "ui-li-heading" )
283 .find( "p, dl" ).addClass( "ui-li-desc" )
286 .find( ".ui-li-aside" ).each(function() {
288 $this.prependTo( $this.parent() ); //shift aside to front for css float
292 .find( ".ui-li-count" ).each( function() {
293 $( this ).closest( "li" ).addClass( "ui-li-has-count" );
294 }).addClass( "ui-btn-up-" + ( $list.jqmData( "counttheme" ) || this.options.countTheme) + " ui-btn-corner-all" );
296 // The idea here is to look at the first image in the list item
297 // itself, and any .ui-link-inherit element it may contain, so we
298 // can place the appropriate classes on the image and list item.
299 // Note that we used to use something like:
301 // li.find(">img:eq(0), .ui-link-inherit>img:eq(0)").each( ... );
303 // But executing a find() like that on Windows Phone 7.5 took a
304 // really long time. Walking things manually with the code below
305 // allows the 400 listview item page to load in about 3 seconds as
306 // opposed to 30 seconds.
308 this._addThumbClasses( li );
309 this._addThumbClasses( $list.find( ".ui-link-inherit" ) );
311 this._refreshCorners( create );
314 //create a string for ID/subpage url creation
315 _idStringEscape: function( str ) {
316 return str.replace(/[^a-zA-Z0-9]/g, '-');
319 _createSubPages: function() {
320 var parentList = this.element,
321 parentPage = parentList.closest( ".ui-page" ),
322 parentUrl = parentPage.jqmData( "url" ),
323 parentId = parentUrl || parentPage[ 0 ][ $.expando ],
324 parentListId = parentList.attr( "id" ),
326 dns = "data-" + $.mobile.ns,
328 persistentFooterID = parentPage.find( ":jqmData(role='footer')" ).jqmData( "id" ),
331 if ( typeof listCountPerPage[ parentId ] === "undefined" ) {
332 listCountPerPage[ parentId ] = -1;
335 parentListId = parentListId || ++listCountPerPage[ parentId ];
337 $( parentList.find( "li>ul, li>ol" ).toArray().reverse() ).each(function( i ) {
340 listId = list.attr( "id" ) || parentListId + "-" + i,
341 parent = list.parent(),
342 nodeEls = $( list.prevAll().toArray().reverse() ),
343 nodeEls = nodeEls.length ? nodeEls : $( "<span>" + $.trim(parent.contents()[ 0 ].nodeValue) + "</span>" ),
344 title = nodeEls.first().getEncodedText(),//url limits to first 30 chars of text
345 id = ( parentUrl || "" ) + "&" + $.mobile.subPageUrlKey + "=" + listId,
346 theme = list.jqmData( "theme" ) || o.theme,
347 countTheme = list.jqmData( "counttheme" ) || parentList.jqmData( "counttheme" ) || o.countTheme,
350 //define hasSubPages for use in later removal
353 newPage = list.detach()
354 .wrap( "<div " + dns + "role='page' " + dns + "url='" + id + "' " + dns + "theme='" + theme + "' " + dns + "count-theme='" + countTheme + "'><div " + dns + "role='content'></div></div>" )
356 .before( "<div " + dns + "role='header' " + dns + "theme='" + o.headerTheme + "'><div class='ui-title'>" + title + "</div></div>" )
357 .after( persistentFooterID ? $( "<div " + dns + "role='footer' " + dns + "id='"+ persistentFooterID +"'>") : "" )
359 .appendTo( $.mobile.pageContainer );
363 anchor = parent.find('a:first');
365 if ( !anchor.length ) {
366 anchor = $( "<a/>" ).html( nodeEls || title ).prependTo( parent.empty() );
369 anchor.attr( "href", "#" + id );
373 // on pagehide, remove any nested pages along with the parent page, as long as they aren't active
374 // and aren't embedded
376 parentPage.is( ":jqmData(external-page='true')" ) &&
377 parentPage.data("page").options.domCache === false ) {
379 var newRemove = function( e, ui ){
380 var nextPage = ui.nextPage, npURL;
383 npURL = nextPage.jqmData( "url" );
384 if( npURL.indexOf( parentUrl + "&" + $.mobile.subPageUrlKey ) !== 0 ){
385 self.childPages().remove();
391 // unbind the original page remove and replace with our specialized version
393 .unbind( "pagehide.remove" )
394 .bind( "pagehide.remove", newRemove);
398 // TODO sort out a better way to track sub pages of the listview this is brittle
399 childPages: function(){
400 var parentUrl = this.parentPage.jqmData( "url" );
402 return $( ":jqmData(url^='"+ parentUrl + "&" + $.mobile.subPageUrlKey +"')");
406 //auto self-init widgets
407 $( document ).bind( "pagecreate create", function( e ){
408 $.mobile.listview.prototype.enhanceWithin( e.target );
412 //>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
414 //>>excludeEnd("jqmBuildExclude");