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 * Virtual List Widget for unlimited data.
28 * To support more then 1,000 items, special list widget developed.
29 * Fast initialize and light DOM tree.
30 * DB connection and works like DB cursor.
34 * data-role: virtuallistview
35 * data-template : jQuery.template ID that populate into virtual list
36 * data-row : Optional. Set number of <li> elements that are used for data handling.
38 * ID : <UL> element that has "data-role=virtuallist" must have ID attribute.
43 * itemData: function ( idx ) { return json_obj; },
44 * numItemData: number or function () { return number; },
45 * cacheItemData: function ( minIdx, maxIdx ) {}
47 * : Create a virtuallist widget. At this moment, _create method is called.
48 * args : A collection of options
49 * itemData: A function that returns JSON object for given index. Mandatory.
50 * numItemData: Total number of itemData. Mandatory.
51 * cacheItemData: Virtuallist will ask itemData between minIdx and maxIdx.
52 * Developers can implement this function for preparing data.
57 * touchstart : Temporary preventDefault applied on touchstart event to avoid broken screen.
61 * <script id="tmp-3-2-7" type="text/x-jquery-tmpl">
62 * <li class="ui-li-3-2-7">
63 * <span class="ui-li-text-main">${NAME}</span>
64 * <img src="00_winset_icon_favorite_on.png" class="ui-li-icon-sub">
65 * <span class="ui-li-text-sub">${ACTIVE}</span>
66 * <span class="ui-li-text-sub2">${FROM}</span>
70 * <ul id="virtuallist-normal_3_2_7_ul" data-role="virtuallistview" data-template="tmp-3-2-7" data-dbtable="JSON_DATA" data-row="100">
76 (function ( $, undefined ) {
78 /* Code for Virtual List Demo */
79 var listCountPerPage = {}, /* Keeps track of the number of lists per page UID. This allows support for multiple nested list in the same page. https://github.com/jquery/jquery-mobile/issues/1617 */
80 _NO_SCROLL = 0, /* ENUM */
81 _SCROLL_DOWN = 1, /* ENUM */
82 _SCROLL_UP = -1; /* ENUM */
84 $.widget( "tizen.virtuallistview", $.mobile.widget, {
93 id: "", /* Virtual list UL elemet's ID */
94 childSelector: " li", /* To support swipe list */
97 dbkey: false, /* Data's unique Key */
101 initSelector: ":jqmData(role='virtuallistview')"
104 _stylerMouseUp: function () {
105 $( this ).addClass( "ui-btn-up-s" );
106 $( this ).removeClass( "ui-btn-down-s" );
109 _stylerMouseDown: function () {
110 $( this ).addClass( "ui-btn-down-s" );
111 $( this ).removeClass( "ui-btn-up-s" );
114 _stylerMouseOver: function () {
115 $( this ).toggleClass( "ui-btn-hover-s" );
118 _stylerMouseOut: function () {
119 $( this ).toggleClass( "ui-btn-hover-s" );
120 $( this ).addClass( "ui-btn-up-s" );
121 $( this ).removeClass( "ui-btn-down-s" );
124 _pushData: function ( template ) {
125 var o = this.options,
127 myTemplate = $( "#" + template ),
128 lastIndex = ( o.row > this._numItemData ? this._numItemData : o.row ),
131 for ( i = 0; i < lastIndex; i++ ) {
132 htmlData = myTemplate.tmpl( this._itemData( i ) );
133 $( o.id ).append( $( htmlData ).attr( 'id', 'li_' + i ) );
136 /* After push data, re-style virtuallist widget */
137 $( o.id ).trigger( "create" );
140 _reposition: function ( event ) {
151 if ( $( o.id + o.childSelector ).size() > 0 ) {
152 t._title_h = $( o.id + o.childSelector + ':first' ).position().top;
153 t._line_h = $( o.id + o.childSelector + ':first' ).outerHeight();
155 t._container_w = $( o.id ).innerWidth();
157 padding = parseInt( $( o.id + o.childSelector ).css( "padding-left" ), 10 ) + parseInt( $( o.id + o.childSelector ).css( "padding-right" ), 10 );
160 $( o.id + ">" + o.childSelector )
161 .addClass( "position_absolute" )
162 .addClass( "ui-btn-up-s" )
163 .bind( "mouseup", t._stylerMouseUp )
164 .bind( "mousedown", t._stylerMouseDown )
165 .bind( "mouseover", t._stylerMouseOver )
166 .bind( "mouseout", t._stylerMouseOut );
169 $( o.id + ">" + o.childSelector ).each( function ( index ) {
170 $( this ).css( "top", t._title_h + t._line_h * index + 'px' )
171 .css( "width", t._container_w - padding );
174 /* Set Max List Height */
175 $( o.id ).height( t._numItemData * t._line_h );
178 _resize: function ( event ) {
189 t._container_w = $( o.id ).innerWidth();
191 padding = parseInt( $( o.id + o.childSelector ).css( "padding-left" ), 10 ) + parseInt( $( o.id + o.childSelector ).css( "padding-right" ), 10 );
193 $( o.id + o.childSelector ).each( function (index) {
194 $( this ).css( "width", t._container_w - padding );
198 _scrollmove: function ( event ) {
199 var t = event.data, // document
203 _replace, /* Function */
204 _moveTopBottom, /* Function */
205 _moveBottomTop, /* Function */
206 _matrixToArray, /* Function */
212 /* Text & image src replace function */
213 _replace = function ( oldItem, newItem, key ) {
218 $( oldItem ).find( ".ui-li-text-main", ".ui-li-text-sub", "ui-btn-text" ).each( function ( index ) {
220 newText = $( newItem ).find( ".ui-li-text-main", ".ui-li-text-sub", "ui-btn-text" ).eq( index ).text();
222 $( oldObj).contents().filter( function () {
223 return ( this.nodeType == 3 );
224 } ).get( 0 ).data = newText;
227 $( oldItem ).find( "img" ).each( function ( imgIndex ) {
229 newImg = $( newItem ).find( "img" ).eq( imgIndex ).attr( "src" );
231 $( oldObj ).attr( "src", newImg );
234 $( oldItem ).removeData( ); // Clear old data
237 $( oldItem ).data( key, $( newItem ).data( key ) );
241 //Move older item to bottom
242 _moveTopBottom = function ( v_firstIndex, v_lastIndex, num, key ) {
247 if (v_firstIndex < 0) {
251 for ( i = 0; i < num; i++ ) {
252 if ( v_lastIndex + i > t._numItemData ) {
256 cur_item = $( '#li_' + ( v_firstIndex + i ) );
259 /* Make New <LI> element from template. */
260 myTemplate = $( "#" + o.template );
261 htmlData = myTemplate.tmpl( t._itemData( v_lastIndex + i ) );
263 /* Copy all data to current item. */
264 _replace( cur_item, htmlData, key );
266 // Clear temporary htmlData to free cache
269 /* Set New Position */
270 ( cur_item ).css( 'top', t._title_h + t._line_h * ( v_lastIndex + 1 + i ) ).attr( 'id', 'li_' + ( v_lastIndex + 1 + i ) );
278 // Move older item to bottom
279 _moveBottomTop = function ( v_firstIndex, v_lastIndex, num, key ) {
284 if ( v_firstIndex < 0 ) {
288 for ( i = 0; i < num; i++ ) {
289 cur_item = $( '#li_' + ( v_lastIndex - i ) );
292 if ( v_firstIndex - 1 - i < 0 ) {
296 /* Make New <LI> element from template. */
297 myTemplate = $( "#" + o.template );
298 htmlData = myTemplate.tmpl( t._itemData( v_firstIndex - 1 - i ) );
300 /* Copy all data to current item. */
301 _replace( cur_item, htmlData, key );
303 // Clear temporary htmlData to free cache
306 /* Set New Position */
307 $( cur_item ).css( 'top', t._title_h + t._line_h * ( v_firstIndex - 1 - i ) ).attr( 'id', 'li_' + ( v_firstIndex - 1 - i ) );
315 /* Matrix to Array function written by Blender@stackoverflow.nnikishi@emich.edu*/
316 _matrixToArray = function ( matrix ) {
317 var contents = matrix.substr( 7 );
319 contents = contents.substr( 0, contents.length - 1 );
321 return contents.split( ', ' );
324 // Get scroll direction and velocity
325 /* with Scroll view */
326 if ( o.scrollview ) {
327 $el = $( o.id ).parentsUntil( ".ui-page" ).find( ".ui-scrollview-view" );
328 transformValue = _matrixToArray( $el.css( "-webkit-transform" ) );
329 curWindowTop = Math.abs( transformValue[ 5 ] ); /* Y vector */
331 curWindowTop = $( window ).scrollTop() - t._line_h;
334 cur_num_top_items = $( o.id + o.childSelector ).filter( function () {
335 return (parseInt( $( this ).css( "top" ), 10 ) < curWindowTop );
338 if ( t._num_top_items < cur_num_top_items ) {
339 t._direction = _SCROLL_DOWN;
340 velocity = cur_num_top_items - t._num_top_items;
341 t._num_top_items = cur_num_top_items;
342 } else if ( t._num_top_items > cur_num_top_items ) {
343 t._direction = _SCROLL_UP;
344 velocity = t._num_top_items - cur_num_top_items;
345 t._num_top_items = cur_num_top_items;
349 if ( t._direction == _SCROLL_DOWN ) {
350 if ( cur_num_top_items > o.page_buf ) {
351 if ( t._last_index + velocity > t._numItemData ) {
352 velocity = t._numItemData - t._last_index - 1;
355 /* Prevent scroll touch event while DOM access */
356 $(document).bind( "touchstart.virtuallist", function (event) {
357 event.preventDefault();
360 _moveTopBottom( t._first_index, t._last_index, velocity, o.dbkey );
362 t._first_index += velocity;
363 t._last_index += velocity;
364 t._num_top_items -= velocity;
366 /* Unset prevent touch event */
367 $( document ).unbind( "touchstart.virtuallist" );
369 } else if ( t._direction == _SCROLL_UP ) {
370 if ( cur_num_top_items <= o.page_buf ) {
371 if ( t._first_index < velocity ) {
372 velocity = t._first_index;
375 /* Prevent scroll touch event while DOM access */
376 $( document ).bind( "touchstart.virtuallist", function ( event ) {
377 event.preventDefault();
380 _moveBottomTop( t._first_index, t._last_index, velocity, o.dbkey );
382 t._first_index -= velocity;
383 t._last_index -= velocity;
384 t._num_top_items += velocity;
386 /* Unset prevent touch event */
387 $( document ).unbind( "touchstart.virtuallist" );
390 if ( t._first_index < o.page_buf ) {
391 t._num_top_items = t._first_index;
396 _recreate: function ( newArray ) {
402 t._numItemData = newArray.length;
403 t._direction = _NO_SCROLL;
405 t._last_index = o.row - 1;
407 t._pushData( o.template );
409 if (o.childSelector == " ul" ) {
410 $( o.id + " ul" ).swipelist();
413 $( o.id ).virtuallistview();
420 _initList: function () {
424 /* After AJAX loading success */
426 /* Make Gen list by template */
427 t._pushData( o.template );
429 $( o.id ).parentsUntil( ".ui-page" ).parent().one( "pageshow", function () {
430 setTimeout( function () {
436 $( document ).bind( "scrollstop.virtuallist", t, t._scrollmove );
438 $( window ).bind( "resize.virtuallist", t._resize );
440 if ( o.childSelector == " ul" ) {
441 $( o.id + " ul" ).swipelist();
447 create: function () {
448 var o = this.options;
450 /* external API for AJAX callback */
451 this._create.apply( this, arguments );
453 this._reposition( o );
456 _create: function ( args ) {
457 // Extend required vars
459 _itemData : function ( idx ) { return null; },
461 _cacheItemData : function ( minIdx, maxIdx ) { },
466 _direction : _NO_SCROLL,
469 _num_top_items : 0 // By scroll move, number of hidden elements.
475 shortcutsContainer = $('<div class="ui-virtuallist"/>'),
476 shortcutsList = $('<ul></ul>'),
477 dividers = $el.find(':jqmData(role="virtuallistview" )'),
479 shortcutscroll = this,
484 // create listview markup
485 t.element.addClass( function ( i, orig ) {
486 return orig + " ui-listview ui-virtual-list-container" + ( t.options.inset ? " ui-listview-inset ui-corner-all ui-shadow " : "" );
489 o.id = "#" + $el.attr( "id" );
491 $( o.id ).bind( "pagehide", function ( e ) {
496 if ( $( ".ui-scrollview-clip" ).size() > 0 ) {
499 o.scrollview = false;
502 /* Init list and page buf */
503 if ( $el.data( "row" ) ) {
504 o.row = $el.data( "row" );
506 if ( o.row < t._minimum_row ) {
507 o.row = t._minimum_row;
510 o.page_buf = parseInt( ( o.row / 2 ), 10 );
515 if ( args.itemData && typeof args.itemData == 'function' ) {
516 t._itemData = args.itemData;
520 if ( args.numItemData ) {
521 if ( typeof args.numItemData == 'function' ) {
522 t._numItemData = args.numItemData( );
523 } else if ( typeof args.numItemData == 'number' ) {
524 t._numItemData = args.numItemData;
531 } else { // No option is given
532 // Legacy support: dbtable
533 console.log("WARNING: The data interface of virtuallist is changed. \nOld data interface(data-dbtable) is still supported, but will be removed in next version. \nPlease fix your code soon!");
535 /* After DB Load complete, Init Vritual list */
536 if ( $( o.id ).hasClass( "vlLoadSuccess" ) ) {
537 dbtable_name = $el.jqmData('dbtable');
538 dbtable = window[ dbtable_name ];
546 if ( $el.data( "template" ) ) {
547 o.template = $el.data( "template" );
549 /* to support swipe list, <li> or <ul> can be main node of virtual list. */
550 if ( $el.data( "swipelist" ) == true ) {
551 o.childSelector = " ul";
553 o.childSelector = " li";
557 /* Set data's unique key */
558 if ( $el.data( "dbkey" ) ) {
559 o.dbkey = $el.data( "dbkey" );
562 t._first_index = 0; //first id of <li> element.
563 t._last_index = o.row - 1; //last id of <li> element.
565 t._itemData = function ( idx ) {
566 return dbtable[ idx ];
568 t._numItemData = dbtable.length;
576 destroy : function () {
577 var o = this.options;
579 $( document ).unbind( "scrollstop" );
581 $( window ).unbind( "resize.virtuallist" );
586 _itemApply: function ( $list, item ) {
587 var $countli = item.find( ".ui-li-count" );
589 if ( $countli.length ) {
590 item.addClass( "ui-li-has-count" );
593 $countli.addClass( "ui-btn-up-" + ( $list.jqmData( "counttheme" ) || this.options.countTheme ) + " ui-btn-corner-all" );
595 // TODO class has to be defined in markup
596 item.find( "h1, h2, h3, h4, h5, h6" ).addClass( "ui-li-heading" ).end()
597 .find( "p, dl" ).addClass( "ui-li-desc" ).end()
598 .find( ">img:eq(0), .ui-link-inherit>img:eq(0)" ).addClass( "ui-li-thumb" ).each( function () {
599 item.addClass( $( this ).is( ".ui-li-icon" ) ? "ui-li-has-icon" : "ui-li-has-thumb" );
601 .find( ".ui-li-aside" ).each(function () {
602 var $this = $( this );
603 $this.prependTo( $this.parent() ); //shift aside to front for css float
607 _removeCorners: function ( li, which ) {
608 var top = "ui-corner-top ui-corner-tr ui-corner-tl",
609 bot = "ui-corner-bottom ui-corner-br ui-corner-bl";
611 li = li.add( li.find( ".ui-btn-inner, .ui-li-link-alt, .ui-li-thumb" ) );
613 if ( which === "top" ) {
614 li.removeClass( top );
615 } else if ( which === "bottom" ) {
616 li.removeClass( bot );
618 li.removeClass( top + " " + bot );
622 _refreshCorners: function ( create ) {
628 if ( this.options.inset ) {
629 $li = this.element.children( "li" );
630 // at create time the li are not visible yet so we need to rely on .ui-screen-hidden
631 $visibleli = create ? $li.not( ".ui-screen-hidden" ) : $li.filter( ":visible" );
633 this._removeCorners( $li );
635 // Select the first visible li element
636 $topli = $visibleli.first()
637 .addClass( "ui-corner-top" );
639 $topli.add( $topli.find( ".ui-btn-inner" ) )
640 .find( ".ui-li-link-alt" )
641 .addClass( "ui-corner-tr" )
643 .find( ".ui-li-thumb" )
644 .not( ".ui-li-icon" )
645 .addClass( "ui-corner-tl" );
647 // Select the last visible li element
648 $bottomli = $visibleli.last()
649 .addClass( "ui-corner-bottom" );
651 $bottomli.add( $bottomli.find( ".ui-btn-inner" ) )
652 .find( ".ui-li-link-alt" )
653 .addClass( "ui-corner-br" )
655 .find( ".ui-li-thumb" )
656 .not( ".ui-li-icon" )
657 .addClass( "ui-corner-bl" );
661 refresh: function ( create ) {
662 this.parentPage = this.element.closest( ".ui-page" );
663 this._createSubPages();
665 var o = this.options,
666 $list = this.element,
668 dividertheme = $list.jqmData( "dividertheme" ) || o.dividerTheme,
669 listsplittheme = $list.jqmData( "splittheme" ),
670 listspliticon = $list.jqmData( "spliticon" ),
671 li = $list.children( "li" ),
672 counter = $.support.cssPseudoElement || !$.nodeName( $list[ 0 ], "ol" ) ? 0 : 1,
686 $list.find( ".ui-li-dec" ).remove();
689 for ( pos = 0, numli = li.length; pos < numli; pos++ ) {
693 // If we're creating the element, we update it regardless
694 if ( create || !item.hasClass( "ui-li" ) ) {
695 itemTheme = item.jqmData( "theme" ) || o.theme;
696 a = item.children( "a" );
699 icon = item.jqmData( "icon" );
706 /* icon: a.length > 1 || icon === false ? false : icon || "arrow-r",*/
707 icon: false, /* Remove unnecessary arrow icon */
711 if ( ( icon != false ) && ( a.length == 1 ) ) {
712 item.addClass( "ui-li-has-arrow" );
715 a.first().addClass( "ui-link-inherit" );
717 if ( a.length > 1 ) {
718 itemClass += " ui-li-has-alt";
721 splittheme = listsplittheme || last.jqmData( "theme" ) || o.splitTheme;
724 .attr( "title", last.getEncodedText() )
725 .addClass( "ui-li-link-alt" )
734 .find( ".ui-btn-inner" )
736 $( "<span />" ).buttonMarkup({
741 icon: listspliticon || last.jqmData( "icon" ) || o.splitIcon
745 } else if ( item.jqmData( "role" ) === "list-divider" ) {
747 itemClass += " ui-li-divider ui-btn ui-bar-" + dividertheme;
748 item.attr( "role", "heading" );
750 //reset counter when a divider heading is encountered
756 itemClass += " ui-li-static ui-body-" + itemTheme;
760 if ( counter && itemClass.indexOf( "ui-li-divider" ) < 0 ) {
761 countParent = item.is( ".ui-li-static:first" ) ? item : item.find( ".ui-link-inherit" );
763 countParent.addClass( "ui-li-jsnumbering" )
764 .prepend( "<span class='ui-li-dec'>" + (counter++) + ". </span>" );
767 item.add( item.children( ".ui-btn-inner" ) ).addClass( itemClass );
769 self._itemApply( $list, item );
772 this._refreshCorners( create );
775 //create a string for ID/subpage url creation
776 _idStringEscape: function ( str ) {
777 return str.replace(/\W/g , "-");
780 _createSubPages: function () {
781 var parentList = this.element,
782 parentPage = parentList.closest( ".ui-page" ),
783 parentUrl = parentPage.jqmData( "url" ),
784 parentId = parentUrl || parentPage[ 0 ][ $.expando ],
785 parentListId = parentList.attr( "id" ),
787 dns = "data-" + $.mobile.ns,
789 persistentFooterID = parentPage.find( ":jqmData(role='footer')" ).jqmData( "id" ),
793 if ( typeof listCountPerPage[ parentId ] === "undefined" ) {
794 listCountPerPage[ parentId ] = -1;
797 parentListId = parentListId || ++listCountPerPage[ parentId ];
799 $( parentList.find( "li>ul, li>ol" ).toArray().reverse() ).each(function ( i ) {
802 listId = list.attr( "id" ) || parentListId + "-" + i,
803 parent = list.parent(),
805 title = nodeEls.first().getEncodedText(),//url limits to first 30 chars of text
806 id = ( parentUrl || "" ) + "&" + $.mobile.subPageUrlKey + "=" + listId,
807 theme = list.jqmData( "theme" ) || o.theme,
808 countTheme = list.jqmData( "counttheme" ) || parentList.jqmData( "counttheme" ) || o.countTheme,
812 nodeEls = $( list.prevAll().toArray().reverse() );
813 nodeEls = nodeEls.length ? nodeEls : $( "<span>" + $.trim( parent.contents()[ 0 ].nodeValue ) + "</span>" );
815 //define hasSubPages for use in later removal
818 newPage = list.detach()
819 .wrap( "<div " + dns + "role='page' " + dns + "url='" + id + "' " + dns + "theme='" + theme + "' " + dns + "count-theme='" + countTheme + "'><div " + dns + "role='content'></div></div>" )
821 .before( "<div " + dns + "role='header' " + dns + "theme='" + o.headerTheme + "'><div class='ui-title'>" + title + "</div></div>" )
822 .after( persistentFooterID ? $( "<div " + dns + "role='footer' " + dns + "id='" + persistentFooterID + "'>" ) : "" )
824 .appendTo( $.mobile.pageContainer );
828 anchor = parent.find('a:first');
830 if ( !anchor.length ) {
831 anchor = $( "<a/>" ).html( nodeEls || title ).prependTo( parent.empty() );
834 anchor.attr( "href", "#" + id );
836 }).virtuallistview();
838 // on pagehide, remove any nested pages along with the parent page, as long as they aren't active
839 // and aren't embedded
841 parentPage.is( ":jqmData(external-page='true')" ) &&
842 parentPage.data( "page" ).options.domCache === false ) {
844 newRemove = function ( e, ui ) {
845 var nextPage = ui.nextPage, npURL;
848 npURL = nextPage.jqmData( "url" );
849 if ( npURL.indexOf( parentUrl + "&" + $.mobile.subPageUrlKey ) !== 0 ) {
850 self.childPages().remove();
856 // unbind the original page remove and replace with our specialized version
858 .unbind( "pagehide.remove" )
859 .bind( "pagehide.remove", newRemove );
863 // TODO sort out a better way to track sub pages of the virtuallistview this is brittle
864 childPages: function () {
865 var parentUrl = this.parentPage.jqmData( "url" );
867 return $( ":jqmData(url^='" + parentUrl + "&" + $.mobile.subPageUrlKey + "')" );
871 //auto self-init widgets
872 $( document ).bind( "pagecreate create", function ( e ) {
873 $( $.tizen.virtuallistview.prototype.options.initSelector, e.target ).virtuallistview();