1 //>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
2 //>>description: Listview which can be extended more and more
3 //>>label: Extendable list
4 //>>group: Tizen:Widgets
6 define( [ '../jquery.mobile.tizen.scrollview' ], function ( ) {
7 //>>excludeEnd("jqmBuildExclude");
9 /****************************************************************************
10 * Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd.
12 * Permission is hereby granted, free of charge, to any person obtaining a
13 * copy of this software and associated documentation files (the "Software"),
14 * to deal in the Software without restriction, including without limitation
15 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
16 * and/or sell copies of the Software, and to permit persons to whom the
17 * Software is furnished to do so, subject to the following conditions:
19 * The above copyright notice and this permission notice shall be included in
20 * all copies or substantial portions of the Software.
22 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
27 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
28 * DEALINGS IN THE SOFTWARE.
29 * ***************************************************************************
31 * Author: Wongi Lee <wongi11.lee@samsung.com>
35 * Extendable List Widget for unlimited data.
36 * To support more then 1,000 items, special list widget developed.
37 * Fast initialize and append some element into the DOM tree repeatedly.
38 * DB connection and works like DB cursor.
42 * data-role: extendablelist
43 * 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.
44 * data-dbtable : DB Table name. It used as window[DB NAME]. Loaded data should be converted as window object.
45 * data-extenditems : Number of elements to extend at once.
47 * ID : <UL> element that has "data-role=extendablelist" must have ID attribute.
48 * Class : <UL> element that has "data-role=extendablelist" should have "vlLoadSuccess" class to guaranty DB loading is completed.
49 * tmp_load_more : Template ID for "load more" message and button.
54 * itemData: function ( idx ) { return json_obj; },
55 * numItemData: number or function () { return number; },
56 * cacheItemData: function ( minIdx, maxIdx ) {}
58 * : Create a extendable list widget. At this moment, _create method is called.
59 * args : A collection of options
60 * itemData: A function that returns JSON object for given index. Mandatory.
61 * numItemData: Total number of itemData. Mandatory.
62 * cacheItemData: Extendable list will ask itemData between minIdx and maxIdx.
63 * Developers can implement this function for preparing data.
68 * <script id="tmp-3-1-1" type="text/x-jquery-tmpl">
69 * <li class="ui-li-3-1-1"><span class="ui-li-text-main">${NAME}</span></li>
72 * <script id="tmp_load_more" type="text/x-jquery-tmpl">
73 * <li class="ui-li-3-1-1" style="text-align:center; margin:0 auto">
74 * <div data-role="button">Load ${NUM_MORE_ITEMS} more items</div>
78 * <ul id = "extendable_list_main" data-role="extendablelist" data-extenditems="50" data-template="tmp-3-1-1">
85 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.
86 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/>
87 To add a extendable list widget to the application, use the following code:
89 <script id="tmp-3-1-1" type="text/x-jquery-tmpl">
90 <li class="ui-li-3-1-1"><span class="ui-li-text-main">${NAME}</span></li>
92 <script id="tmp_load_more" type="text/x-jquery-tmpl">
93 <li class="ui-li-3-1-1" style="text-align:center; margin:0 auto">
94 <div data-role="button">Load ${NUM_MORE_ITEMS} more items</div>
97 <ul id="extendable_list_main" data-role="extendablelist" data-extenditems="50" data-template="tmp-3-1-1">
101 @property {String} data-role
102 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.
105 @property {String} data-template
106 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.
109 @property {Integer} data-extenditems
110 Defines the number of data elements to be extended at a time.
112 ( function ( $, undefined ) {
114 //Keeps track of the number of lists per page UID
115 //This allows support for multiple nested list in the same page
116 //https://github.com/jquery/jquery-mobile/issues/1617
117 var listCountPerPage = {};
119 $.widget( "tizen.extendablelist", $.mobile.widget, {
125 splitIcon: "arrow-r",
128 id: "", /* Extendable list UL elemet's ID */
129 extenditems: 50, /* Number of append items */
130 childSelector: " li", /* To support swipe list */
132 template : "", /* Template for each list item */
133 loadmore : "tmp_load_more", /* Template for "Load more" message */
135 initSelector: ":jqmData(role='extendablelist')"
138 _stylerMouseUp: function () {
139 $( this ).addClass( "ui-btn-up-s" );
140 $( this ).removeClass( "ui-btn-down-s" );
143 _stylerMouseDown: function () {
144 $( this ).addClass( "ui-btn-down-s" );
145 $( this ).removeClass( "ui-btn-up-s" );
148 _stylerMouseOver: function () {
149 $( this ).toggleClass( "ui-btn-hover-s" );
152 _stylerMouseOut: function () {
153 $( this ).toggleClass( "ui-btn-hover-s" );
154 $( this ).addClass( "ui-btn-up-s" );
155 $( this ).removeClass( "ui-btn-down-s" );
158 _pushData: function ( template ) {
159 var o = this.options,
162 myTemplate = $( "#" + template ),
163 loadMoreItems = ( o.extenditems > t._numItemData - t._lastIndex ? t._numItemData - t.lastIndex : o.extenditems ),
166 for (i = 0; i < loadMoreItems; i++ ) {
167 htmlData = myTemplate.tmpl( t._itemData( i ) );
168 $( o.id ).append( $( htmlData ).attr( 'id', 'li_' + i ) );
171 $( o.id + ">" + o.childSelector )
172 .addClass( "ui-btn-up-s" )
173 .bind( "mouseup", t._stylerMouseUp )
174 .bind( "mousedown", t._stylerMouseDown )
175 .bind( "mouseover", t._stylerMouseOver )
176 .bind( "mouseout", t._stylerMouseOut );
181 /* After push data, re-style extendable list widget */
182 $( o.id ).trigger( "create" );
185 _loadmore: function ( event ) {
186 var t = event.data, // <li> element
189 myTemplate = $( "#" + o.template ),
190 loadMoreItems = ( o.extenditems > t._numItemData - t._lastIndex ? t._numItemData - t._lastIndex : o.extenditems ),
195 /* Remove load more message */
196 $( "#load_more_message" ).remove();
198 /* Append More Items */
199 for ( i = 0; i < loadMoreItems; i++ ) {
200 htmlData = myTemplate.tmpl( t._itemData( t._lastIndex ) );
201 $( o.id ).append( $( htmlData ).attr( 'id', 'li_' + t._lastIndex ) );
205 /* Append "Load more" message on the last of list */
206 if ( t._numItemData > t._lastIndex ) {
207 myTemplate = $( "#" + o.loadmore );
208 more_items_to_load = t._numItemData - t._lastIndex;
209 num_next_load_items = ( o.extenditems <= more_items_to_load ) ? o.extenditems : more_items_to_load;
210 htmlData = myTemplate.tmpl( { NUM_MORE_ITEMS : num_next_load_items } );
212 $( o.id ).append( $( htmlData ).attr( 'id', "load_more_message" ) );
215 $( o.id ).trigger( "create" );
216 $( o.id ).extendablelist( "refresh" );
219 recreate: function ( newArray ) {
221 itemData: function ( idx ) { return newArray[ idx ]; },
222 numItemData: newArray.length
226 _initList: function (args ) {
234 /* Make Gen list by template */
235 if ( t._lastIndex <= 0 ) {
236 t._pushData( o.template );
238 /* Append "Load more" message on the last of list */
239 if ( t._numItemData > t._lastIndex ) {
240 myTemplate = $( "#" + o.loadmore );
241 more_items_to_load = t._numItemData - t._lastIndex;
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, 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.apply( this, arguments );
271 _create: function ( args ) {
281 _itemData: function ( idx ) { return null; },
283 _cacheItemData: function ( minIdx, maxIdx ) { },
288 // create listview markup
289 t.element.addClass( function ( i, orig ) {
290 return orig + " ui-listview ui-extendable-list-container" + ( t.options.inset ? " ui-listview-inset ui-corner-all ui-shadow " : "" );
293 o.id = "#" + $el.attr( "id" );
295 if ( $el.data( "extenditems" ) ) {
296 o.extenditems = parseInt( $el.data( "extenditems" ), 10 );
299 $( o.id ).bind( "pagehide", function (e) {
304 if ( $( ".ui-scrollview-clip" ).size() > 0) {
307 o.scrollview = false;
311 if ( !t._loadData( args ) ) {
315 // Legacy support: dbtable
316 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!");
318 if ( $( o.id ).hasClass( "elLoadSuccess" ) ) {
319 dbtable_name = $el.jqmData('dbtable');
320 o.dbtable = window[ dbtable_name ];
321 if ( !(o.dbtable) ) {
324 t._itemData = function ( idx ) {
325 return o.dbtable[ idx ];
327 t._numItemData = o.dbtable.length;
330 console.warn("No elLoadSuccess class");
335 if ( $el.data( "template" ) ) {
336 o.template = $el.data( "template" );
338 /* to support swipe list, <li> or <ul> can be main node of extendable list. */
339 if ( $el.data( "swipelist" ) == true ) {
340 o.childSelector = " ul";
342 o.shildSelector = " li";
348 _loadData : function ( args ) {
351 if ( args.itemData && typeof args.itemData == 'function' ) {
352 self._itemData = args.itemData;
356 if ( args.numItemData ) {
357 if ( typeof args.numItemData == 'function' ) {
358 self._numItemData = args.numItemData( );
359 } else if ( typeof args.numItemData == 'number' ) {
360 self._numItemData = args.numItemData;
371 destroy : function () {
372 var o = this.options,
378 $( "#load_more_message" ).die();
381 _itemApply: function ( $list, item ) {
382 var $countli = item.find( ".ui-li-count" );
384 if ( $countli.length ) {
385 item.addClass( "ui-li-has-count" );
388 $countli.addClass( "ui-btn-up-" + ( $list.jqmData( "counttheme" ) || this.options.countTheme ) + " ui-btn-corner-all" );
390 // TODO class has to be defined in markup
391 item.find( "h1, h2, h3, h4, h5, h6" ).addClass( "ui-li-heading" ).end()
392 .find( "p, dl" ).addClass( "ui-li-desc" ).end()
393 .find( ">img:eq(0), .ui-link-inherit>img:eq(0)" ).addClass( "ui-li-thumb" ).each(function () {
394 item.addClass( $( this ).is( ".ui-li-icon" ) ? "ui-li-has-icon" : "ui-li-has-thumb" );
396 .find( ".ui-li-aside" ).each(function () {
397 var $this = $( this );
398 $this.prependTo( $this.parent() ); //shift aside to front for css float
402 _removeCorners: function ( li, which ) {
403 var top = "ui-corner-top ui-corner-tr ui-corner-tl",
404 bot = "ui-corner-bottom ui-corner-br ui-corner-bl";
406 li = li.add( li.find( ".ui-btn-inner, .ui-li-link-alt, .ui-li-thumb" ) );
408 if ( which === "top" ) {
409 li.removeClass( top );
410 } else if ( which === "bottom" ) {
411 li.removeClass( bot );
413 li.removeClass( top + " " + bot );
417 _refreshCorners: function ( create ) {
423 if ( this.options.inset ) {
424 $li = this.element.children( "li" );
425 // at create time the li are not visible yet so we need to rely on .ui-screen-hidden
426 $visibleli = create ? $li.not( ".ui-screen-hidden" ) : $li.filter( ":visible" );
428 this._removeCorners( $li );
430 // Select the first visible li element
431 $topli = $visibleli.first()
432 .addClass( "ui-corner-top" );
434 $topli.add( $topli.find( ".ui-btn-inner" ) )
435 .find( ".ui-li-link-alt" )
436 .addClass( "ui-corner-tr" )
438 .find( ".ui-li-thumb" )
439 .not( ".ui-li-icon" )
440 .addClass( "ui-corner-tl" );
442 // Select the last visible li element
443 $bottomli = $visibleli.last()
444 .addClass( "ui-corner-bottom" );
446 $bottomli.add( $bottomli.find( ".ui-btn-inner" ) )
447 .find( ".ui-li-link-alt" )
448 .addClass( "ui-corner-br" )
450 .find( ".ui-li-thumb" )
451 .not( ".ui-li-icon" )
452 .addClass( "ui-corner-bl" );
456 refresh: function ( create ) {
457 this.parentPage = this.element.closest( ".ui-page" );
458 this._createSubPages();
460 var o = this.options,
461 $list = this.element,
463 dividertheme = $list.jqmData( "dividertheme" ) || o.dividerTheme,
464 listsplittheme = $list.jqmData( "splittheme" ),
465 listspliticon = $list.jqmData( "spliticon" ),
466 li = $list.children( "li" ),
467 counter = $.support.cssPseudoElement || !$.nodeName( $list[ 0 ], "ol" ) ? 0 : 1,
480 $list.find( ".ui-li-dec" ).remove();
483 for ( pos = 0, numli = li.length; pos < numli; pos++ ) {
487 // If we're creating the element, we update it regardless
488 if ( create || !item.hasClass( "ui-li" ) ) {
489 itemTheme = item.jqmData( "theme" ) || o.theme;
490 a = item.children( "a" );
493 icon = item.jqmData( "icon" );
500 /* icon: a.length > 1 || icon === false ? false : icon || "arrow-r",*/
501 icon: false, /* Remove unnecessary arrow icon */
505 if ( ( icon != false ) && ( a.length == 1 ) ) {
506 item.addClass( "ui-li-has-arrow" );
509 a.first().addClass( "ui-link-inherit" );
511 if ( a.length > 1 ) {
512 itemClass += " ui-li-has-alt";
515 splittheme = listsplittheme || last.jqmData( "theme" ) || o.splitTheme;
518 .attr( "title", last.getEncodedText() )
519 .addClass( "ui-li-link-alt" )
528 .find( ".ui-btn-inner" )
530 $( "<span />" ).buttonMarkup( {
535 icon : listspliticon || last.jqmData( "icon" ) || o.splitIcon
539 } else if ( item.jqmData( "role" ) === "list-divider" ) {
541 itemClass += " ui-li-divider ui-btn ui-bar-" + dividertheme;
542 item.attr( "role", "heading" );
544 //reset counter when a divider heading is encountered
550 itemClass += " ui-li-static ui-body-" + itemTheme;
554 if ( counter && itemClass.indexOf( "ui-li-divider" ) < 0 ) {
555 countParent = item.is( ".ui-li-static:first" ) ? item : item.find( ".ui-link-inherit" );
557 countParent.addClass( "ui-li-jsnumbering" )
558 .prepend( "<span class='ui-li-dec'>" + (counter++) + ". </span>" );
561 item.add( item.children( ".ui-btn-inner" ) ).addClass( itemClass );
563 self._itemApply( $list, item );
566 this._refreshCorners( create );
569 //create a string for ID/subpage url creation
570 _idStringEscape: function ( str ) {
571 return str.replace(/\W/g , "-");
575 _createSubPages: function () {
576 var parentList = this.element,
577 parentPage = parentList.closest( ".ui-page" ),
578 parentUrl = parentPage.jqmData( "url" ),
579 parentId = parentUrl || parentPage[ 0 ][ $.expando ],
580 parentListId = parentList.attr( "id" ),
582 dns = "data-" + $.mobile.ns,
584 persistentFooterID = parentPage.find( ":jqmData(role='footer')" ).jqmData( "id" ),
588 if ( typeof listCountPerPage[ parentId ] === "undefined" ) {
589 listCountPerPage[ parentId ] = -1;
592 parentListId = parentListId || ++listCountPerPage[ parentId ];
594 $( parentList.find( "li>ul, li>ol" ).toArray().reverse() ).each(function ( i ) {
597 listId = list.attr( "id" ) || parentListId + "-" + i,
598 parent = list.parent(),
600 title = nodeEls.first().getEncodedText(),//url limits to first 30 chars of text
601 id = ( parentUrl || "" ) + "&" + $.mobile.subPageUrlKey + "=" + listId,
602 theme = list.jqmData( "theme" ) || o.theme,
603 countTheme = list.jqmData( "counttheme" ) || parentList.jqmData( "counttheme" ) || o.countTheme,
607 nodeEls = $( list.prevAll().toArray().reverse() );
608 nodeEls = nodeEls.length ? nodeEls : $( "<span>" + $.trim(parent.contents()[ 0 ].nodeValue) + "</span>" );
610 //define hasSubPages for use in later removal
613 newPage = list.detach()
614 .wrap( "<div " + dns + "role='page' " + dns + "url='" + id + "' " + dns + "theme='" + theme + "' " + dns + "count-theme='" + countTheme + "'><div " + dns + "role='content'></div></div>" )
616 .before( "<div " + dns + "role='header' " + dns + "theme='" + o.headerTheme + "'><div class='ui-title'>" + title + "</div></div>" )
617 .after( persistentFooterID ? $( "<div " + dns + "role='footer' " + dns + "id='" + persistentFooterID + "'>" ) : "" )
619 .appendTo( $.mobile.pageContainer );
623 anchor = parent.find('a:first');
625 if ( !anchor.length ) {
626 anchor = $( "<a/>" ).html( nodeEls || title ).prependTo( parent.empty() );
629 anchor.attr( "href", "#" + id );
633 // on pagehide, remove any nested pages along with the parent page, as long as they aren't active
634 // and aren't embedded
636 parentPage.is( ":jqmData(external-page='true')" ) &&
637 parentPage.data( "page" ).options.domCache === false ) {
639 newRemove = function ( e, ui ) {
640 var nextPage = ui.nextPage, npURL;
643 npURL = nextPage.jqmData( "url" );
644 if ( npURL.indexOf( parentUrl + "&" + $.mobile.subPageUrlKey ) !== 0 ) {
645 self.childPages().remove();
651 // unbind the original page remove and replace with our specialized version
653 .unbind( "pagehide.remove" )
654 .bind( "pagehide.remove", newRemove);
658 // TODO sort out a better way to track sub pages of the extendable listview this is brittle
659 childPages: function () {
660 var parentUrl = this.parentPage.jqmData( "url" );
662 return $( ":jqmData(url^='" + parentUrl + "&" + $.mobile.subPageUrlKey + "')" );
666 //auto self-init widgets
667 $( document ).bind( "pagecreate create", function ( e ) {
668 $( $.tizen.extendablelist.prototype.options.initSelector, e.target ).extendablelist();
673 //>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
675 //>>excludeEnd("jqmBuildExclude");