1 // ==========================================================================
  2 // Project:   The M-Project - Mobile HTML5 Application Framework
  3 // Copyright: (c) 2010 M-Way Solutions GmbH. All rights reserved.
  4 // Creator:   Dominik
  5 // Date:      03.11.2010
  6 // License:   Dual licensed under the MIT or GPL Version 2 licenses.
  7 //            http://github.com/mwaylabs/The-M-Project/blob/master/MIT-LICENSE
  8 //            http://github.com/mwaylabs/The-M-Project/blob/master/GPL-LICENSE
  9 // ==========================================================================
 10 
 11 m_require('ui/search_bar.js');
 12 
 13 /**
 14  * @class
 15  *
 16  * M.ListView is the prototype of any list view. It is used to display static or dynamic
 17  * content as vertically aligned list items (M.ListItemView). A list view provides some
 18  * easy to use helper method, e.g. an out-of-the-box delete view for items.
 19  *
 20  * @extends M.View
 21  */
 22 M.ListView = M.View.extend(
 23 /** @scope M.ListView.prototype */ {
 24 
 25     /**
 26      * The type of this object.
 27      *
 28      * @type String
 29      */
 30     type: 'M.ListView',
 31 
 32     /**
 33      * Determines whether to remove all item if the list is updated or not.
 34      *
 35      * @type Boolean
 36      */
 37     removeItemsOnUpdate: YES,
 38 
 39     /**
 40      * Determines whether to display the list as a divided list or not.
 41      *
 42      * @type Boolean
 43      */
 44     isDividedList: NO,
 45 
 46     /**
 47      * If the list view is a divided list, this property can be used to customize the style
 48      * of the list's dividers.
 49      *
 50      * @type String
 51      */
 52     cssClassForDivider: null,
 53 
 54     /**
 55      * Determines whether to display the the number of child items for each list item view.
 56      *
 57      * @type Boolean
 58      */
 59     isCountedList: NO,
 60 
 61     /**
 62      * If the list view is a counted list, this property can be used to customize the style
 63      * of the list item's counter.
 64      *
 65      * @type String
 66      */
 67     cssClassForCounter: null,
 68 
 69     /**
 70      * This property can be used to customize the style of the list view's split view. For example
 71      * the toggleRemove() of a list view uses the built-in split view functionality.
 72      *
 73      * @type String
 74      */
 75     cssClassForSplitView: null,
 76 
 77     /**
 78      * The list view's items, respectively its child views.
 79      *
 80      * @type Array
 81      */
 82     items: null,
 83 
 84     /**
 85      * States whether the list view is currently in edit mode or not. This is mainly used by the
 86      * built-in toggleRemove() functionality. 
 87      *
 88      * @type Boolean
 89      */
 90     inEditMode: NO,
 91 
 92     /**
 93      * This property contains all available options for the edit mode. For example the target and action
 94      * of the automatically rendered delete button can be specified using this property.
 95      *
 96      * @type Object
 97      */
 98     editOptions: null,
 99 
100     /**
101      * Defines if the ListView is rendered with prefixed numbering for each item.
102      *
103      * @type Boolean
104      */
105     isNumberedList: NO,
106 
107     /**
108      * This property contains the list view's template view, the blueprint for every child view.
109      *
110      * @type M.ListItemView
111      */
112     listItemTemplateView: null,
113 
114     /**
115      * Determines whether to display the list view 'inset' or at full width.
116      *
117      * @type Boolean
118      */
119     isInsetList: NO,
120 
121     /**
122      * The list view's search bar.
123      *
124      * @type Object
125       */
126     searchBar: M.SearchBarView,
127 
128     /**
129      * Determines whether or not to display a search bar at the top of the list view. 
130      *
131      * @type Boolean
132      */
133     hasSearchBar: NO,
134 
135     /**
136      * If the hasSearchBar property is set to YES, this property determines whether to use the built-in
137      * simple search filters or not. If set to YES, the list is simply filtered on the fly according
138      * to the entered search string. Only list items matching the entered search string will be visible.
139      *
140      * If a custom search behaviour is needed, this property must be set to NO.
141      *
142      * @type Boolean
143      */
144     usesDefaultSearchBehaviour: YES,
145 
146     /**
147      * An object containing target and action to be triggered if the search string changes.
148      *
149      * @type Object
150      */
151     onSearchStringDidChange: null,
152 
153     /**
154      * This method renders the empty list view either as an ordered or as an unordered list. It also applies
155      * some styling, if the corresponding properties where set.
156      *
157      * @private
158      * @returns {String} The list view's styling as html representation.
159      */
160     render: function() {
161         if(this.hasSearchBar && !this.usesDefaultSearchBehaviour) {
162             this.searchBar.isListViewSearchBar = YES;
163             this.searchBar.listView = this;
164             this.searchBar = M.SearchBarView.design(this.searchBar);
165             this.html += this.searchBar.render();
166         }
167 
168         var listTagName = this.isNumberedList ? 'ol' : 'ul';
169         this.html += '<' + listTagName + ' id="' + this.id + '" data-role="listview"' + this.style() + '></' + listTagName + '>';
170         
171         return this.html;
172     },
173 
174     /**
175      * This method adds a new list item to the list view by simply appending its html representation
176      * to the list view inside the DOM. This method is based on jQuery's append().
177      *
178      * @param {String} item The html representation of a list item to be added.
179      */
180     addItem: function(item) {
181         $('#' + this.id).append(item);
182     },
183 
184     /**
185      * This method removes all of the list view's items by removing all of its content in the DOM. This
186      * method is based on jQuery's empty().
187      */
188     removeAllItems: function() {
189         $('#' + this.id).empty();
190     },
191 
192     /**
193      * Updates the the list view by re-rendering all of its child views, respectively its item views. There
194      * is no rendering done inside this method itself. It is more like the manager of the rendering process
195      * and delegates the responsibility to renderListItemDivider() and renderListItemView() based on the
196      * given list view configuration.
197      *
198      * @private
199      */
200     renderUpdate: function() {
201 
202         /* Remove all list items if the removeItemsOnUpdate property is set to YES */
203         if(this.removeItemsOnUpdate) {
204             this.removeAllItems();
205         }
206 
207         /* Save this in variable that for later use within an other scope (e.g. _each()) */
208         var that = this;
209 
210         /* Get the list view's content as an object from the assigned content binding */
211         var content = eval(this.contentBinding);
212 
213         /* Get the list view's template view for each list item */
214         var templateView = this.listItemTemplateView;
215 
216         /* If there is an items property, re-assign this to content, otherwise iterate through content itself */
217         if(this.items) {
218             content = content[this.items];
219         }
220 
221         if(this.isDividedList) {
222             _.each(content, function(items, divider) {
223                 that.renderListItemDivider(divider);
224                 that.renderListItemView(items, templateView);
225             });
226         } else {
227             this.renderListItemView(content, templateView);
228         }
229 
230         /* Finally let the whole list look nice */
231         this.themeUpdate();
232     },
233 
234     /**
235      * Renders a list item divider based on a string given by its only parameter.
236      *
237      * @param {String} name The name of the list divider to be rendered.
238      * @private
239      */
240     renderListItemDivider: function(name) {
241         var obj = M.ListItemView.design({});
242         obj.value = name;
243         obj.isDivider = YES,
244         this.addItem(obj.render());
245         obj.theme();
246     },
247 
248     /**
249      * This method renders list items based on the passed parameters.
250      *
251      * @param {Array} content The list items to be rendered.
252      * @param {M.ListItemView} templateView The template for for each list item.
253      * @private
254      */
255     renderListItemView: function(content, templateView) {
256         /* Save this in variable that for later use within an other scope (e.g. _each()) */
257         var that = this;
258 
259         _.each(content, function(item) {
260 
261             /* Create a new object for the current template view */
262             var obj = templateView.design({});
263 
264             /* If item is a model, assign the model's id to the view's modelId property */
265             if(item.type === 'M.Model') {
266                 obj.modelId = item.id;
267             }
268 
269             /* Get the child views as an array of strings */
270             var childViewsArray = obj.childViews.split(' ');
271 
272             /* If the item is a model, read the values from the 'record' property instead */
273             var record = item.type === 'M.Model' ? item.record : item;
274 
275             /* Iterate through all views defined in the template view */
276             for(var i in childViewsArray) {
277                 /* Create a new object for the current view */
278                 obj[childViewsArray[i]] = obj[childViewsArray[i]].design({});
279 
280                 var regexResult = null;
281                 if(obj[childViewsArray[i]].computedValue) {
282                     /* This regex looks for a variable inside the template view (<%= ... %>) ... */
283                     regexResult = /^<%=\s+([.|_|-|$|�|a-zA-Z]+[0-9]*[.|_|-|$|�|a-zA-Z]*)\s*%>$/.exec(obj[childViewsArray[i]].computedValue.valuePattern);
284                 } else {
285                     regexResult = /^<%=\s+([.|_|-|$|�|a-zA-Z]+[0-9]*[.|_|-|$|�|a-zA-Z]*)\s*%>$/.exec(obj[childViewsArray[i]].valuePattern);
286                 }
287 
288                 /* ... if a match was found, the variable is replaced by the corresponding value inside the record */
289                 if(regexResult) {
290                     switch (obj[childViewsArray[i]].type) {
291                         case 'M.LabelView':
292                         case 'M.ButtonView':
293                         case 'M.ImageView':
294                             obj[childViewsArray[i]].value = record[regexResult[1]];
295                             break;
296                     }
297                 }
298             }
299 
300             /* If edit mode is on, render a delete button */
301             if(that.inEditMode) {
302                 obj.inEditMode = that.inEditMode;
303                 obj.deleteButton = obj.deleteButton.design({
304                     modelId: item.id,
305                     target: that.editOptions.target,
306                     action: that.editOptions.action
307                 });
308             }
309 
310             /* set the list view as 'parent' for the current list item view */
311             obj.listView = that;
312 
313             /* Add the current list view item to the list view ... */
314             that.addItem(obj.render());
315 
316             /* ... once it is in the DOM, make it look nice */
317             for(var i in childViewsArray) {
318                 obj[childViewsArray[i]].theme();
319             }
320         });
321     },
322 
323     /**
324      * Triggers the rendering engine, jQuery mobile, to style the list view.
325      *
326      * @private
327      */
328     theme: function() {
329         $('#' + this.id).listview();
330         if(this.searchBar) {
331             this.searchBar.theme();
332         }
333     },
334 
335     /**
336      * Triggers the rendering engine, jQuery mobile, to re-style the list view.
337      *
338      * @private
339      */
340     themeUpdate: function() {
341         $('#' + this.id).listview('refresh');
342     },
343 
344     /**
345      * This method activates the edit mode and forces the list view to re-render itself
346      * and to display a remove button for every list view item.
347      *
348      * @param {Object} options The options for the remove button.
349      */
350     toggleRemove: function(options) {
351         if(eval(this.contentBinding)) {
352             this.inEditMode = !this.inEditMode;
353             this.editOptions = options;
354             this.renderUpdate();
355         }
356     },
357 
358     /**
359      * This method activates a list item by applying the default 'isActive' css style to its
360      * DOM representation.
361      *
362      * @param {String} listItemId The id of the list item to be set active.
363      */
364     setActiveListItem: function(listItemId) {
365         $('#' + this.id).find('li').each(function() {
366             var listItem = M.ViewManager.getViewById($(this).attr('id'));
367             listItem.removeCssClass('ui-btn-active');
368         });
369         M.ViewManager.getViewById(listItemId).addCssClass('ui-btn-active')
370     },
371 
372     /**
373      * Applies some style-attributes to the list view.
374      *
375      * @private
376      * @returns {String} The list's styling as html representation.
377      */
378     style: function() {
379         var html = '';
380         if(this.isDividedList && this.cssClassForDivider) {
381             html += ' data-dividertheme="' + this.cssClassForDivider + '"';
382         }
383         if(this.isInsetList) {
384             html += ' data-inset="true"';
385         }
386         if(this.isCountedList && this.cssClassForCounter) {
387             html += ' data-counttheme="' + this.cssClassForCounter + '"';
388         }
389         if(this.cssClassForSplitView) {
390             html += ' data-splittheme="' + this.cssClassForSplitView + '"';
391         }
392         if(this.hasSearchBar && this.usesDefaultSearchBehaviour) {
393             html += ' data-filter="true"';
394         }
395         return html;
396     }
397 
398 });