1 //>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
2 //>>description: Applies listview styling of various types (standard, numbered, split button, etc.)
5 //>>css.structure: ../css/structure/jquery.mobile.listview.css
6 //>>css.theme: ../css/themes/default/jquery.mobile.theme.css
8 define( [ "jquery", "../jquery.mobile.widget", "../jquery.mobile.buttonMarkup", "./page", "./page.sections" ], function( $ ) {
9 //>>excludeEnd("jqmBuildExclude");
10 (function( $, undefined ) {
12 //Keeps track of the number of lists per page UID
13 //This allows support for multiple nested list in the same page
14 //https://github.com/jquery/jquery-mobile/issues/1617
15 var listCountPerPage = {};
17 $.widget( "mobile.listview", $.mobile.widget, {
27 initSelector: ":jqmData(role='listview')"
34 listviewClasses += t.options.inset ? " ui-listview-inset ui-corner-all ui-shadow " : "";
36 // create listview markup
37 t.element.addClass(function( i, orig ) {
38 return orig + " ui-listview " + listviewClasses;
44 _removeCorners: function( li, which ) {
45 var top = "ui-corner-top ui-corner-tr ui-corner-tl",
46 bot = "ui-corner-bottom ui-corner-br ui-corner-bl";
48 li = li.add( li.find( ".ui-btn-inner, .ui-li-link-alt, .ui-li-thumb" ) );
50 if ( which === "top" ) {
51 li.removeClass( top );
52 } else if ( which === "bottom" ) {
53 li.removeClass( bot );
55 li.removeClass( top + " " + bot );
59 _refreshCorners: function( create ) {
65 $li = this.element.children( "li" );
66 // At create time and when autodividers calls refresh the li are not visible yet so we need to rely on .ui-screen-hidden
67 $visibleli = create || $li.filter( ":visible" ).length === 0 ? $li.not( ".ui-screen-hidden" ) : $li.filter( ":visible" );
69 // ui-li-last is used for setting border-bottom on the last li
70 $li.filter( ".ui-li-last" ).removeClass( "ui-li-last" );
72 if ( this.options.inset ) {
73 this._removeCorners( $li );
75 // Select the first visible li element
76 $topli = $visibleli.first()
77 .addClass( "ui-corner-top" );
79 $topli.add( $topli.find( ".ui-btn-inner" )
80 .not( ".ui-li-link-alt span:first-child" ) )
81 .addClass( "ui-corner-top" )
83 .find( ".ui-li-link-alt, .ui-li-link-alt span:first-child" )
84 .addClass( "ui-corner-tr" )
86 .find( ".ui-li-thumb" )
88 .addClass( "ui-corner-tl" );
90 // Select the last visible li element
91 $bottomli = $visibleli.last()
92 .addClass( "ui-corner-bottom ui-li-last" );
94 $bottomli.add( $bottomli.find( ".ui-btn-inner" ) )
95 .find( ".ui-li-link-alt" )
96 .addClass( "ui-corner-br" )
98 .find( ".ui-li-thumb" )
100 .addClass( "ui-corner-bl" );
102 $visibleli.last().addClass( "ui-li-last" );
105 this.element.trigger( "updatelayout" );
109 // This is a generic utility method for finding the first
110 // node with a given nodeName. It uses basic DOM traversal
111 // to be fast and is meant to be a substitute for simple
112 // $.fn.closest() and $.fn.children() calls on a single
113 // element. Note that callers must pass both the lowerCase
114 // and upperCase version of the nodeName they are looking for.
115 // The main reason for this is that this function will be
116 // called many times and we want to avoid having to lowercase
117 // the nodeName from the element every time to ensure we have
118 // a match. Note that this function lives here for now, but may
119 // be moved into $.mobile if other components need a similar method.
120 _findFirstElementByTagName: function( ele, nextProp, lcName, ucName ) {
122 dict[ lcName ] = dict[ ucName ] = true;
124 if ( dict[ ele.nodeName ] ) {
127 ele = ele[ nextProp ];
131 _getChildrenByTagName: function( ele, lcName, ucName ) {
134 dict[ lcName ] = dict[ ucName ] = true;
135 ele = ele.firstChild;
137 if ( dict[ ele.nodeName ] ) {
140 ele = ele.nextSibling;
145 _addThumbClasses: function( containers ) {
146 var i, img, len = containers.length;
147 for ( i = 0; i < len; i++ ) {
148 img = $( this._findFirstElementByTagName( containers[ i ].firstChild, "nextSibling", "img", "IMG" ) );
150 img.addClass( "ui-li-thumb" );
151 $( this._findFirstElementByTagName( img[ 0 ].parentNode, "parentNode", "li", "LI" ) ).addClass( img.is( ".ui-li-icon" ) ? "ui-li-has-icon" : "ui-li-has-thumb" );
156 refresh: function( create ) {
157 this.parentPage = this.element.closest( ".ui-page" );
158 this._createSubPages();
160 var o = this.options,
161 $list = this.element,
163 dividertheme = $list.jqmData( "dividertheme" ) || o.dividerTheme,
164 listsplittheme = $list.jqmData( "splittheme" ),
165 listspliticon = $list.jqmData( "spliticon" ),
166 li = this._getChildrenByTagName( $list[ 0 ], "li", "LI" ),
167 ol = !!$.nodeName( $list[ 0 ], "ol" ),
168 jsCount = !$.support.cssPseudoElement,
169 start = $list.attr( "start" ),
171 item, itemClass, itemTheme,
172 a, last, splittheme, counter, startCount, newStartCount, countParent, icon, imgParents, img, linkIcon;
174 if ( ol && jsCount ) {
175 $list.find( ".ui-li-dec" ).remove();
179 // Check if a start attribute has been set while taking a value of 0 into account
180 if ( start || start === 0 ) {
182 startCount = parseFloat( start ) - 1;
183 $list.css( "counter-reset", "listnumbering " + startCount );
185 counter = parseFloat( start );
187 } else if ( jsCount ) {
193 o.theme = $.mobile.getInheritedTheme( this.element, "c" );
196 for ( var pos = 0, numli = li.length; pos < numli; pos++ ) {
200 // If we're creating the element, we update it regardless
201 if ( create || !item.hasClass( "ui-li" ) ) {
202 itemTheme = item.jqmData( "theme" ) || o.theme;
203 a = this._getChildrenByTagName( item[ 0 ], "a", "A" );
204 var isDivider = ( item.jqmData( "role" ) === "list-divider" );
206 if ( a.length && !isDivider ) {
207 icon = item.jqmData( "icon" );
214 icon: a.length > 1 || icon === false ? false : icon || "arrow-r",
218 if ( ( icon !== false ) && ( a.length === 1 ) ) {
219 item.addClass( "ui-li-has-arrow" );
222 a.first().removeClass( "ui-link" ).addClass( "ui-link-inherit" );
224 if ( a.length > 1 ) {
225 itemClass += " ui-li-has-alt";
228 splittheme = listsplittheme || last.jqmData( "theme" ) || o.splitTheme;
229 linkIcon = last.jqmData( "icon" );
231 last.appendTo( item )
232 .attr( "title", last.getEncodedText() )
233 .addClass( "ui-li-link-alt" )
242 .find( ".ui-btn-inner" )
244 $( document.createElement( "span" ) ).buttonMarkup({
249 // link icon overrides list item icon overrides ul element overrides options
250 icon: linkIcon || icon || listspliticon || o.splitIcon
254 } else if ( isDivider ) {
256 itemClass += " ui-li-divider ui-bar-" + dividertheme;
257 item.attr( "role", "heading" );
260 //reset counter when a divider heading is encountered
261 if ( start || start === 0 ) {
263 newStartCount = parseFloat( start ) - 1;
264 item.css( "counter-reset", "listnumbering " + newStartCount );
266 counter = parseFloat( start );
268 } else if ( jsCount ) {
274 itemClass += " ui-li-static ui-btn-up-" + itemTheme;
278 if ( ol && jsCount && itemClass.indexOf( "ui-li-divider" ) < 0 ) {
279 countParent = itemClass.indexOf( "ui-li-static" ) > 0 ? item : item.find( ".ui-link-inherit" );
281 countParent.addClass( "ui-li-jsnumbering" )
282 .prepend( "<span class='ui-li-dec'>" + ( counter++ ) + ". </span>" );
285 // Instead of setting item class directly on the list item and its
286 // btn-inner at this point in time, push the item into a dictionary
287 // that tells us what class to set on it so we can do this after this
288 // processing loop is finished.
290 if ( !itemClassDict[ itemClass ] ) {
291 itemClassDict[ itemClass ] = [];
294 itemClassDict[ itemClass ].push( item[ 0 ] );
297 // Set the appropriate listview item classes on each list item
298 // and their btn-inner elements. The main reason we didn't do this
299 // in the for-loop above is because we can eliminate per-item function overhead
300 // by calling addClass() and children() once or twice afterwards. This
301 // can give us a significant boost on platforms like WP7.5.
303 for ( itemClass in itemClassDict ) {
304 $( itemClassDict[ itemClass ] ).addClass( itemClass ).children( ".ui-btn-inner" ).addClass( itemClass );
307 $list.find( "h1, h2, h3, h4, h5, h6" ).addClass( "ui-li-heading" )
310 .find( "p, dl" ).addClass( "ui-li-desc" )
313 .find( ".ui-li-aside" ).each(function() {
314 var $this = $( this );
315 $this.prependTo( $this.parent() ); //shift aside to front for css float
319 .find( ".ui-li-count" ).each(function() {
320 $( this ).closest( "li" ).addClass( "ui-li-has-count" );
321 }).addClass( "ui-btn-up-" + ( $list.jqmData( "counttheme" ) || this.options.countTheme) + " ui-btn-corner-all" );
323 // The idea here is to look at the first image in the list item
324 // itself, and any .ui-link-inherit element it may contain, so we
325 // can place the appropriate classes on the image and list item.
326 // Note that we used to use something like:
328 // li.find(">img:eq(0), .ui-link-inherit>img:eq(0)").each( ... );
330 // But executing a find() like that on Windows Phone 7.5 took a
331 // really long time. Walking things manually with the code below
332 // allows the 400 listview item page to load in about 3 seconds as
333 // opposed to 30 seconds.
335 this._addThumbClasses( li );
336 this._addThumbClasses( $list.find( ".ui-link-inherit" ) );
338 this._refreshCorners( create );
340 // autodividers binds to this to redraw dividers after the listview refresh
341 this._trigger( "afterrefresh" );
344 //create a string for ID/subpage url creation
345 _idStringEscape: function( str ) {
346 return str.replace(/[^a-zA-Z0-9]/g, '-');
349 _createSubPages: function() {
350 var parentList = this.element,
351 parentPage = parentList.closest( ".ui-page" ),
352 parentUrl = parentPage.jqmData( "url" ),
353 parentId = parentUrl || parentPage[ 0 ][ $.expando ],
354 parentListId = parentList.attr( "id" ),
356 dns = "data-" + $.mobile.ns,
358 persistentFooterID = parentPage.find( ":jqmData(role='footer')" ).jqmData( "id" ),
361 if ( typeof listCountPerPage[ parentId ] === "undefined" ) {
362 listCountPerPage[ parentId ] = -1;
365 parentListId = parentListId || ++listCountPerPage[ parentId ];
367 $( parentList.find( "li>ul, li>ol" ).toArray().reverse() ).each(function( i ) {
370 listId = list.attr( "id" ) || parentListId + "-" + i,
371 parent = list.parent(),
372 nodeElsFull = $( list.prevAll().toArray().reverse() ),
373 nodeEls = nodeElsFull.length ? nodeElsFull : $( "<span>" + $.trim(parent.contents()[ 0 ].nodeValue) + "</span>" ),
374 title = nodeEls.first().getEncodedText(),//url limits to first 30 chars of text
375 id = ( parentUrl || "" ) + "&" + $.mobile.subPageUrlKey + "=" + listId,
376 theme = list.jqmData( "theme" ) || o.theme,
377 countTheme = list.jqmData( "counttheme" ) || parentList.jqmData( "counttheme" ) || o.countTheme,
380 //define hasSubPages for use in later removal
383 newPage = list.detach()
384 .wrap( "<div " + dns + "role='page' " + dns + "url='" + id + "' " + dns + "theme='" + theme + "' " + dns + "count-theme='" + countTheme + "'><div " + dns + "role='content'></div></div>" )
386 .before( "<div " + dns + "role='header' " + dns + "theme='" + o.headerTheme + "'><div class='ui-title'>" + title + "</div></div>" )
387 .after( persistentFooterID ? $( "<div " + dns + "role='footer' " + dns + "id='"+ persistentFooterID +"'>" ) : "" )
389 .appendTo( $.mobile.pageContainer );
393 anchor = parent.find( 'a:first' );
395 if ( !anchor.length ) {
396 anchor = $( "<a/>" ).html( nodeEls || title ).prependTo( parent.empty() );
399 anchor.attr( "href", "#" + id );
403 // on pagehide, remove any nested pages along with the parent page, as long as they aren't active
404 // and aren't embedded
406 parentPage.is( ":jqmData(external-page='true')" ) &&
407 parentPage.data( "page" ).options.domCache === false ) {
409 var newRemove = function( e, ui ) {
410 var nextPage = ui.nextPage, npURL,
411 prEvent = new $.Event( "pageremove" );
414 npURL = nextPage.jqmData( "url" );
415 if ( npURL.indexOf( parentUrl + "&" + $.mobile.subPageUrlKey ) !== 0 ) {
416 self.childPages().remove();
417 parentPage.trigger( prEvent );
418 if ( !prEvent.isDefaultPrevented() ) {
419 parentPage.removeWithDependents();
425 // unbind the original page remove and replace with our specialized version
427 .unbind( "pagehide.remove" )
428 .bind( "pagehide.remove", newRemove);
432 // TODO sort out a better way to track sub pages of the listview this is brittle
433 childPages: function() {
434 var parentUrl = this.parentPage.jqmData( "url" );
436 return $( ":jqmData(url^='"+ parentUrl + "&" + $.mobile.subPageUrlKey + "')" );
440 //auto self-init widgets
441 $( document ).bind( "pagecreate create", function( e ) {
442 $.mobile.listview.prototype.enhanceWithin( e.target );
446 //>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
448 //>>excludeEnd("jqmBuildExclude");