blob: 939cac0cb4ffaf51e02f64901958e7863d01f28e [file] [log] [blame]
//>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
//>>description: Shows list index and scroll to the index directly
//>>label: Fastscroll
//>>group: Tizen:Widgets
define( [
'jquery',
'../jquery.mobile.tizen.scrollview'
], function ( jQuery ) {
//>>excludeEnd("jqmBuildExclude");
/*
* jQuery Mobile Widget @VERSION
*
* This software is licensed under the MIT licence (as defined by the OSI at
* http://www.opensource.org/licenses/mit-license.php)
*
* ***************************************************************************
* Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd.
* Copyright (c) 2011 by Intel Corporation Ltd.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
* ***************************************************************************
*
* Authors: Elliot Smith <elliot.smith@intel.com>
* Yonghwi Park <yonghwi0324.park@samsung.com>
*/
// fastscroll is a scrollview controller, which binds
// a scrollview to a a list of short cuts; the shortcuts are built
// from the text on dividers in the list. Clicking on a shortcut
// instantaneously jumps the scrollview to the selected list divider;
// mouse movements on the shortcut column move the scrollview to the
// list divider matching the text currently under the touch; a popup
// with the text currently under the touch is also displayed.
//
// To apply, add the attribute data-fastscroll="true" to a listview
// (a <ul> or <ol> element inside a page). Alternatively, call
// fastscroll() on an element.
//
// The closest element with class ui-scrollview-clip is used as the
// scrollview to be controlled.
//
// If a listview has no dividers or a single divider, the widget won't
// display.
/**
@class fastscroll
The shortcut scroll widget shows a shortcut list that is bound to its parent scroll bar and respective list view. This widget is displayed as a text pop-up representing shortcuts to different list dividers in the list view. If you select a shortcut text from the shortcut scroll, the parent list view is moved to the location representing the selected shortcut.
To add a shortcut scroll widget to the application, use the following code:
<div class="content" data-role="content" data-scroll="y">
<ul id="contacts" data-role="listview" data-fastscroll="true">
<li>Anton</li>
</ul>
</div>
For the shortcut scroll widget to be visible, the parent list view must have multiple list dividers.
*/
/**
@property {Boolean} data-fastscroll
When set to true, creates a shortcut scroll using the HTML unordered list (&lt;ul&gt;) element.
*/
/**
@method fastscroll
The shortcut scroll is created for the closest list view with the ui-scrollview-clip class.
*/
/**
@method indexString
The indexString method is used to get (if no value is defined) or set the string to present the index.
<div class="content" data-role="content" data-scroll="y">
ul id="contacts" data-role="listview" data-fastscroll="true">
<li data-role="list-divider">A</li>
<li>Anton</li>
</ul>
</div>
$(".selector").fastscroll( "indexString" [, indexAlphabet] );
*/
(function ( $, undefined ) {
$.widget( "tizen.fastscroll", $.mobile.widget, {
options: {
initSelector: ":jqmData(fastscroll)"
},
_primaryLanguage: null,
_secondLanguage: null,
_dividerMap: {},
_charSet: null,
_create: function () {
var $el = this.element,
self = this,
$popup,
page = $el.closest( ':jqmData(role="page")' ),
jumpToDivider;
this.scrollview = $el.addClass( 'ui-fastscroll-target' ).closest( '.ui-scrollview-clip' );
this.shortcutsContainer = $( '<div class="ui-fastscroll" aria-label="Fast scroll bar, double tap to fast scroll mode" tabindex="0"/>' );
this.shortcutsList = $( '<ul aria-hidden="true"></ul>' );
// popup for the hovering character
this.scrollview.append($( '<div class="ui-fastscroll-popup"></div>' ) );
$popup = this.scrollview.find( '.ui-fastscroll-popup' );
this.shortcutsContainer.append( this.shortcutsList );
this.scrollview.append( this.shortcutsContainer );
// find the bottom of the last item in the listview
this.lastListItem = $el.children().last();
// remove scrollbars from scrollview
this.scrollview.find( '.ui-scrollbar' ).hide();
this.jumpToDivider = function ( divider ) {
// get the vertical position of the divider (so we can scroll to it)
var dividerY = $( divider ).position().top,
// find the bottom of the last list item
bottomOffset = self.lastListItem.outerHeight( true ) + self.lastListItem.position().top,
scrollviewHeight = self.scrollview.height(),
// check that after the candidate scroll, the bottom of the
// last item will still be at the bottom of the scroll view
// and not some way up the page
maxScroll = bottomOffset - scrollviewHeight,
dstOffset;
dividerY = ( dividerY > maxScroll ? maxScroll : dividerY );
// don't apply a negative scroll, as this means the
// divider should already be visible
dividerY = Math.max( dividerY, 0 );
// apply the scroll
self.scrollview.scrollview( 'scrollTo', 0, -dividerY );
dstOffset = self.scrollview.offset();
};
this.shortcutsList
// bind mouse over so it moves the scroller to the divider
.bind( 'touchstart mousedown vmousedown touchmove vmousemove vmouseover', function ( e ) {
// Get coords relative to the element
var coords = $.mobile.tizen.targetRelativeCoordsFromEvent( e ),
shortcutsListOffset = self.shortcutsList.offset();
self.shortcutsContainer.addClass( "ui-fastscroll-hover" );
// If the element is a list item, get coordinates relative to the shortcuts list
if ( e.target.tagName.toLowerCase() === "li" ) {
coords.x += $( e.target ).offset().left - shortcutsListOffset.left;
coords.y += $( e.target ).offset().top - shortcutsListOffset.top;
}
if ( e.target.tagName.toLowerCase() === "span" ) {
coords.x += $( e.target ).parent().offset().left - shortcutsListOffset.left;
coords.y += $( e.target ).parent().offset().top - shortcutsListOffset.top;
}
self.shortcutsList.find( 'li' ).each( function () {
var listItem = $( this );
$( listItem )
.removeClass( "ui-fastscroll-hover" )
.removeClass( "ui-fastscroll-hover-down" );
});
// Hit test each list item
self.shortcutsList.find( 'li' ).each( function () {
var listItem = $( this ),
l = listItem.offset().left - shortcutsListOffset.left,
t = listItem.offset().top - shortcutsListOffset.top,
r = l + Math.abs(listItem.outerWidth( true ) ),
b = t + Math.abs(listItem.outerHeight( true ) ),
unit,
baseTop,
baseBottom,
omitSet,
i;
if ( coords.x >= l && coords.x <= r && coords.y >= t && coords.y <= b ) {
if ( listItem.text() !== "." ) {
self._hitItem( listItem );
} else {
omitSet = listItem.data( "omitSet" );
unit = ( b - t ) / omitSet.length;
for ( i = 0; i < omitSet.length; i++ ) {
baseTop = t + ( i * unit );
baseBottom = baseTop + unit;
if ( coords.y >= baseTop && coords.y <= baseBottom ) {
self._hitOmitItem( listItem, omitSet.charAt( i ) );
}
}
}
return false;
}
return true;
} );
e.preventDefault();
e.stopPropagation();
} )
// bind mouseout of the fastscroll container to remove popup
.bind( 'touchend mouseup vmouseup vmouseout', function () {
$popup.hide();
$( ".ui-fastscroll-hover" ).removeClass( "ui-fastscroll-hover" );
$( ".ui-fastscroll-hover-first-item" ).removeClass( "ui-fastscroll-hover-first-item" );
$( ".ui-fastscroll-hover-down" ).removeClass( "ui-fastscroll-hover-down" );
self.shortcutsContainer.removeClass( "ui-fastscroll-hover" );
} );
if ( page && !( page.is( ':visible' ) ) ) {
page.bind( 'pageshow', function () { self.refresh(); } );
} else {
self.refresh();
}
// refresh the list when dividers are filtered out
$el.bind( 'updatelayout', function () {
self.refresh();
} );
},
_findClosestDivider: function ( targetChar ) {
var i,
dividerMap = this._dividerMap,
charSet = this._charSet,
charSetLen = charSet.length,
targetIdx = charSet.indexOf( targetChar ),
lastDivider,
subDivider = null;
for ( i = 0; i < targetIdx; ++i ) {
lastDivider = dividerMap[ charSet.charAt( i ) ];
if ( lastDivider !== undefined ) {
subDivider = lastDivider;
}
}
if ( !subDivider ) {
for ( ++i; i < charSetLen; ++i ) {
lastDivider = dividerMap[ charSet.charAt( i ) ];
if ( lastDivider !== undefined ) {
subDivider = lastDivider;
break;
}
}
}
return subDivider;
},
_hitOmitItem: function ( listItem, text ) {
var $popup = this.scrollview.find( '.ui-fastscroll-popup' ),
divider;
divider = this._dividerMap[ text ] || this._findClosestDivider( text );
if ( typeof divider !== "undefined" ) {
this.jumpToDivider( $( divider ) );
}
$popup.text( text ).show();
$( listItem ).addClass( "ui-fastscroll-hover" );
if ( listItem.index() === 0 ) {
$( listItem ).addClass( "ui-fastscroll-hover-first-item" );
}
$( listItem ).siblings().eq( listItem.index() ).addClass( "ui-fastscroll-hover-down" );
},
_hitItem: function ( listItem ) {
var $popup = this.scrollview.find( '.ui-fastscroll-popup' ),
text = listItem.text(),
divider;
if ( text === "#" ) {
divider = this._dividerMap.number;
} else {
divider = this._dividerMap[ text ] || this._findClosestDivider( text );
}
if ( typeof divider !== "undefined" ) {
this.jumpToDivider( $( divider ) );
}
$popup.text( text ).show();
$( listItem ).addClass( "ui-fastscroll-hover" );
if ( listItem.index() === 0 ) {
$( listItem ).addClass( "ui-fastscroll-hover-first-item" );
}
$( listItem ).siblings().eq( listItem.index() ).addClass( "ui-fastscroll-hover-down" );
},
_focusItem: function ( listItem ) {
var self = this,
$popup = self.scrollview.find( '.ui-fastscroll-popup' );
listItem.focusin( function ( e ) {
self.shortcutsList.attr( "aria-hidden", false );
self._hitItem( listItem );
}).focusout( function ( e ) {
self.shortcutsList.attr( "aria-hidden", true );
$popup.hide();
$( ".ui-fastscroll-hover" ).removeClass( "ui-fastscroll-hover" );
$( ".ui-fastscroll-hover-first-item" ).removeClass( "ui-fastscroll-hover-first-item" );
$( ".ui-fastscroll-hover-down" ).removeClass( "ui-fastscroll-hover-down" );
});
},
_contentHeight: function () {
var self = this,
$content = $( '.ui-scrollview-clip' ),
header = null,
footer = null,
paddingValue = 0,
clipSize = $( window ).height();
if ( $content.hasClass( "ui-content" ) ) {
paddingValue = parseInt( $content.css( "padding-top" ), 10 );
clipSize = clipSize - ( paddingValue || 0 );
paddingValue = parseInt( $content.css( "padding-bottom" ), 10 );
clipSize = clipSize - ( paddingValue || 0 );
header = $content.siblings( ".ui-header:visible" );
footer = $content.siblings( ".ui-footer:visible" );
if ( header ) {
if ( header.outerHeight( true ) === null ) {
clipSize = clipSize - ( $( ".ui-header" ).outerHeight() || 0 );
} else {
clipSize = clipSize - header.outerHeight( true );
}
}
if ( footer ) {
clipSize = clipSize - footer.outerHeight( true );
}
} else {
clipSize = $content.height();
}
return clipSize;
},
_omit: function ( numOfItems, maxNumOfItems ) {
var maxGroupNum = parseInt( ( maxNumOfItems - 1 ) / 2, 10 ),
numOfExtraItems = numOfItems - maxNumOfItems,
groupPos = [],
omitInfo = [],
groupPosLength,
group,
size,
i;
if ( ( maxNumOfItems < 3 ) || ( numOfItems <= maxNumOfItems ) ) {
return;
}
if ( numOfExtraItems >= maxGroupNum ) {
size = 2;
group = 1;
groupPosLength = maxGroupNum;
} else {
size = maxNumOfItems / ( numOfExtraItems + 1 );
group = size;
groupPosLength = numOfExtraItems;
}
for ( i = 0; i < groupPosLength; i++ ) {
groupPos.push( parseInt( group, 10 ) );
group += size;
}
for ( i = 0; i < maxNumOfItems; i++ ) {
omitInfo.push( 1 );
}
for ( i = 0; i < numOfExtraItems; i++ ) {
omitInfo[ groupPos[ i % maxGroupNum ] ]++;
}
return omitInfo;
},
_createDividerMap: function () {
var primaryCharacterSet = this._primaryLanguage ? this._primaryLanguage.replace( /,/g, "" ) : null,
secondCharacterSet = this._secondLanguage ? this._secondLanguage.replace( /,/g, "" ) : null,
numberSet = "0123456789",
dividers = this.element.find( '.ui-li-divider' ),
map = {},
matchToDivider,
makeCharacterSet,
indexChar,
i;
matchToDivider = function ( index, divider ) {
if ( $( divider ).text() === indexChar ) {
map[ indexChar ] = divider;
}
};
makeCharacterSet = function ( index, divider ) {
primaryCharacterSet += $( divider ).text();
};
if ( primaryCharacterSet === null ) {
primaryCharacterSet = "";
dividers.each( makeCharacterSet );
}
for ( i = 0; i < primaryCharacterSet.length; i++ ) {
indexChar = primaryCharacterSet.charAt( i );
dividers.each( matchToDivider );
}
if ( secondCharacterSet !== null ) {
for ( i = 0; i < secondCharacterSet.length; i++ ) {
indexChar = secondCharacterSet.charAt( i );
dividers.each( matchToDivider );
}
}
dividers.each( function ( index, divider ) {
if ( numberSet.search( $( divider ).text() ) !== -1 ) {
map.number = divider;
return false;
}
});
this._dividerMap = map;
this._charSet = primaryCharacterSet + secondCharacterSet;
},
indexString: function ( indexAlphabet ) {
var self = this,
characterSet = [];
if ( typeof indexAlphabet === "undefined" ) {
return self._primaryLanguage + ":" + self._secondLanguage;
}
characterSet = indexAlphabet.split( ":" );
self._primaryLanguage = characterSet[ 0 ];
if ( characterSet.length === 2 ) {
self._secondLanguage = characterSet[ 1 ];
}
},
refresh: function () {
var self = this,
primaryCharacterSet = self._primaryLanguage ? self._primaryLanguage.replace( /,/g, "" ) : null,
secondCharacterSet = self._secondLanguage ? self._secondLanguage.replace( /,/g, "" ) : null,
contentHeight = self._contentHeight(),
shapItem = $( '<li tabindex="0" aria-label="double to move Number list"><span aria-hidden="true">#</span><span aria-label="Number"/></li>' ),
$popup = this.scrollview.find( '.ui-fastscroll-popup' ),
omitIndex = 0,
makeCharacterSet,
makeOmitSet,
itemHandler,
containerHeight,
shortcutsItems,
shortcutItem,
shortcutsTop,
minClipHeight,
maxNumOfItems,
numOfItems,
minHeight,
omitInfo,
dividers,
listItems,
emptySize,
correction,
indexChar,
lastIndex,
seconds,
height,
size,
i;
makeCharacterSet = function ( index, divider ) {
primaryCharacterSet += $( divider ).text();
};
makeOmitSet = function ( index, length ) {
var count,
omitSet = "";
for ( count = 0; count < length; count++ ) {
omitSet += primaryCharacterSet[ index + count ];
}
return omitSet;
};
self._createDividerMap();
self.shortcutsList.find( 'li' ).remove();
// get all the dividers from the list and turn them into shortcuts
dividers = self.element.find( '.ui-li-divider' );
// get all the list items
listItems = self.element.find('li').not('.ui-li-divider');
// only use visible dividers
dividers = dividers.filter( ':visible' );
listItems = listItems.filter( ':visible' );
if ( dividers.length < 2 ) {
self.shortcutsList.hide();
return;
}
self.shortcutsList.show();
self.lastListItem = listItems.last();
self.shortcutsList.append( shapItem );
self._focusItem( shapItem );
if ( primaryCharacterSet === null ) {
primaryCharacterSet = "";
dividers.each( makeCharacterSet );
}
minHeight = shapItem.outerHeight( true );
maxNumOfItems = parseInt( contentHeight / minHeight - 1, 10 );
numOfItems = primaryCharacterSet.length;
maxNumOfItems = secondCharacterSet ? maxNumOfItems - 2 : maxNumOfItems;
if ( maxNumOfItems < 3 ) {
shapItem.remove();
return;
}
omitInfo = self._omit( numOfItems, maxNumOfItems );
for ( i = 0; i < primaryCharacterSet.length; i++ ) {
indexChar = primaryCharacterSet.charAt( i );
shortcutItem = $( '<li tabindex="0" aria-label="double to move ' + indexChar + ' list">' + indexChar + '</li>' );
self._focusItem( shortcutItem );
if ( typeof omitInfo !== "undefined" && omitInfo[ omitIndex ] > 1 ) {
shortcutItem = $( '<li>.</li>' );
shortcutItem.data( "omitSet", makeOmitSet( i, omitInfo[ omitIndex ] ) );
i += omitInfo[ omitIndex ] - 1;
}
self.shortcutsList.append( shortcutItem );
omitIndex++;
}
if ( secondCharacterSet !== null ) {
lastIndex = secondCharacterSet.length - 1;
seconds = [];
seconds.push( secondCharacterSet.charAt( 0 ) );
seconds.push( secondCharacterSet.charAt( lastIndex ) );
for ( i = 0; i < seconds.length; i++ ) {
indexChar = seconds[ i ];
shortcutItem = $( '<li tabindex="0" aria-label="double to move ' + indexChar + ' list">' + indexChar + '</li>' );
self._focusItem( shortcutItem );
shortcutItem.bind( 'vclick', itemHandler );
self.shortcutsList.append( shortcutItem );
}
}
containerHeight = self.shortcutsContainer.outerHeight( true );
emptySize = contentHeight - containerHeight;
shortcutsItems = self.shortcutsList.children();
size = parseInt( emptySize / shortcutsItems.length, 10 );
correction = emptySize - ( shortcutsItems.length * size );
if ( emptySize > 0 ) {
shortcutsItems.each( function ( index, item ) {
height = $( item ).height() + size;
if ( correction !== 0 ) {
height += 1;
correction -= 1;
}
$( item ).css( {
height: height,
lineHeight: height + "px"
} );
} );
}
// position the shortcut flush with the top of the first list divider
shortcutsTop = dividers.first().position().top;
self.shortcutsContainer.css( 'top', shortcutsTop );
// make the scrollview clip tall enough to show the whole of the shortcutslist
minClipHeight = shortcutsTop + self.shortcutsContainer.outerHeight() + 'px';
self.scrollview.css( 'min-height', minClipHeight );
$popup.text( "M" ) // Popup size is determined based on "M".
.width( $popup.height() )
.css( { marginLeft: -( $popup.outerWidth() / 2 ),
marginTop: -( $popup.outerHeight() / 2 ) } );
}
} );
$( document ).bind( "pagecreate create", function ( e ) {
$( $.tizen.fastscroll.prototype.options.initSelector, e.target )
.not( ":jqmData(role='none'), :jqmData(role='nojs')" )
.fastscroll();
} );
$( window ).bind( "resize orientationchange", function ( e ) {
$( ".ui-page-active .ui-fastscroll-target" ).fastscroll( "refresh" );
} );
} ( jQuery ) );
//>>excludeStart("jqmBuildExclude", pragmas.jqmBuildExclude);
} );
//>>excludeEnd("jqmBuildExclude");