1 (function( $, undefined ) {
3 //Keeps track of the number of lists per page UID
4 //This allows support for multiple nested list in the same page
5 //https://github.com/jquery/jquery-mobile/issues/1617
6 var listCountPerPage = {};
8 $.widget( "mobile.listview", $.mobile.widget, {
18 initSelector: ":jqmData(role='listview')"
25 listviewClasses += t.options.inset ? " ui-listview-inset ui-corner-all ui-shadow " : "";
27 // create listview markup
28 t.element.addClass(function( i, orig ) {
29 return orig + " ui-listview " + listviewClasses;
35 _removeCorners: function( li, which ) {
36 var top = "ui-corner-top ui-corner-tr ui-corner-tl",
37 bot = "ui-corner-bottom ui-corner-br ui-corner-bl";
39 li = li.add( li.find( ".ui-btn-inner, .ui-li-link-alt, .ui-li-thumb" ) );
41 if ( which === "top" ) {
42 li.removeClass( top );
43 } else if ( which === "bottom" ) {
44 li.removeClass( bot );
46 li.removeClass( top + " " + bot );
50 _refreshCorners: function( create ) {
56 $li = this.element.children( "li" );
57 // At create time and when autodividers calls refresh the li are not visible yet so we need to rely on .ui-screen-hidden
58 $visibleli = create || $li.filter( ":visible" ).length === 0 ? $li.not( ".ui-screen-hidden" ) : $li.filter( ":visible" );
60 // ui-li-last is used for setting border-bottom on the last li
61 $li.filter( ".ui-li-last" ).removeClass( "ui-li-last" );
63 if ( this.options.inset ) {
64 this._removeCorners( $li );
66 // Select the first visible li element
67 $topli = $visibleli.first()
68 .addClass( "ui-corner-top" );
70 $topli.add( $topli.find( ".ui-btn-inner" )
71 .not( ".ui-li-link-alt span:first-child" ) )
72 .addClass( "ui-corner-top" )
74 .find( ".ui-li-link-alt, .ui-li-link-alt span:first-child" )
75 .addClass( "ui-corner-tr" )
77 .find( ".ui-li-thumb" )
79 .addClass( "ui-corner-tl" );
81 // Select the last visible li element
82 $bottomli = $visibleli.last()
83 .addClass( "ui-corner-bottom ui-li-last" );
85 $bottomli.add( $bottomli.find( ".ui-btn-inner" ) )
86 .find( ".ui-li-link-alt" )
87 .addClass( "ui-corner-br" )
89 .find( ".ui-li-thumb" )
91 .addClass( "ui-corner-bl" );
93 $visibleli.last().addClass( "ui-li-last" );
96 this.element.trigger( "updatelayout" );
100 // This is a generic utility method for finding the first
101 // node with a given nodeName. It uses basic DOM traversal
102 // to be fast and is meant to be a substitute for simple
103 // $.fn.closest() and $.fn.children() calls on a single
104 // element. Note that callers must pass both the lowerCase
105 // and upperCase version of the nodeName they are looking for.
106 // The main reason for this is that this function will be
107 // called many times and we want to avoid having to lowercase
108 // the nodeName from the element every time to ensure we have
109 // a match. Note that this function lives here for now, but may
110 // be moved into $.mobile if other components need a similar method.
111 _findFirstElementByTagName: function( ele, nextProp, lcName, ucName ) {
113 dict[ lcName ] = dict[ ucName ] = true;
115 if ( dict[ ele.nodeName ] ) {
118 ele = ele[ nextProp ];
122 _getChildrenByTagName: function( ele, lcName, ucName ) {
125 dict[ lcName ] = dict[ ucName ] = true;
126 ele = ele.firstChild;
128 if ( dict[ ele.nodeName ] ) {
131 ele = ele.nextSibling;
136 _addThumbClasses: function( containers ) {
137 var i, img, len = containers.length;
138 for ( i = 0; i < len; i++ ) {
139 img = $( this._findFirstElementByTagName( containers[ i ].firstChild, "nextSibling", "img", "IMG" ) );
141 img.addClass( "ui-li-thumb" ).attr( {
143 "aria-label" : "icon"
145 $( this._findFirstElementByTagName( img[ 0 ].parentNode, "parentNode", "li", "LI" ) ).addClass( img.is( ".ui-li-icon" ) ? "ui-li-has-icon" : "ui-li-has-thumb" );
150 _addCheckboxRadioClasses: function( containers )
152 var i, inputAttr, len = containers.length;
153 for ( i = 0; i < len; i++ ) {
154 inputAttr = $( containers[ i ] ).find( "input" );
155 if ( inputAttr.attr( "type" ) == "checkbox" ) {
156 $( containers[ i ] ).addClass( "ui-li-has-checkbox" );
157 } else if ( inputAttr.attr( "type" ) == "radio" ) {
158 $( containers[ i ] ).addClass( "ui-li-has-radio" );
163 _addRightBtnClasses: function( containers )
165 var i, btnAttr, len = containers.length;
166 for ( i = 0; i < len; i++ ) {
167 btnAttr = $( containers[ i ] ).find( ":jqmData(role='button'),input[type='button'],select:jqmData(role='slider')" );
168 if ( btnAttr.length ) {
169 if ( btnAttr.jqmData( "style" ) == "circle" ) {
170 $( containers[ i ] ).addClass( "ui-li-has-right-circle-btn" );
172 $( containers[ i ] ).addClass( "ui-li-has-right-btn" );
178 refresh: function( create ) {
179 this.parentPage = this.element.closest( ".ui-page" );
180 this._createSubPages();
182 var o = this.options,
183 $list = this.element,
185 dividertheme = $.mobile.getAttrFixed( $list[0], "data-" + $.mobile.ns + "dividertheme" ) || o.dividerTheme,
186 listsplittheme = $.mobile.getAttrFixed( $list[0], "data-" + $.mobile.ns + "splittheme" ),
187 listspliticon = $.mobile.getAttrFixed( $list[0], "data-" + $.mobile.ns + "spliticon" ),
188 li = this._getChildrenByTagName( $list[ 0 ], "li", "LI" ),
189 ol = !!$.nodeName( $list[ 0 ], "ol" ),
190 jsCount = !$.support.cssPseudoElement,
191 start = $list.attr( "start" ),
193 item, itemClass, itemTheme,
194 a, last, splittheme, counter, startCount, newStartCount, countParent, icon, imgParents, img, linkIcon;
196 if ( ol && jsCount ) {
197 $list.find( ".ui-li-dec" ).remove();
201 // Check if a start attribute has been set while taking a value of 0 into account
202 if ( start || start === 0 ) {
204 startCount = parseFloat( start ) - 1;
205 $list.css( "counter-reset", "listnumbering " + startCount );
207 counter = parseFloat( start );
209 } else if ( jsCount ) {
215 o.theme = $.mobile.getInheritedTheme( this.element, "c" );
218 for ( var pos = 0, numli = li.length; pos < numli; pos++ ) {
222 // If we're creating the element, we update it regardless
223 if ( create || !item.hasClass( "ui-li" ) ) {
224 itemTheme = $.mobile.getAttrFixed( item[0], "data-" + $.mobile.ns + "theme" ) || o.theme;
225 a = this._getChildrenByTagName( item[ 0 ], "a", "A" ).attr( {
229 var isDivider = ( $.mobile.getAttrFixed( item[0], "data-" + $.mobile.ns + "role" ) === "list-divider" );
231 if ( item.hasClass( "ui-li-has-checkbox" ) || item.hasClass( "ui-li-has-radio" ) ) {
232 item.on( "vclick", function ( e ) {
233 var targetItem = $( e.target );
234 var checkboxradio = targetItem.find( ".ui-checkbox" );
235 if ( !checkboxradio.length ) {
236 checkboxradio = targetItem.find( ".ui-radio" );
239 if ( checkboxradio.length ) {
240 checkboxradio.children( "label" ).trigger( "vclick" );
245 if ( a.length && !isDivider ) {
246 icon = $.mobile.getAttrFixed( item[0], "data-" + $.mobile.ns + "icon" );
248 /* Remove auto populated right-arrow button. */
249 if ( icon === undefined ) {
258 icon: a.length > 1 || icon === false ? false : icon || "arrow-r",
262 if ( ( icon !== false ) && ( a.length === 1 ) ) {
263 item.addClass( "ui-li-has-arrow" );
266 a.first().removeClass( "ui-link" ).addClass( "ui-link-inherit" );
268 if ( a.length > 1 ) {
269 itemClass += " ui-li-has-alt";
272 splittheme = listsplittheme || $.mobile.getAttrFixed( last[0], "data-" + $.mobile.ns + "theme" ) || o.splitTheme;
273 linkIcon = $.mobile.getAttrFixed( last[0], "data-" + $.mobile.ns + "icon" );
275 last.appendTo( item )
276 .attr( "title", last.getEncodedText() )
277 .addClass( "ui-li-link-alt" )
286 .find( ".ui-btn-inner" )
288 $( document.createElement( "span" ) ).buttonMarkup({
293 // link icon overrides list item icon overrides ul element overrides options
294 icon: linkIcon || icon || listspliticon || o.splitIcon
298 } else if ( isDivider ) {
300 itemClass += " ui-li-divider ui-bar-" + dividertheme;
301 item.attr( { "role": "heading", "tabindex": "0" } );
304 //reset counter when a divider heading is encountered
305 if ( start || start === 0 ) {
307 newStartCount = parseFloat( start ) - 1;
308 item.css( "counter-reset", "listnumbering " + newStartCount );
310 counter = parseFloat( start );
312 } else if ( jsCount ) {
318 itemClass += " ui-li-static ui-btn-up-" + itemTheme;
319 item.attr( "tabindex", "0" );
323 if ( ol && jsCount && itemClass.indexOf( "ui-li-divider" ) < 0 ) {
324 countParent = itemClass.indexOf( "ui-li-static" ) > 0 ? item : item.find( ".ui-link-inherit" );
326 countParent.addClass( "ui-li-jsnumbering" )
327 .prepend( "<span class='ui-li-dec'>" + ( counter++ ) + ". </span>" );
330 // Instead of setting item class directly on the list item and its
331 // btn-inner at this point in time, push the item into a dictionary
332 // that tells us what class to set on it so we can do this after this
333 // processing loop is finished.
335 if ( !itemClassDict[ itemClass ] ) {
336 itemClassDict[ itemClass ] = [];
339 itemClassDict[ itemClass ].push( item[ 0 ] );
342 // Set the appropriate listview item classes on each list item
343 // and their btn-inner elements. The main reason we didn't do this
344 // in the for-loop above is because we can eliminate per-item function overhead
345 // by calling addClass() and children() once or twice afterwards. This
346 // can give us a significant boost on platforms like WP7.5.
348 for ( itemClass in itemClassDict ) {
349 $( itemClassDict[ itemClass ] ).addClass( itemClass ).children( ".ui-btn-inner" ).addClass( itemClass );
352 $list.find( "h1, h2, h3, h4, h5, h6" ).addClass( "ui-li-heading" )
355 .find( "p, dl" ).addClass( "ui-li-desc" )
358 .find( ".ui-li-aside" ).each(function() {
359 var $this = $( this );
360 $this.prependTo( $this.parent() ); //shift aside to front for css float
364 .find( ".ui-li-count" ).each(function() {
365 $( this ).closest( "li" ).addClass( "ui-li-has-count" );
366 }).addClass( "ui-btn-up-" + ( $.mobile.getAttrFixed( $list[0], "data-" + $.mobile.ns + "counttheme" ) || this.options.countTheme) + " ui-btn-corner-all" );
368 // The idea here is to look at the first image in the list item
369 // itself, and any .ui-link-inherit element it may contain, so we
370 // can place the appropriate classes on the image and list item.
371 // Note that we used to use something like:
373 // li.find(">img:eq(0), .ui-link-inherit>img:eq(0)").each( ... );
375 // But executing a find() like that on Windows Phone 7.5 took a
376 // really long time. Walking things manually with the code below
377 // allows the 400 listview item page to load in about 3 seconds as
378 // opposed to 30 seconds.
380 this._addThumbClasses( li );
381 this._addThumbClasses( $list.find( ".ui-link-inherit" ) );
383 this._addCheckboxRadioClasses( li );
384 this._addCheckboxRadioClasses( $list.find( ".ui-link-inherit" ) );
386 this._addRightBtnClasses( li );
387 this._addRightBtnClasses( $list.find( ".ui-link-inherit" ) );
389 this._refreshCorners( create );
391 // autodividers binds to this to redraw dividers after the listview refresh
392 this._trigger( "afterrefresh" );
395 //create a string for ID/subpage url creation
396 _idStringEscape: function( str ) {
397 return str.replace(/[^a-zA-Z0-9]/g, '-');
400 _createSubPages: function() {
401 var parentList = this.element,
402 parentPage = parentList.closest( ".ui-page" ),
403 parentUrl = parentPage.jqmData( "url" ),
404 parentId = parentUrl || parentPage[ 0 ][ $.expando ],
405 parentListId = parentList.attr( "id" ),
407 dns = "data-" + $.mobile.ns,
409 persistentFooterID = parentPage.find( ":jqmData(role='footer')" ).jqmData( "id" ),
412 if ( typeof listCountPerPage[ parentId ] === "undefined" ) {
413 listCountPerPage[ parentId ] = -1;
416 parentListId = parentListId || ++listCountPerPage[ parentId ];
418 $( parentList.find( "li>ul, li>ol" ).toArray().reverse() ).each(function( i ) {
421 listId = list.attr( "id" ) || parentListId + "-" + i,
422 parent = list.parent(),
423 nodeElsFull = $( list.prevAll().toArray().reverse() ),
424 nodeEls = nodeElsFull.length ? nodeElsFull : $( "<span>" + $.trim(parent.contents()[ 0 ].nodeValue) + "</span>" ),
425 title = nodeEls.first().getEncodedText(),//url limits to first 30 chars of text
426 id = ( parentUrl || "" ) + "&" + $.mobile.subPageUrlKey + "=" + listId,
427 theme = $.mobile.getAttrFixed( list[0], "data-" + $.mobile.ns + "theme" ) || o.theme,
428 countTheme = $.mobile.getAttrFixed( list[0], "data-" + $.mobile.ns + "counttheme" ) || $.mobile.getAttrFixed( parentList[0], "data-" + $.mobile.ns + "counttheme" ) || o.countTheme,
431 //define hasSubPages for use in later removal
434 newPage = list.detach()
435 .wrap( "<div " + dns + "role='page' " + dns + "url='" + id + "' " + dns + "theme='" + theme + "' " + dns + "count-theme='" + countTheme + "'><div " + dns + "role='content'></div></div>" )
437 .before( "<div " + dns + "role='header' " + dns + "theme='" + o.headerTheme + "'><div class='ui-title'>" + title + "</div></div>" )
438 .after( persistentFooterID ? $( "<div " + dns + "role='footer' " + dns + "id='"+ persistentFooterID +"'>" ) : "" )
440 .appendTo( $.mobile.pageContainer );
444 anchor = parent.find( 'a:first' );
446 if ( !anchor.length ) {
447 anchor = $( "<a/>" ).html( nodeEls || title ).prependTo( parent.empty() );
450 anchor.attr( "href", "#" + id );
454 // on pagehide, remove any nested pages along with the parent page, as long as they aren't active
455 // and aren't embedded
457 parentPage.is( ":jqmData(external-page='true')" ) &&
458 parentPage.data( "page" ).options.domCache === false ) {
460 var newRemove = function( e, ui ) {
461 var nextPage = ui.nextPage, npURL,
462 prEvent = new $.Event( "pageremove" );
465 npURL = nextPage.jqmData( "url" );
466 if ( npURL.indexOf( parentUrl + "&" + $.mobile.subPageUrlKey ) !== 0 ) {
467 self.childPages().remove();
468 parentPage.trigger( prEvent );
469 if ( !prEvent.isDefaultPrevented() ) {
470 parentPage.removeWithDependents();
476 // unbind the original page remove and replace with our specialized version
478 .unbind( "pagehide.remove" )
479 .bind( "pagehide.remove", newRemove);
483 addItem : function( listitem , idx ) {
484 var $item = $(listitem),
488 $li = _self.element.children( 'li' );
489 $item.css( { 'opacity' : 0,
490 'display' : 'none' } );
492 || $li.length <= idx)
494 $( _self.element ).append( $item );
496 $( $li.get( idx ) ).before( $item );
498 $(_self.element).trigger("create")
499 .listview( 'refresh' );
501 $item.css( 'min-height' , '0px' );
503 $item.slideDown( 'fast' , function( ){
504 $item.addClass("addli");
505 $item.css( { 'opacity' : 1 } );
509 removeItem : function( idx ) {
514 $li = _self.element.children( 'li' );
515 if( $li.length <= 0 ||
519 $item = $( $li.get( idx ) );
520 $item.addClass("removeli");
521 $item.slideUp('normal',
527 // TODO sort out a better way to track sub pages of the listview this is brittle
528 childPages: function() {
529 var parentUrl = this.parentPage.jqmData( "url" );
531 return $( ":jqmData(url^='"+ parentUrl + "&" + $.mobile.subPageUrlKey + "')" );
535 //delegate auto self-init widgets
536 $.delegateSelfInitWithSingleSelector( $.mobile.listview );