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.
46 * itemData: function ( idx ) { return json_obj; },
47 * numItemData: number or function () { return number; },
48 * cacheItemData: function ( minIdx, maxIdx ) {}
50 * : Create a extendable list widget. At this moment, _create method is called.
51 * args : A collection of options
52 * itemData: A function that returns JSON object for given index. Mandatory.
53 * numItemData: Total number of itemData. Mandatory.
54 * cacheItemData: Extendable list will ask itemData between minIdx and maxIdx.
55 * Developers can implement this function for preparing data.
60 * <script id="tmp-3-1-1" type="text/x-jquery-tmpl">
61 * <li class="ui-li-3-1-1"><span class="ui-li-text-main">${NAME}</span></li>
64 * <script id="tmp_load_more" type="text/x-jquery-tmpl">
65 * <li class="ui-li-3-1-1" style="text-align:center; margin:0 auto">
66 * <div data-role="button">Load ${NUM_MORE_ITEMS} more items</div>
70 * <ul id = "extendable_list_main" data-role="extendablelist" data-extenditems="50" data-template="tmp-3-1-1">
77 In the Web environment, it is challenging to display a large amount of data in a list, such as displaying a contact list of over 1000 list items. It takes time to display the entire list in HTML and the DOM manipulation is complex.
78 The extendable list widget is used to display a list of unlimited data elements on the screen for better performance. The list is extended if you click the button at the bottom of the list to load more data elements. Extendable lists are based on the jQuery.template plugin as described in the jQuery documentation for jQuery.template plugin.<br/>
79 To add a extendable list widget to the application, use the following code:
81 <script id="tmp-3-1-1" type="text/x-jquery-tmpl">
82 <li class="ui-li-3-1-1"><span class="ui-li-text-main">${NAME}</span></li>
84 <script id="tmp_load_more" type="text/x-jquery-tmpl">
85 <li class="ui-li-3-1-1" style="text-align:center; margin:0 auto">
86 <div data-role="button">Load ${NUM_MORE_ITEMS} more items</div>
89 <ul id="extendable_list_main" data-role="extendablelist" data-extenditems="50" data-template="tmp-3-1-1">
93 @property {String} data-role
94 Creates the extendable list view. The value must be set to extendablelist. Only the <ul> element, which a id attribute defined, supports this option. Also, the elLoadSuccess class attribute must be defined in the <ul> element to ensure that loading data from the database is complete.
97 @property {String} data-template
98 Specifies the jQuery.template element ID. The jQuery.template must be defined. The template style can use rem units to support scalability. For using the button at the bottom of the list to load more data elements, there must be list view template with the button. The attribute ID must be tmp_load_more.
101 @property {Integer} data-extenditems
102 Defines the number of data elements to be extended at a time.
104 ( function ( $, undefined ) {
106 //Keeps track of the number of lists per page UID
107 //This allows support for multiple nested list in the same page
108 //https://github.com/jquery/jquery-mobile/issues/1617
109 var listCountPerPage = {};
111 $.widget( "tizen.extendablelist", $.mobile.widget, {
117 splitIcon: "arrow-r",
120 id: "", /* Extendable list UL elemet's ID */
121 extenditems: 50, /* Number of append items */
122 childSelector: " li", /* To support swipe list */
124 template : "", /* Template for each list item */
125 loadmore : "tmp_load_more", /* Template for "Load more" message */
127 initSelector: ":jqmData(role='extendablelist')"
130 _stylerMouseUp: function () {
131 $( this ).addClass( "ui-btn-up-s" );
132 $( this ).removeClass( "ui-btn-down-s" );
135 _stylerMouseDown: function () {
136 $( this ).addClass( "ui-btn-down-s" );
137 $( this ).removeClass( "ui-btn-up-s" );
140 _stylerMouseOver: function () {
141 $( this ).toggleClass( "ui-btn-hover-s" );
144 _stylerMouseOut: function () {
145 $( this ).toggleClass( "ui-btn-hover-s" );
146 $( this ).addClass( "ui-btn-up-s" );
147 $( this ).removeClass( "ui-btn-down-s" );
150 _pushData: function ( template ) {
151 var o = this.options,
154 myTemplate = $( "#" + template ),
155 loadMoreItems = ( o.extenditems > t._numItemData - t._lastIndex ? t._numItemData - t.lastIndex : o.extenditems ),
158 for (i = 0; i < loadMoreItems; i++ ) {
159 htmlData = myTemplate.tmpl( t._itemData( i ) );
160 $( o.id ).append( $( htmlData ).attr( 'id', 'li_' + i ) );
163 $( o.id + ">" + o.childSelector )
164 .addClass( "ui-btn-up-s" )
165 .bind( "mouseup", t._stylerMouseUp )
166 .bind( "mousedown", t._stylerMouseDown )
167 .bind( "mouseover", t._stylerMouseOver )
168 .bind( "mouseout", t._stylerMouseOut );
173 /* After push data, re-style extendable list widget */
174 $( o.id ).trigger( "create" );
177 _loadmore: function ( event ) {
178 var t = event.data, // <li> element
181 myTemplate = $( "#" + o.template ),
182 loadMoreItems = ( o.extenditems > t._numItemData - t._lastIndex ? t._numItemData - t._lastIndex : o.extenditems ),
187 /* Remove load more message */
188 $( "#load_more_message" ).remove();
190 /* Append More Items */
191 for ( i = 0; i < loadMoreItems; i++ ) {
192 htmlData = myTemplate.tmpl( t._itemData( t._lastIndex ) );
193 $( o.id ).append( $( htmlData ).attr( 'id', 'li_' + t._lastIndex ) );
197 /* Append "Load more" message on the last of list */
198 if ( t._numItemData > t._lastIndex ) {
199 myTemplate = $( "#" + o.loadmore );
200 more_items_to_load = t._numItemData - t._lastIndex;
201 num_next_load_items = ( o.extenditems <= more_items_to_load ) ? o.extenditems : more_items_to_load;
202 htmlData = myTemplate.tmpl( { NUM_MORE_ITEMS : num_next_load_items } );
204 $( o.id ).append( $( htmlData ).attr( 'id', "load_more_message" ) );
207 $( o.id ).trigger( "create" );
208 $( o.id ).extendablelist( "refresh" );
211 recreate: function ( newArray ) {
213 itemData: function ( idx ) { return newArray[ idx ]; },
214 numItemData: newArray.length
218 _initList: function (args ) {
226 /* Make Gen list by template */
227 if ( t._lastIndex <= 0 ) {
228 t._pushData( o.template );
230 /* Append "Load more" message on the last of list */
231 if ( t._numItemData > t._lastIndex ) {
232 myTemplate = $( "#" + o.loadmore );
233 more_items_to_load = t._numItemData - t._lastIndex;
234 num_next_load_items = ( o.extenditems <= more_items_to_load) ? o.extenditems : more_items_to_load;
235 htmlData = myTemplate.tmpl( { NUM_MORE_ITEMS : num_next_load_items } );
237 $( o.id ).append( $( htmlData ).attr( 'id', "load_more_message" ) );
239 $( "#load_more_message" ).live( "click", t, t._loadmore );
241 /* No more items to load */
242 $( "#load_more_message" ).die();
243 $( "#load_more_message" ).remove();
247 if ( o.childSelector == " ul" ) {
248 $( o.id + " ul" ).swipelist();
251 $( o.id ).trigger( "create" );
256 create: function () {
257 var o = this.options;
259 /* external API for AJAX callback */
260 this._create.apply( this, arguments );
263 _create: function ( args ) {
273 _itemData: function ( idx ) { return null; },
275 _cacheItemData: function ( minIdx, maxIdx ) { },
280 // create listview markup
281 t.element.addClass( function ( i, orig ) {
282 return orig + " ui-listview ui-extendable-list-container" + ( t.options.inset ? " ui-listview-inset ui-corner-all ui-shadow " : "" );
285 o.id = "#" + $el.attr( "id" );
287 if ( $el.data( "extenditems" ) ) {
288 o.extenditems = parseInt( $el.data( "extenditems" ), 10 );
291 $( o.id ).bind( "pagehide", function (e) {
296 if ( $( ".ui-scrollview-clip" ).size() > 0) {
299 o.scrollview = false;
303 if ( !t._loadData( args ) ) {
307 // Legacy support: dbtable
308 console.warn("WARNING: The data interface of extendable list is changed. \nOld data interface(data-dbtable) is still supported, but will be removed in next version. \nPlease fix your code soon!");
310 if ( $( o.id ).hasClass( "elLoadSuccess" ) ) {
311 dbtable_name = $el.jqmData('dbtable');
312 o.dbtable = window[ dbtable_name ];
313 if ( !(o.dbtable) ) {
316 t._itemData = function ( idx ) {
317 return o.dbtable[ idx ];
319 t._numItemData = o.dbtable.length;
322 console.warn("No elLoadSuccess class");
327 if ( $el.data( "template" ) ) {
328 o.template = $el.data( "template" );
330 /* to support swipe list, <li> or <ul> can be main node of extendable list. */
331 if ( $el.data( "swipelist" ) == true ) {
332 o.childSelector = " ul";
334 o.shildSelector = " li";
340 _loadData : function ( args ) {
343 if ( args.itemData && typeof args.itemData == 'function' ) {
344 self._itemData = args.itemData;
348 if ( args.numItemData ) {
349 if ( typeof args.numItemData == 'function' ) {
350 self._numItemData = args.numItemData( );
351 } else if ( typeof args.numItemData == 'number' ) {
352 self._numItemData = args.numItemData;
363 destroy : function () {
364 var o = this.options,
370 $( "#load_more_message" ).die();
373 _itemApply: function ( $list, item ) {
374 var $countli = item.find( ".ui-li-count" );
376 if ( $countli.length ) {
377 item.addClass( "ui-li-has-count" );
380 $countli.addClass( "ui-btn-up-" + ( $list.jqmData( "counttheme" ) || this.options.countTheme ) + " ui-btn-corner-all" );
382 // TODO class has to be defined in markup
383 item.find( "h1, h2, h3, h4, h5, h6" ).addClass( "ui-li-heading" ).end()
384 .find( "p, dl" ).addClass( "ui-li-desc" ).end()
385 .find( ">img:eq(0), .ui-link-inherit>img:eq(0)" ).addClass( "ui-li-thumb" ).each(function () {
386 item.addClass( $( this ).is( ".ui-li-icon" ) ? "ui-li-has-icon" : "ui-li-has-thumb" );
388 .find( ".ui-li-aside" ).each(function () {
389 var $this = $( this );
390 $this.prependTo( $this.parent() ); //shift aside to front for css float
394 _removeCorners: function ( li, which ) {
395 var top = "ui-corner-top ui-corner-tr ui-corner-tl",
396 bot = "ui-corner-bottom ui-corner-br ui-corner-bl";
398 li = li.add( li.find( ".ui-btn-inner, .ui-li-link-alt, .ui-li-thumb" ) );
400 if ( which === "top" ) {
401 li.removeClass( top );
402 } else if ( which === "bottom" ) {
403 li.removeClass( bot );
405 li.removeClass( top + " " + bot );
409 _refreshCorners: function ( create ) {
415 if ( this.options.inset ) {
416 $li = this.element.children( "li" );
417 // at create time the li are not visible yet so we need to rely on .ui-screen-hidden
418 $visibleli = create ? $li.not( ".ui-screen-hidden" ) : $li.filter( ":visible" );
420 this._removeCorners( $li );
422 // Select the first visible li element
423 $topli = $visibleli.first()
424 .addClass( "ui-corner-top" );
426 $topli.add( $topli.find( ".ui-btn-inner" ) )
427 .find( ".ui-li-link-alt" )
428 .addClass( "ui-corner-tr" )
430 .find( ".ui-li-thumb" )
431 .not( ".ui-li-icon" )
432 .addClass( "ui-corner-tl" );
434 // Select the last visible li element
435 $bottomli = $visibleli.last()
436 .addClass( "ui-corner-bottom" );
438 $bottomli.add( $bottomli.find( ".ui-btn-inner" ) )
439 .find( ".ui-li-link-alt" )
440 .addClass( "ui-corner-br" )
442 .find( ".ui-li-thumb" )
443 .not( ".ui-li-icon" )
444 .addClass( "ui-corner-bl" );
448 refresh: function ( create ) {
449 this.parentPage = this.element.closest( ".ui-page" );
450 this._createSubPages();
452 var o = this.options,
453 $list = this.element,
455 dividertheme = $list.jqmData( "dividertheme" ) || o.dividerTheme,
456 listsplittheme = $list.jqmData( "splittheme" ),
457 listspliticon = $list.jqmData( "spliticon" ),
458 li = $list.children( "li" ),
459 counter = $.support.cssPseudoElement || !$.nodeName( $list[ 0 ], "ol" ) ? 0 : 1,
472 $list.find( ".ui-li-dec" ).remove();
475 for ( pos = 0, numli = li.length; pos < numli; pos++ ) {
479 // If we're creating the element, we update it regardless
480 if ( create || !item.hasClass( "ui-li" ) ) {
481 itemTheme = item.jqmData( "theme" ) || o.theme;
482 a = item.children( "a" );
485 icon = item.jqmData( "icon" );
492 /* icon: a.length > 1 || icon === false ? false : icon || "arrow-r",*/
493 icon: false, /* Remove unnecessary arrow icon */
497 if ( ( icon != false ) && ( a.length == 1 ) ) {
498 item.addClass( "ui-li-has-arrow" );
501 a.first().addClass( "ui-link-inherit" );
503 if ( a.length > 1 ) {
504 itemClass += " ui-li-has-alt";
507 splittheme = listsplittheme || last.jqmData( "theme" ) || o.splitTheme;
510 .attr( "title", last.getEncodedText() )
511 .addClass( "ui-li-link-alt" )
520 .find( ".ui-btn-inner" )
522 $( "<span />" ).buttonMarkup( {
527 icon : listspliticon || last.jqmData( "icon" ) || o.splitIcon
531 } else if ( item.jqmData( "role" ) === "list-divider" ) {
533 itemClass += " ui-li-divider ui-btn ui-bar-" + dividertheme;
534 item.attr( "role", "heading" );
536 //reset counter when a divider heading is encountered
542 itemClass += " ui-li-static ui-body-" + itemTheme;
546 if ( counter && itemClass.indexOf( "ui-li-divider" ) < 0 ) {
547 countParent = item.is( ".ui-li-static:first" ) ? item : item.find( ".ui-link-inherit" );
549 countParent.addClass( "ui-li-jsnumbering" )
550 .prepend( "<span class='ui-li-dec'>" + (counter++) + ". </span>" );
553 item.add( item.children( ".ui-btn-inner" ) ).addClass( itemClass );
555 self._itemApply( $list, item );
558 this._refreshCorners( create );
561 //create a string for ID/subpage url creation
562 _idStringEscape: function ( str ) {
563 return str.replace(/\W/g , "-");
567 _createSubPages: function () {
568 var parentList = this.element,
569 parentPage = parentList.closest( ".ui-page" ),
570 parentUrl = parentPage.jqmData( "url" ),
571 parentId = parentUrl || parentPage[ 0 ][ $.expando ],
572 parentListId = parentList.attr( "id" ),
574 dns = "data-" + $.mobile.ns,
576 persistentFooterID = parentPage.find( ":jqmData(role='footer')" ).jqmData( "id" ),
580 if ( typeof listCountPerPage[ parentId ] === "undefined" ) {
581 listCountPerPage[ parentId ] = -1;
584 parentListId = parentListId || ++listCountPerPage[ parentId ];
586 $( parentList.find( "li>ul, li>ol" ).toArray().reverse() ).each(function ( i ) {
589 listId = list.attr( "id" ) || parentListId + "-" + i,
590 parent = list.parent(),
592 title = nodeEls.first().getEncodedText(),//url limits to first 30 chars of text
593 id = ( parentUrl || "" ) + "&" + $.mobile.subPageUrlKey + "=" + listId,
594 theme = list.jqmData( "theme" ) || o.theme,
595 countTheme = list.jqmData( "counttheme" ) || parentList.jqmData( "counttheme" ) || o.countTheme,
599 nodeEls = $( list.prevAll().toArray().reverse() );
600 nodeEls = nodeEls.length ? nodeEls : $( "<span>" + $.trim(parent.contents()[ 0 ].nodeValue) + "</span>" );
602 //define hasSubPages for use in later removal
605 newPage = list.detach()
606 .wrap( "<div " + dns + "role='page' " + dns + "url='" + id + "' " + dns + "theme='" + theme + "' " + dns + "count-theme='" + countTheme + "'><div " + dns + "role='content'></div></div>" )
608 .before( "<div " + dns + "role='header' " + dns + "theme='" + o.headerTheme + "'><div class='ui-title'>" + title + "</div></div>" )
609 .after( persistentFooterID ? $( "<div " + dns + "role='footer' " + dns + "id='" + persistentFooterID + "'>" ) : "" )
611 .appendTo( $.mobile.pageContainer );
615 anchor = parent.find('a:first');
617 if ( !anchor.length ) {
618 anchor = $( "<a/>" ).html( nodeEls || title ).prependTo( parent.empty() );
621 anchor.attr( "href", "#" + id );
625 // on pagehide, remove any nested pages along with the parent page, as long as they aren't active
626 // and aren't embedded
628 parentPage.is( ":jqmData(external-page='true')" ) &&
629 parentPage.data( "page" ).options.domCache === false ) {
631 newRemove = function ( e, ui ) {
632 var nextPage = ui.nextPage, npURL;
635 npURL = nextPage.jqmData( "url" );
636 if ( npURL.indexOf( parentUrl + "&" + $.mobile.subPageUrlKey ) !== 0 ) {
637 self.childPages().remove();
643 // unbind the original page remove and replace with our specialized version
645 .unbind( "pagehide.remove" )
646 .bind( "pagehide.remove", newRemove);
650 // TODO sort out a better way to track sub pages of the extendable listview this is brittle
651 childPages: function () {
652 var parentUrl = this.parentPage.jqmData( "url" );
654 return $( ":jqmData(url^='" + parentUrl + "&" + $.mobile.subPageUrlKey + "')" );
658 //auto self-init widgets
659 $( document ).bind( "pagecreate create", function ( e ) {
660 $( $.tizen.extendablelist.prototype.options.initSelector, e.target ).extendablelist();