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 });