1 // ==========================================================================
  2 // Project:   The M-Project - Mobile HTML5 Application Framework
  3 // Copyright: (c) 2010 M-Way Solutions GmbH. All rights reserved.
  4 //            (c) 2011 panacoda GmbH. All rights reserved.
  5 // Creator:   Dominik
  6 // Date:      03.11.2010
  7 // License:   Dual licensed under the MIT or GPL Version 2 licenses.
  8 //            http://github.com/mwaylabs/The-M-Project/blob/master/MIT-LICENSE
  9 //            http://github.com/mwaylabs/The-M-Project/blob/master/GPL-LICENSE
 10 // ==========================================================================
 11 
 12 m_require('ui/search_bar.js');
 13 
 14 /**
 15  * @class
 16  *
 17  * M.ListView is the prototype of any list view. It is used to display static or dynamic
 18  * content as vertically aligned list items (M.ListItemView). A list view provides some
 19  * easy to use helper method, e.g. an out-of-the-box delete view for items.
 20  *
 21  * @extends M.View
 22  */
 23 M.ListView = M.View.extend(
 24 /** @scope M.ListView.prototype */ {
 25 
 26     /**
 27      * The type of this object.
 28      *
 29      * @type String
 30      */
 31     type: 'M.ListView',
 32 
 33     /**
 34      * Determines whether to remove all item if the list is updated or not.
 35      *
 36      * @type Boolean
 37      */
 38     removeItemsOnUpdate: YES,
 39 
 40     /**
 41      * Determines whether to display the list as a divided list or not.
 42      *
 43      * @type Boolean
 44      */
 45     isDividedList: NO,
 46 
 47     /**
 48      * If the list view is a divided list, this property can be used to customize the style
 49      * of the list's dividers.
 50      *
 51      * @type String
 52      */
 53     cssClassForDivider: null,
 54 
 55     /**
 56      * Determines whether to display the the number of child items for each list item view.
 57      *
 58      * @type Boolean
 59      */
 60     isCountedList: NO,
 61 
 62     /**
 63      * If the list view is a counted list, this property can be used to customize the style
 64      * of the list item's counter.
 65      *
 66      * @type String
 67      */
 68     cssClassForCounter: null,
 69 
 70     /**
 71      * This property can be used to customize the style of the list view's split view. For example
 72      * the toggleRemove() of a list view uses the built-in split view functionality.
 73      *
 74      * @type String
 75      */
 76     cssClassForSplitView: null,
 77 
 78     /**
 79      * The list view's items, respectively its child views.
 80      *
 81      * @type Array
 82      */
 83     items: null,
 84 
 85     /**
 86      * States whether the list view is currently in edit mode or not. This is mainly used by the
 87      * built-in toggleRemove() functionality. 
 88      *
 89      * @type Boolean
 90      */
 91     inEditMode: NO,
 92 
 93     /**
 94      * This property contains all available options for the edit mode. For example the target and action
 95      * of the automatically rendered delete button can be specified using this property.
 96      *
 97      * @type Object
 98      */
 99     editOptions: null,
100 
101     /**
102      * Defines if the ListView is rendered with prefixed numbering for each item.
103      *
104      * @type Boolean
105      */
106     isNumberedList: NO,
107 
108     /**
109      * This property contains the list view's template view, the blueprint for every child view.
110      *
111      * @type M.ListItemView
112      */
113     listItemTemplateView: null,
114 
115     /**
116      * Determines whether to display the list view 'inset' or at full width.
117      *
118      * @type Boolean
119      */
120     isInset: NO,
121 
122     /**
123      * Determines whether to add margin at the top of the list or not. This is useful whenever
124      * the list is not the first element within a page's content area to make sure the list does
125      * not overlap preceding elements.
126      *
127      * @type Boolean
128      */
129     doNotOverlapAtTop: NO,
130 
131 
132     /**
133      * Determines whether to add margin at the bottom of the list or not. This is useful whenever
134      * the list is not the last element within a page's content area to make sure the list does
135      * not overlap following elements.
136      *
137      * @type Boolean
138      */
139     doNotOverlapAtBottom: NO,
140 
141     /**
142      * The list view's search bar.
143      *
144      * @type Object
145       */
146     searchBar: M.SearchBarView,
147 
148     /**
149      * Determines whether or not to display a search bar at the top of the list view. 
150      *
151      * @type Boolean
152      */
153     hasSearchBar: NO,
154 
155     /**
156      * If the hasSearchBar property is set to YES, this property determines whether to use the built-in
157      * simple search filters or not. If set to YES, the list is simply filtered on the fly according
158      * to the entered search string. Only list items matching the entered search string will be visible.
159      *
160      * If a custom search behaviour is needed, this property must be set to NO.
161      *
162      * @type Boolean
163      */
164     usesDefaultSearchBehaviour: YES,
165 
166     /**
167      * If the hasSearchBar property is set to YES and the usesDefaultSearchBehaviour is set to YES, this
168      * property can be used to specify the inital text for the search bar. This text will be shown as long
169      * as nothing else is entered into the search bar text field.
170      *
171      * @type String
172      */
173     searchBarInitialText: 'Search...',
174 
175     /**
176      * An object containing target and action to be triggered if the search string changes.
177      *
178      * @type Object
179      */
180     onSearchStringDidChange: null,
181 
182     /**
183      * An optional String defining the id property that is passed in view as record id
184      *
185      * @type String
186      */
187     idName: null,
188 
189     /**
190      * Contains a reference to the currently selected list item.
191      *
192      * @type Object
193      */
194     selectedItem: null,
195 
196     /**
197      * This method renders the empty list view either as an ordered or as an unordered list. It also applies
198      * some styling, if the corresponding properties where set.
199      *
200      * @private
201      * @returns {String} The list view's styling as html representation.
202      */
203     render: function() {
204         /* add the list view to its surrounding page */
205         if(!M.ViewManager.currentlyRenderedPage.listList) {
206             M.ViewManager.currentlyRenderedPage.listList = [];
207         }
208         M.ViewManager.currentlyRenderedPage.listList.push(this);
209 
210         if(this.hasSearchBar && !this.usesDefaultSearchBehaviour) {
211             this.searchBar.isListViewSearchBar = YES;
212             this.searchBar.listView = this;
213             this.searchBar = M.SearchBarView.design(this.searchBar);
214             this.html += this.searchBar.render();
215         }
216 
217         var listTagName = this.isNumberedList ? 'ol' : 'ul';
218         this.html += '<' + listTagName + ' id="' + this.id + '" data-role="listview"' + this.style() + '></' + listTagName + '>';
219 
220         return this.html;
221     },
222 
223     /**
224      * This method is responsible for registering events for view elements and its child views. It
225      * basically passes the view's event-property to M.EventDispatcher to bind the appropriate
226      * events.
227      *
228      * It extend M.View's registerEvents method with some special stuff for list views and their
229      * internal events.
230      */
231     registerEvents: function() {
232         /*this.internalEvents = {
233             focus: {
234                 target: this,
235                 action: 'gotFocus'
236             },
237             blur: {
238                 target: this,
239                 action: 'lostFocus'
240             },
241             keyup: {
242                 target: this,
243                 action: 'setValueFromDOM'
244             }
245         }*/
246         this.bindToCaller(this, M.View.registerEvents)();
247         if(this.hasSearchBar && !this.usesDefaultSearchBehaviour) {
248             this.searchBar.registerEvents();
249         }
250     },
251 
252     /**
253      * This method adds a new list item to the list view by simply appending its html representation
254      * to the list view inside the DOM. This method is based on jQuery's append().
255      *
256      * @param {String} item The html representation of a list item to be added.
257      */
258     addItem: function(item) {
259         $('#' + this.id).append(item);
260     },
261 
262     /**
263      * This method removes all of the list view's items by removing all of its content in the DOM. This
264      * method is based on jQuery's empty().
265      */
266     removeAllItems: function() {
267         $('#' + this.id).empty();
268     },
269 
270     /**
271      * Updates the the list view by re-rendering all of its child views, respectively its item views. There
272      * is no rendering done inside this method itself. It is more like the manager of the rendering process
273      * and delegates the responsibility to renderListItemDivider() and renderListItemView() based on the
274      * given list view configuration.
275      *
276      * @private
277      */
278     renderUpdate: function() {
279 
280         /* Remove all list items if the removeItemsOnUpdate property is set to YES */
281         if(this.removeItemsOnUpdate) {
282             this.removeAllItems();
283         }
284 
285         /* Save this in variable that for later use within an other scope (e.g. _each()) */
286         var that = this;
287 
288         /* Get the list view's content as an object from the assigned content binding */
289         if(this.contentBinding && typeof(this.contentBinding.target) === 'object' && typeof(this.contentBinding.property) === 'string' && this.contentBinding.target[this.contentBinding.property]) {
290             var content = this.contentBinding.target[this.contentBinding.property];
291         } else {
292             M.Logger.log('The specified content binding for the list view (' + this.id + ') is invalid!', M.WARN);
293             return;
294         }
295 
296         /* Get the list view's template view for each list item */
297         var templateView = this.listItemTemplateView;
298 
299         /* if there is no template, log error and stop */
300         if(!templateView) {
301             M.Logger.log('The template view could not be loaded! Maybe you forgot to use m_require to set up the correct load order?', M.ERR);
302             return;
303         }
304 
305         /* If there is an items property, re-assign this to content, otherwise iterate through content itself */
306         if(this.items) {
307             content = content[this.items];
308         }
309 
310         if(this.isDividedList) {
311             _.each(content, function(items, divider) {
312                 that.renderListItemDivider(divider);
313                 that.renderListItemView(items, templateView);
314             });
315         } else {
316             this.renderListItemView(content, templateView);
317         }
318 
319         /* Finally let the whole list look nice */
320         this.themeUpdate();
321 
322         /* At last fix the toolbar */
323         $.mobile.fixedToolbars.show();
324     },
325 
326     /**
327      * Renders a list item divider based on a string given by its only parameter.
328      *
329      * @param {String} name The name of the list divider to be rendered.
330      * @private
331      */
332     renderListItemDivider: function(name) {
333         var obj = M.ListItemView.design({});
334         obj.value = name;
335         obj.isDivider = YES,
336         this.addItem(obj.render());
337         obj.theme();
338     },
339 
340     /**
341      * This method renders list items based on the passed parameters.
342      *
343      * @param {Array} content The list items to be rendered.
344      * @param {M.ListItemView} templateView The template for for each list item.
345      * @private
346      */
347     renderListItemView: function(content, templateView) {
348         /* Save this in variable that for later use within an other scope (e.g. _each()) */
349         var that = this;
350 
351         _.each(content, function(item) {
352 
353             /* Create a new object for the current template view */
354             var obj = templateView.design({});
355             /* If item is a model, assign the model's id to the view's modelId property */
356             if(item.type === 'M.Model') {
357                 obj.modelId = item.m_id;
358             /* Otherwise, if there is an id property, save this automatically to have a reference */
359             } else if(item.id || !isNaN(item.id)) {
360                 obj.modelId = item.id;
361             } else if(item[that.idName] || item[that.idName] === "") {
362                 obj.modelId = item[that.idName];
363             }
364 
365             /* Get the child views as an array of strings */
366             var childViewsArray = obj.getChildViewsAsArray();
367 
368             /* If the item is a model, read the values from the 'record' property instead */
369             var record = item.type === 'M.Model' ? item.record : item;
370 
371             /* Iterate through all views defined in the template view */
372             for(var i in childViewsArray) {
373                 /* Create a new object for the current view */
374                 obj[childViewsArray[i]] = obj[childViewsArray[i]].design({});
375 
376                 var regexResult = null;
377                 if(obj[childViewsArray[i]].computedValue) {
378                     /* This regex looks for a variable inside the template view (<%= ... %>) ... */
379                     regexResult = /^<%=\s+([.|_|-|$|§|a-zA-Z]+[0-9]*[.|_|-|$|§|a-zA-Z]*)\s*%>$/.exec(obj[childViewsArray[i]].computedValue.valuePattern);
380                 } else {
381                     regexResult = /^<%=\s+([.|_|-|$|§|a-zA-Z]+[0-9]*[.|_|-|$|§|a-zA-Z]*)\s*%>$/.exec(obj[childViewsArray[i]].valuePattern);
382                 }
383 
384                 /* ... if a match was found, the variable is replaced by the corresponding value inside the record */
385                 if(regexResult) {
386                     switch (obj[childViewsArray[i]].type) {
387                         case 'M.LabelView':
388                         case 'M.ButtonView':
389                         case 'M.ImageView':
390                         case 'M.TextFieldView':
391                             obj[childViewsArray[i]].value = record[regexResult[1]];
392                             break;
393                     }
394                 }
395             }
396 
397             /* If edit mode is on, render a delete button */
398             if(that.inEditMode) {
399                 obj.inEditMode = that.inEditMode;
400                 obj.deleteButton = obj.deleteButton.design({
401                     modelId: obj.modelId,
402                     events: {
403                         tap: {
404                             target: that.editOptions.target,
405                             action: that.editOptions.action
406                         }
407                     },
408                     internalEvents: {
409                         tap: {
410                             target: that,
411                             action: 'removeListItem'
412                         }
413                     }
414                 });
415             }
416 
417             /* set the list view as 'parent' for the current list item view */
418             obj.parentView = that;
419 
420             /* Add the current list view item to the list view ... */
421             that.addItem(obj.render());
422 
423             /* register events */
424             obj.registerEvents();
425             if(obj.deleteButton) {
426                 obj.deleteButton.registerEvents();
427             }
428 
429             /* ... once it is in the DOM, make it look nice */
430             for(var i in childViewsArray) {
431                 obj[childViewsArray[i]].theme();
432             }
433         });
434     },
435 
436     /**
437      * Triggers the rendering engine, jQuery mobile, to style the list view.
438      *
439      * @private
440      */
441     theme: function() {
442         if(this.searchBar) {
443             /* JQM-hack: remove multiple search bars */
444             if($('#' + this.id) && $('#' + this.id).parent()) {
445                 var searchBarsFound = 0;
446                 $('#' + this.id).parent().find('form.ui-listview-filter').each(function() {
447                     searchBarsFound += 1;
448                     if(searchBarsFound == 1) {
449                         return;
450                     }
451                     $(this).remove();
452                 });
453             }
454             this.searchBar.theme();
455         }
456     },
457 
458     /**
459      * Triggers the rendering engine, jQuery mobile, to re-style the list view.
460      *
461      * @private
462      */
463     themeUpdate: function() {
464         $('#' + this.id).listview('refresh');
465     },
466 
467     /**
468      * This method activates the edit mode and forces the list view to re-render itself
469      * and to display a remove button for every list view item.
470      *
471      * @param {Object} options The options for the remove button.
472      */
473     toggleRemove: function(options) {
474         if(this.contentBinding && typeof(this.contentBinding.target) === 'object' && typeof(this.contentBinding.property) === 'string' && this.contentBinding.target[this.contentBinding.property]) {
475             this.inEditMode = !this.inEditMode;
476             this.editOptions = options;
477             this.renderUpdate();
478         }
479     },
480 
481     /**
482      * This method activates a list item by applying the default 'isActive' css style to its
483      * DOM representation.
484      *
485      * @param {String} listItemId The id of the list item to be set active.
486      */
487     setActiveListItem: function(listItemId, event, nextEvent) {
488         if(this.selectedItem) {
489             this.selectedItem.removeCssClass('ui-btn-active');
490         }
491         this.selectedItem = M.ViewManager.getViewById(listItemId);
492 
493         /* is the selection list items are selectable, activate the right one */
494         if(this.listItemTemplateView && this.listItemTemplateView.isSelectable) {
495             this.selectedItem.addCssClass('ui-btn-active');
496         }
497 
498         /* delegate event to external handler, if specified */
499         if(nextEvent) {
500             M.EventDispatcher.callHandler(nextEvent, event, NO, [listItemId, this.selectedItem.modelId]);
501         }
502     },
503 
504     /**
505      * This method resets the list by applying the default css style to its currently activated
506      * list item.
507      */
508     resetActiveListItem: function() {
509         if(this.selectedItem) {
510             this.selectedItem.removeCssClass('ui-btn-active');
511         }
512     },
513 
514     /**
515      * Applies some style-attributes to the list view.
516      *
517      * @private
518      * @returns {String} The list's styling as html representation.
519      */
520     style: function() {
521         var html = '';
522         if(this.cssClass || this.doNotOverlapAtTop || this.doNotOverlapAtBottom) {
523             html += ' class="'
524                 + (this.cssClass ? this.cssClass : '')
525                 + (!this.isInset && this.doNotOverlapAtTop ? ' listview-do-not-overlap-at-top' : '')
526                 + (!this.isInset && this.doNotOverlapAtBottom ? ' listview-do-not-overlap-at-bottom' : '')
527                 + '"';
528         }
529         if(this.isDividedList && this.cssClassForDivider) {
530             html += ' data-dividertheme="' + this.cssClassForDivider + '"';
531         }
532         if(this.isInset) {
533             html += ' data-inset="true"';
534         }
535         if(this.isCountedList && this.cssClassForCounter) {
536             html += ' data-counttheme="' + this.cssClassForCounter + '"';
537         }
538         if(this.cssClassForSplitView) {
539             html += ' data-splittheme="' + this.cssClassForSplitView + '"';
540         }
541         if(this.hasSearchBar && this.usesDefaultSearchBehaviour) {
542             html += ' data-filter="true" data-filter-placeholder="' + this.searchBarInitialText + '"';
543         }
544         return html;
545     },
546 
547     removeListItem: function(id, event, nextEvent) {
548         var modelId = M.ViewManager.getViewById(id).modelId;
549 
550         /* delegate event to external handler, if specified */
551         if(nextEvent) {
552             M.EventDispatcher.callHandler(nextEvent, event, NO, [id, modelId]);
553         }
554     }
555 
556 });