e1bd579ca9200a8b6317bc4293e5d2353c1ea988
[framework/web/web-ui-fw.git] / src / widgets / autodividers / js / autodividers.js
1 /* TBD */
2 /*
3  * jQuery Mobile Widget @VERSION - listview autodividers
4  *
5  * This software is licensed under the MIT licence (as defined by the OSI at
6  * http://www.opensource.org/licenses/mit-license.php)
7  * 
8  * ***************************************************************************
9  * Copyright (C) 2011 by Intel Corporation Ltd.
10  * 
11  * Permission is hereby granted, free of charge, to any person obtaining a
12  * copy of this software and associated documentation files (the "Software"),
13  * to deal in the Software without restriction, including without limitation
14  * the rights to use, copy, modify, merge, publish, distribute, sublicense,
15  * and/or sell copies of the Software, and to permit persons to whom the
16  * Software is furnished to do so, subject to the following conditions:
17  * 
18  * The above copyright notice and this permission notice shall be included in
19  * all copies or substantial portions of the Software.
20  * 
21  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
27  * DEALINGS IN THE SOFTWARE.
28  * ***************************************************************************
29  *
30  * Authors: Elliot Smith <elliot.smith@intel.com>
31  */
32
33 // Applies dividers automatically to a listview, using link text
34 // (for link lists) or text (for readonly lists) as the basis for the
35 // divider text.
36 //
37 // Apply using autodividers({type: 'X'}) on a <ul> with
38 // data-role="listview", or with data-autodividers="true", where X
39 // is the type of divider to create. The default divider type is 'alpha',
40 // meaning first characters of list item text, upper-cased.
41 //
42 // The element used to derive the text for the auto dividers defaults
43 // to the first link inside the li; failing that, the text directly inside
44 // the li element is used. This can be overridden with the
45 // data-autodividers-selector attribute or via options; the selector
46 // will use each li element as its context.
47 //
48 // Any time a new li element is added to the list, or an li element is
49 // removed, this extension will update the dividers in the listview
50 // accordingly.
51 //
52 // Note that if a listview already has dividers, applying this
53 // extension will remove all the existing dividers and replace them
54 // with new, generated ones.
55 //
56 // Also note that this extension doesn't sort the list: it only creates
57 // dividers based on text inside list items. So if your list isn't
58 // alphabetically-sorted, you may get duplicate dividers.
59 //
60 // So, for example, this markup:
61 //
62 // <ul id="has-no-dividers" data-role="listview" data-autodividers="alpha">
63 //              <li>Barry</li>
64 //              <li>Carrie</li>
65 //              <li>Betty</li>
66 //              <li>Harry</li>
67 //              <li>Carly</li>
68 //              <li>Hetty</li>
69 // </ul>
70 //
71 // will produce dividers like this:
72 //
73 // <ul data-role="listview" data-autodividers="alpha">
74 //      <li data-role="list-divider">B</li>
75 //      <li>Barry</li>
76 //      <li data-role="list-divider">C</li>
77 //      <li>Carrie</li>
78 //      <li data-role="list-divider">B</li>
79 //      <li>Betty</li>
80 //      <li data-role="list-divider">H</li>
81 //      <li>Harry</li>
82 //      <li data-role="list-divider">C</li>
83 //      <li>Carly</li>
84 //      <li data-role="list-divider">H</li>
85 //      <li>Hetty</li>
86 // </ul>
87 //
88 // with each divider occuring twice.
89 //
90 // Options:
91 //
92 //      selector: The jQuery selector to use to find text for the
93 //                      generated dividers. Default is to use the first 'a'
94 //                      (link) element. If this selector doesn't find any
95 //                      text, the widget automatically falls back to the text
96 //                      inside the li (for read-only lists). Can be set to a custom
97 //                      selector via data-autodividers-selector="..." or the 'selector'
98 //                      option.
99 //
100 //       type: 'alpha' (default) sets the auto divider type to "uppercased
101 //               first character of text selected from each item"; "full" sets
102 //               it to the unmodified text selected from each item. Set via
103 //               the data-autodividers="<type>" attribute on the listview or
104 //               the 'type' option.
105 //
106 // Events:
107 //
108 //      updatelayout: Triggered if the dividers in the list change;
109 //              this happens if list items are added to the listview,
110 //              which causes the autodividers to be regenerated.
111
112 (function( $, undefined ) {
113
114 var autodividers = function(options) {
115         var list = $( this );
116         options = options || {};
117
118         var listview = list.data( 'listview' );
119
120         var dividerType = options.type || list.jqmData( 'autodividers' ) || 'alpha';
121
122         var textSelector = options.selector || list.jqmData( 'autodividers-selector' ) || 'a';
123
124         var getDividerText = function( elt ) {
125                 // look for some text in the item
126                 var text = elt.find( textSelector ).text() || elt.text() || null;
127
128                 if ( !text ) {
129                         return null;
130                 }
131
132                 // create the text for the divider
133                 if ( dividerType === 'alpha' ) {
134                         text = text.slice( 0, 1 ).toUpperCase();
135                 }
136
137                 return text;
138         };
139
140         var mergeDividers = function() {
141                 var dividersChanged = false;
142
143                 // any dividers which are following siblings of a divider, where
144                 // there are no dividers with different text inbetween, can be removed
145                 list.find( 'li.ui-li-divider' ).each(function() {
146                         var divider = $( this );
147                         var dividerText = divider.text();
148                         var selector = '.ui-li-divider:not(:contains(' + dividerText + '))';
149                         var nextDividers = divider.nextUntil( selector );
150                         nextDividers = nextDividers.filter( '.ui-li-divider:contains(' + dividerText + ')' );
151
152                         if (nextDividers.length > 0) {
153                                 nextDividers.remove();
154                                 dividersChanged = true;
155                         }
156                 });
157
158                 if (dividersChanged) {
159                         list.trigger( 'updatelayout' );
160                 }
161         };
162
163         // check that elt is a non-divider li element
164         var isNonDividerLi = function( elt ) {
165                 return elt.is('li') &&
166                        elt.jqmData( 'role' ) !== 'list-divider';
167         };
168
169         // li element inserted, so check whether it needs a divider
170         var liAdded = function( li ) {
171                 var dividerText = getDividerText( li );
172
173                 if ( !dividerText ) {
174                         listview.refresh();
175                         return;
176                 }
177
178                 // add expected divider for this li if it doesn't exist
179                 var existingDividers = li.prevAll( '.ui-li-divider:first:contains(' + dividerText + ')' );
180
181                 if ( existingDividers.length === 0 ) {
182                         var divider = $( '<li>' + dividerText + '</li>' );
183                         divider.attr( 'data-' + $.mobile.ns + 'role', 'list-divider' );
184                         li.before( divider );
185
186                         listview.refresh();
187
188                         mergeDividers();
189                 }
190                 else {
191                         listview.refresh();
192                 }
193         };
194
195         // li element removed, so check whether its divider should go
196         var liRemoved = function( li ) {
197
198                 var dividerText = getDividerText( li );
199
200                 if ( !dividerText ) {
201                         listview.refresh();
202                         return;
203                 }
204
205                 // remove divider for this li if there are no other
206                 // li items for the divider before or after this li item
207                 var precedingItems = li.prevUntil( '.ui-li-divider:contains(' + dividerText + ')' );
208                 var nextItems = li.nextUntil( '.ui-li-divider' );
209
210                 if ( precedingItems.length === 0 && nextItems.length === 0 ) {
211                         li.prevAll( '.ui-li-divider:contains(' + dividerText + '):first' ).remove();
212
213                         listview.refresh();
214
215                         mergeDividers();
216                 }
217                 else {
218                         listview.refresh();
219                 }
220         };
221
222         // set up the dividers on first create
223         list.find( 'li' ).each( function() {
224                 var li = $( this );
225
226                 // remove existing dividers
227                 if ( li.jqmData( 'role' ) === 'list-divider' ) {
228                         li.remove();
229                 }
230                 // make new dividers for list items
231                 else {
232                         liAdded( li );
233                 }
234         });
235
236         // bind to DOM events to keep list up to date
237         list.bind( 'DOMNodeInserted', function( e ) {
238                 var elt = $( e.target );
239
240                 if ( !isNonDividerLi( elt ) ) {
241                         return;
242                 }
243
244                 liAdded( elt );
245         });
246
247         list.bind( 'DOMNodeRemoved', function( e ) {
248                 var elt = $( e.target );
249
250                 if ( !isNonDividerLi( elt ) ) {
251                         return;
252                 }
253
254                 liRemoved( elt );
255         });
256 };
257
258 $.fn.autodividers = autodividers;
259
260 $( ":jqmData(role=listview)" ).live( "listviewcreate", function() {
261         var list = $( this );
262
263         if ( list.is( ':jqmData(autodividers)' ) ) {
264                 list.autodividers();
265         }
266 });
267
268 })( jQuery );