1 /* ***************************************************************************
2 * Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd.
4 * Permission is hereby granted, free of charge, to any person obtaining a
5 * copy of this software and associated documentation files (the "Software"),
6 * to deal in the Software without restriction, including without limitation
7 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
8 * and/or sell copies of the Software, and to permit persons to whom the
9 * Software is furnished to do so, subject to the following conditions:
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20 * DEALINGS IN THE SOFTWARE.
21 * ***************************************************************************
23 * Author: Wongi Lee <wongi11.lee@samsung.com>
27 * Extendable List Widget for unlimited data.
28 * To support more then 1,000 items, special list widget developed.
29 * Fast initialize and append some element into the DOM tree repeatedly.
30 * DB connection and works like DB cursor.
34 * data-role: extendablelist
35 * data-template : jQuery.template ID that populate into extendable list. A button : a <DIV> element with "data-role : button" should be included on data-template.
36 * data-dbtable : DB Table name. It used as window[DB NAME]. Loaded data should be converted as window object.
37 * data-extenditems : Number of elements to extend at once.
39 * ID : <UL> element that has "data-role=extendablelist" must have ID attribute.
40 * Class : <UL> element that has "data-role=extendablelist" should have "vlLoadSuccess" class to guaranty DB loading is completed.
41 * tmp_load_more : Template ID for "load more" message and button.
47 * : API to call _create method. API for AJAX or DB loading callback.
50 * : Update extendable list with new data array. For example, update with search result.
54 * <script id="tmp-3-1-1" type="text/x-jquery-tmpl">
55 * <li class="ui-li-3-1-1"><span class="ui-li-text-main">${NAME}</span></li>
58 * <script id="tmp_load_more" type="text/x-jquery-tmpl">
59 * <li class="ui-li-3-1-1" style="text-align:center; margin:0 auto">
60 * <div data-role="button">Load ${NUM_MORE_ITEMS} more items</div>
64 * <ul id = "extendable_list_main" data-role="extendablelist" data-extenditems="50" data-template="tmp-3-1-1" data-dbtable="JSON_DATA">
70 ( function ( $, undefined ) {
72 //Keeps track of the number of lists per page UID
73 //This allows support for multiple nested list in the same page
74 //https://github.com/jquery/jquery-mobile/issues/1617
75 var listCountPerPage = {},
79 $.widget( "tizen.extendablelist", $.mobile.widget, {
88 id: "", /* Extendable list UL elemet's ID */
89 extenditems: 50, /* Number of append items */
90 childSelector: " li", /* To support swipe list */
92 template : "", /* Template for each list item */
93 loadmore : "tmp_load_more", /* Template for "Load more" message */
95 initSelector: ":jqmData(role='extendablelist')"
98 _stylerMouseUp: function () {
99 $( this ).addClass( "ui-btn-up-s" );
100 $( this ).removeClass( "ui-btn-down-s" );
103 _stylerMouseDown: function () {
104 $( this ).addClass( "ui-btn-down-s" );
105 $( this ).removeClass( "ui-btn-up-s" );
108 _stylerMouseOver: function () {
109 $( this ).toggleClass( "ui-btn-hover-s" );
112 _stylerMouseOut: function () {
113 $( this ).toggleClass( "ui-btn-hover-s" );
114 $( this ).addClass( "ui-btn-up-s" );
115 $( this ).removeClass( "ui-btn-down-s" );
118 _pushData: function ( template, data ) {
119 var o = this.options,
123 myTemplate = $( "#" + template ),
124 loadMoreItems = ( o.extenditems > data.length - last_index ? data.length - last_index : o.extenditems ),
127 for (i = 0; i < loadMoreItems; i++ ) {
128 htmlData = myTemplate.tmpl( dataTable[ i ] );
129 $( o.id ).append( $( htmlData ).attr( 'id', 'li_' + i ) );
132 $( o.id + ">" + o.childSelector )
133 .addClass( "ui-btn-up-s" )
134 .bind( "mouseup", t._stylerMouseUp )
135 .bind( "mousedown", t._stylerMouseDown )
136 .bind( "mouseover", t._stylerMouseOver )
137 .bind( "mouseout", t._stylerMouseOut );
142 /* After push data, re-style extendable list widget */
143 $( o.id ).trigger( "create" );
146 _loadmore: function ( event ) {
150 dataTable = window[ o.dbtable ],
151 myTemplate = $( "#" + o.template ),
152 loadMoreItems = ( o.extenditems > dataTable.length - last_index ? dataTable.length - last_index : o.extenditems ),
157 /* Remove load more message */
158 $( "#load_more_message" ).remove();
160 /* Append More Items */
161 for ( i = 0; i < loadMoreItems; i++ ) {
162 htmlData = myTemplate.tmpl( dataTable[ last_index ] );
163 $( o.id ).append( $( htmlData ).attr( 'id', 'li_' + last_index ) );
167 /* Append "Load more" message on the last of list */
168 if ( TOTAL_ITEMS > last_index ) {
169 myTemplate = $( "#" + o.loadmore );
170 more_items_to_load = TOTAL_ITEMS - last_index;
171 num_next_load_items = ( o.extenditems <= more_items_to_load ) ? o.extenditems : more_items_to_load;
172 htmlData = myTemplate.tmpl( { NUM_MORE_ITEMS : num_next_load_items } );
174 $( o.id ).append( $( htmlData ).attr( 'id', "load_more_message" ) );
177 $( o.id ).trigger( "create" );
178 $( o.id ).extendablelist( "refresh" );
181 recreate: function ( newArray ) {
192 TOTAL_ITEMS = newArray.length;
194 t._pushData( ( o.template), newArray );
196 /* Append "Load more" message on the last of list */
197 if ( TOTAL_ITEMS > last_index ) {
198 myTemplate = $( "#" + o.loadmore );
199 more_items_to_load = TOTAL_ITEMS - last_index;
200 num_next_load_items = ( o.extenditems <= more_items_to_load) ? o.extenditems : more_items_to_load;
201 htmlData = myTemplate.tmpl( { NUM_MORE_ITEMS : num_next_load_items } );
203 $( o.id ).append( $( htmlData ).attr( 'id', "load_more_message" ) );
205 $( "#load_more_message" ).live( "click", t.options, t._loadmore );
207 /* No more items to load */
208 $( "#load_more_message" ).die();
209 $( "#load_more_message" ).remove();
212 if ( o.childSelector == " ul" ) {
213 $( o.id + " ul" ).swipelist();
216 $( o.id ).extendablelist();
221 _initList: function () {
229 /* After AJAX loading success */
230 o.dbtable = t.element.data( "dbtable" );
232 TOTAL_ITEMS = $( window[ o.dbtable ] ).size();
234 /* Make Gen list by template */
235 if ( last_index <= 0 ) {
236 t._pushData( ( o.template ), window[ o.dbtable ] );
238 /* Append "Load more" message on the last of list */
239 if ( TOTAL_ITEMS > last_index ) {
240 myTemplate = $( "#" + o.loadmore );
241 more_items_to_load = TOTAL_ITEMS - last_index;
242 num_next_load_items = ( o.extenditems <= more_items_to_load) ? o.extenditems : more_items_to_load;
243 htmlData = myTemplate.tmpl( { NUM_MORE_ITEMS : num_next_load_items } );
245 $( o.id ).append( $( htmlData ).attr( 'id', "load_more_message" ) );
247 $( "#load_more_message" ).live( "click", t.options, t._loadmore );
249 /* No more items to load */
250 $( "#load_more_message" ).die();
251 $( "#load_more_message" ).remove();
255 if ( o.childSelector == " ul" ) {
256 $( o.id + " ul" ).swipelist();
259 $( o.id ).trigger( "create" );
264 create: function () {
265 var o = this.options;
267 /* external API for AJAX callback */
268 this._create( "create" );
271 _create: function ( event ) {
276 // create listview markup
277 t.element.addClass( function ( i, orig ) {
278 return orig + " ui-listview ui-extendable-list-container" + ( t.options.inset ? " ui-listview-inset ui-corner-all ui-shadow " : "" );
281 o.id = "#" + $el.attr( "id" );
283 if ( $el.data( "extenditems" ) ) {
284 o.extenditems = parseInt( $el.data( "extenditems" ), 10 );
287 $( o.id ).bind( "pagehide", function (e) {
292 if ( $( ".ui-scrollview-clip" ).size() > 0) {
295 o.scrollview = false;
298 /* After DB Load complete, Init Extendable list */
299 if ( $( o.id ).hasClass( "elLoadSuccess" ) ) {
300 if ( !$( o.id ).hasClass( "elInitComplete" ) ) {
301 if ( $el.data( "template" ) ) {
302 o.template = $el.data( "template" );
304 /* to support swipe list, <li> or <ul> can be main node of extendable list. */
305 if ( $el.data( "swipelist" ) == true ) {
306 o.childSelector = " ul";
308 o.shildSelector = " li";
312 $( o.id ).addClass( "elInitComplete" );
319 destroy : function () {
320 var o = this.options;
327 $( "#load_more_message" ).die();
330 _itemApply: function ( $list, item ) {
331 var $countli = item.find( ".ui-li-count" );
333 if ( $countli.length ) {
334 item.addClass( "ui-li-has-count" );
337 $countli.addClass( "ui-btn-up-" + ( $list.jqmData( "counttheme" ) || this.options.countTheme ) + " ui-btn-corner-all" );
339 // TODO class has to be defined in markup
340 item.find( "h1, h2, h3, h4, h5, h6" ).addClass( "ui-li-heading" ).end()
341 .find( "p, dl" ).addClass( "ui-li-desc" ).end()
342 .find( ">img:eq(0), .ui-link-inherit>img:eq(0)" ).addClass( "ui-li-thumb" ).each(function () {
343 item.addClass( $( this ).is( ".ui-li-icon" ) ? "ui-li-has-icon" : "ui-li-has-thumb" );
345 .find( ".ui-li-aside" ).each(function () {
346 var $this = $( this );
347 $this.prependTo( $this.parent() ); //shift aside to front for css float
351 _removeCorners: function ( li, which ) {
352 var top = "ui-corner-top ui-corner-tr ui-corner-tl",
353 bot = "ui-corner-bottom ui-corner-br ui-corner-bl";
355 li = li.add( li.find( ".ui-btn-inner, .ui-li-link-alt, .ui-li-thumb" ) );
357 if ( which === "top" ) {
358 li.removeClass( top );
359 } else if ( which === "bottom" ) {
360 li.removeClass( bot );
362 li.removeClass( top + " " + bot );
366 _refreshCorners: function ( create ) {
372 if ( this.options.inset ) {
373 $li = this.element.children( "li" );
374 // at create time the li are not visible yet so we need to rely on .ui-screen-hidden
375 $visibleli = create ? $li.not( ".ui-screen-hidden" ) : $li.filter( ":visible" );
377 this._removeCorners( $li );
379 // Select the first visible li element
380 $topli = $visibleli.first()
381 .addClass( "ui-corner-top" );
383 $topli.add( $topli.find( ".ui-btn-inner" ) )
384 .find( ".ui-li-link-alt" )
385 .addClass( "ui-corner-tr" )
387 .find( ".ui-li-thumb" )
388 .not( ".ui-li-icon" )
389 .addClass( "ui-corner-tl" );
391 // Select the last visible li element
392 $bottomli = $visibleli.last()
393 .addClass( "ui-corner-bottom" );
395 $bottomli.add( $bottomli.find( ".ui-btn-inner" ) )
396 .find( ".ui-li-link-alt" )
397 .addClass( "ui-corner-br" )
399 .find( ".ui-li-thumb" )
400 .not( ".ui-li-icon" )
401 .addClass( "ui-corner-bl" );
405 refresh: function ( create ) {
406 this.parentPage = this.element.closest( ".ui-page" );
407 this._createSubPages();
409 var o = this.options,
410 $list = this.element,
412 dividertheme = $list.jqmData( "dividertheme" ) || o.dividerTheme,
413 listsplittheme = $list.jqmData( "splittheme" ),
414 listspliticon = $list.jqmData( "spliticon" ),
415 li = $list.children( "li" ),
416 counter = $.support.cssPseudoElement || !$.nodeName( $list[ 0 ], "ol" ) ? 0 : 1,
429 $list.find( ".ui-li-dec" ).remove();
432 for ( pos = 0, numli = li.length; pos < numli; pos++ ) {
436 // If we're creating the element, we update it regardless
437 if ( create || !item.hasClass( "ui-li" ) ) {
438 itemTheme = item.jqmData( "theme" ) || o.theme;
439 a = item.children( "a" );
442 icon = item.jqmData( "icon" );
449 /* icon: a.length > 1 || icon === false ? false : icon || "arrow-r",*/
450 icon: false, /* Remove unnecessary arrow icon */
454 if ( ( icon != false ) && ( a.length == 1 ) ) {
455 item.addClass( "ui-li-has-arrow" );
458 a.first().addClass( "ui-link-inherit" );
460 if ( a.length > 1 ) {
461 itemClass += " ui-li-has-alt";
464 splittheme = listsplittheme || last.jqmData( "theme" ) || o.splitTheme;
467 .attr( "title", last.getEncodedText() )
468 .addClass( "ui-li-link-alt" )
477 .find( ".ui-btn-inner" )
479 $( "<span />" ).buttonMarkup( {
484 icon : listspliticon || last.jqmData( "icon" ) || o.splitIcon
488 } else if ( item.jqmData( "role" ) === "list-divider" ) {
490 itemClass += " ui-li-divider ui-btn ui-bar-" + dividertheme;
491 item.attr( "role", "heading" );
493 //reset counter when a divider heading is encountered
499 itemClass += " ui-li-static ui-body-" + itemTheme;
503 if ( counter && itemClass.indexOf( "ui-li-divider" ) < 0 ) {
504 countParent = item.is( ".ui-li-static:first" ) ? item : item.find( ".ui-link-inherit" );
506 countParent.addClass( "ui-li-jsnumbering" )
507 .prepend( "<span class='ui-li-dec'>" + (counter++) + ". </span>" );
510 item.add( item.children( ".ui-btn-inner" ) ).addClass( itemClass );
512 self._itemApply( $list, item );
515 this._refreshCorners( create );
518 //create a string for ID/subpage url creation
519 _idStringEscape: function ( str ) {
520 return str.replace(/\W/g , "-");
524 _createSubPages: function () {
525 var parentList = this.element,
526 parentPage = parentList.closest( ".ui-page" ),
527 parentUrl = parentPage.jqmData( "url" ),
528 parentId = parentUrl || parentPage[ 0 ][ $.expando ],
529 parentListId = parentList.attr( "id" ),
531 dns = "data-" + $.mobile.ns,
533 persistentFooterID = parentPage.find( ":jqmData(role='footer')" ).jqmData( "id" ),
537 if ( typeof listCountPerPage[ parentId ] === "undefined" ) {
538 listCountPerPage[ parentId ] = -1;
541 parentListId = parentListId || ++listCountPerPage[ parentId ];
543 $( parentList.find( "li>ul, li>ol" ).toArray().reverse() ).each(function ( i ) {
546 listId = list.attr( "id" ) || parentListId + "-" + i,
547 parent = list.parent(),
549 title = nodeEls.first().getEncodedText(),//url limits to first 30 chars of text
550 id = ( parentUrl || "" ) + "&" + $.mobile.subPageUrlKey + "=" + listId,
551 theme = list.jqmData( "theme" ) || o.theme,
552 countTheme = list.jqmData( "counttheme" ) || parentList.jqmData( "counttheme" ) || o.countTheme,
556 nodeEls = $( list.prevAll().toArray().reverse() );
557 nodeEls = nodeEls.length ? nodeEls : $( "<span>" + $.trim(parent.contents()[ 0 ].nodeValue) + "</span>" );
559 //define hasSubPages for use in later removal
562 newPage = list.detach()
563 .wrap( "<div " + dns + "role='page' " + dns + "url='" + id + "' " + dns + "theme='" + theme + "' " + dns + "count-theme='" + countTheme + "'><div " + dns + "role='content'></div></div>" )
565 .before( "<div " + dns + "role='header' " + dns + "theme='" + o.headerTheme + "'><div class='ui-title'>" + title + "</div></div>" )
566 .after( persistentFooterID ? $( "<div " + dns + "role='footer' " + dns + "id='" + persistentFooterID + "'>" ) : "" )
568 .appendTo( $.mobile.pageContainer );
572 anchor = parent.find('a:first');
574 if ( !anchor.length ) {
575 anchor = $( "<a/>" ).html( nodeEls || title ).prependTo( parent.empty() );
578 anchor.attr( "href", "#" + id );
582 // on pagehide, remove any nested pages along with the parent page, as long as they aren't active
583 // and aren't embedded
585 parentPage.is( ":jqmData(external-page='true')" ) &&
586 parentPage.data( "page" ).options.domCache === false ) {
588 newRemove = function ( e, ui ) {
589 var nextPage = ui.nextPage, npURL;
592 npURL = nextPage.jqmData( "url" );
593 if ( npURL.indexOf( parentUrl + "&" + $.mobile.subPageUrlKey ) !== 0 ) {
594 self.childPages().remove();
600 // unbind the original page remove and replace with our specialized version
602 .unbind( "pagehide.remove" )
603 .bind( "pagehide.remove", newRemove);
607 // TODO sort out a better way to track sub pages of the extendable listview this is brittle
608 childPages: function () {
609 var parentUrl = this.parentPage.jqmData( "url" );
611 return $( ":jqmData(url^='" + parentUrl + "&" + $.mobile.subPageUrlKey + "')" );
615 //auto self-init widgets
616 $( document ).bind( "pagecreate create", function ( e ) {
617 $( $.tizen.extendablelist.prototype.options.initSelector, e.target ).extendablelist();