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: 30.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 /** 13 * A constant value for single selection mode. 14 * 15 * @type String 16 */ 17 M.SINGLE_SELECTION = 'radio'; 18 19 /** 20 * A constant value for multiple selection mode. 21 * 22 * @type String 23 */ 24 M.MULTIPLE_SELECTION = 'checkbox'; 25 26 /** 27 * A constant value for single selection mode in a dialog / popup. 28 * 29 * @type String 30 */ 31 M.SINGLE_SELECTION_DIALOG = 'select'; 32 33 /** 34 * A constant value for multiple selection mode in a dialog / popup. 35 * 36 * @type String 37 */ 38 M.MULTIPLE_SELECTION_DIALOG = 'select_multiple'; 39 40 m_require('ui/selection_list_item.js'); 41 42 /** 43 * @class 44 * 45 * This defines the prototype of any selection list view. A selection list view displays 46 * a list with several items of which either only one single item (M.SINGLE_SELECTION / 47 * M.SINGLE_SELECTION_DIALOG) or many items (M.MULTIPLE_SELECTION / 48 * M.MULTIPLE_SELECTION_DIALOG) can be selected. 49 * 50 * @extends M.View 51 */ 52 M.SelectionListView = M.View.extend( 53 /** @scope M.SelectionListView.prototype */ { 54 55 /** 56 * The type of this object. 57 * 58 * @type String 59 */ 60 type: 'M.SelectionListView', 61 62 /** 63 * Determines whether to remove all item if the list is updated or not. 64 * 65 * @type Boolean 66 */ 67 removeItemsOnUpdate: YES, 68 69 /** 70 * The selection mode for this selection list. This can either be single or 71 * multiple selection. To set this value use one of the three constants: 72 * 73 * - M.SINGLE_SELECTION 74 * 75 * This selection mode will render a selection list with several list items 76 * of which only one can be selected. Whenever a new item is selected, the 77 * previously selected item automatically gets de-selected. This selection 78 * mode's behaviour is equivalent to the plain HTML's radio button. 79 * 80 * 81 * - M.SINGLE_SELECTION_DIALOG 82 * 83 * This selection mode will render a selection list equivalent to the plain 84 * HTML's select menu. Only the currently selected item will be visible, and 85 * by clicking on this item, the selection list will be displayed in a dialog 86 * respectively a popup view. By selecting on of the items, this popup will 87 * automatically close and the selected value will be displayed. 88 * 89 * 90 * - M.MULTIPLE_SELECTION 91 * 92 * This selection mode will render a selection list with several list items 93 * of which all be selected. So the selection of a new item doesn't lead to 94 * automatic de-selected of previously selected items. This selection mode's 95 * behaviour is equivalent to the plain HTML's checkboxes. 96 * 97 * 98 * - M.MULTIPLE_SELECTION_DIALOG 99 * 100 * This selection mode will render a selection list equivalent to the plain 101 * HTML's select menu, but with the possibility to select multiple options. 102 * In contrast to the single selection dialog mode, it also is possible to 103 * select no option at all. As with the multiple selecton mode, the selection 104 * of a new item doesn't lead to automatic de-selected of previously selected 105 * items. 106 * 107 * Note: This mode currently only works on mobile devices!! 108 * 109 * @type String 110 */ 111 selectionMode: M.SINGLE_SELECTION, 112 113 /** 114 * The selected item(s) of this list. 115 * 116 * @type String, Array 117 */ 118 selection: null, 119 120 /** 121 * This property defines the tab bar's name. This is used internally to identify 122 * the selection list inside the DOM. 123 * 124 * @type String 125 */ 126 name: null, 127 128 129 /** 130 * This property is used to specify an initial value for the selection list if 131 * it is running in 'multiple selection dialog' (M.MULTIPLE_SELECTION_DIALOG) mode. 132 * This value is then displayed at startup. You would typically use this e.g. to 133 * specify something like: 'Please select...'. 134 * 135 * As long as this initial value is 'selected', the getSelection() of this selection 136 * list will return nothing. Once a 'real' option is selected, this value will visually 137 * disappear. If at some point no option will be selected again, this initial text 138 * will be shown again. 139 * 140 * @type String 141 */ 142 initialText: null, 143 144 /** 145 * The label proeprty defines a text that is shown above or next to the selection list as a 'title' 146 * for the selection list. e.g. "Name:". If no label is specified, no label will be displayed. 147 * 148 * @type String 149 */ 150 label: null, 151 152 /** 153 * Determines whether to display the selection list grouped with the label specified with the label property. 154 * If set to YES, the selection list and its label are wrapped in a container and styled as a unit 'out of 155 * the box'. If set to NO, custom styling could be necessary. 156 * 157 * @type Boolean 158 */ 159 isGrouped: NO, 160 161 /** 162 * This property is used internally to store the selection list's initial state. This is used to be able 163 * to reset the selection list later on using the resetSelection method. 164 * 165 * Note: This property is only used if the selection list's child views are specified directly (without 166 * content binding). Otherwise the state is stored within the content binding and does not need to be 167 * stored with this selection list. 168 * 169 * @private 170 * @type Object 171 */ 172 initialState: null, 173 174 /** 175 * This property specifies the recommended events for this type of view. 176 * 177 * @type Array 178 */ 179 recommendedEvents: ['change'], 180 181 /** 182 * Renders a selection list. 183 * 184 * @private 185 * @returns {String} The selection list view's html representation. 186 */ 187 render: function() { 188 189 /* initialize the initialState property as new array */ 190 this.initialState = []; 191 192 this.html += '<div id="' + this.id + '_container"'; 193 194 if(this.isGrouped) { 195 this.html += ' data-role="fieldcontain"'; 196 } 197 198 if(this.cssClass) { 199 this.html += ' class="'; 200 var cssClasses = $.trim(this.cssClass).split(' '); 201 for(var i in cssClasses) { 202 this.html += (i > 0 ? ' ' : '') + cssClasses[i] + '_container'; 203 } 204 this.html += '"'; 205 } 206 207 this.html += '>'; 208 209 if(this.selectionMode === M.SINGLE_SELECTION_DIALOG || this.selectionMode === M.MULTIPLE_SELECTION_DIALOG) { 210 211 if(this.label) { 212 this.html += '<label for="' + this.id + '">' + this.label + '</label>'; 213 } 214 215 this.html += '<select name="' + (this.name ? this.name : this.id) + '" id="' + this.id + '"' + this.style() + (this.selectionMode === M.MULTIPLE_SELECTION_DIALOG ? ' multiple="multiple"' : '') + '>'; 216 217 this.renderChildViews(); 218 219 this.html += '</select>'; 220 221 } else { 222 223 this.html += '<fieldset data-role="controlgroup" data-native-menu="false" id="' + this.id + '">'; 224 225 if(this.label) { 226 this.html += '<legend>' + this.label + '</legend>'; 227 } 228 229 this.renderChildViews(); 230 231 this.html += '</fieldset>'; 232 233 } 234 235 this.html += '</div>'; 236 237 return this.html; 238 }, 239 240 /** 241 * Triggers render() on all children of type M.ButtonView based on the specified 242 * selection mode (single or multiple selection). 243 * 244 * @private 245 */ 246 renderChildViews: function() { 247 if(this.childViews) { 248 var childViews = this.getChildViewsAsArray(); 249 250 for(var i in childViews) { 251 var view = this[childViews[i]]; 252 if(view.type === 'M.SelectionListItemView') { 253 view.parentView = this; 254 view._name = childViews[i]; 255 this.html += view.render(); 256 257 /* store list item in initialState property */ 258 this.initialState.push({ 259 value: view.value, 260 label: view.label, 261 isSelected: view.isSelected 262 }); 263 } else { 264 M.Logger.log('Invalid child views specified for SelectionListView. Only SelectionListItemViews accepted.', M.WARN); 265 } 266 } 267 } else if(!this.contentBinding) { 268 M.Logger.log('No SelectionListItemViews specified.', M.WARN); 269 } 270 }, 271 272 /** 273 * This method is responsible for registering events for view elements and its child views. It 274 * basically passes the view's event-property to M.EventDispatcher to bind the appropriate 275 * events. 276 * 277 * It extend M.View's registerEvents method with some special stuff for text field views and 278 * their internal events. 279 */ 280 registerEvents: function() { 281 this.internalEvents = { 282 change: { 283 target: this, 284 action: 'itemSelected' 285 } 286 } 287 this.bindToCaller(this, M.View.registerEvents)(); 288 }, 289 290 /** 291 * This method adds a new selection list item to the selection list view by simply appending 292 * its html representation to the selection list view inside the DOM. This method is based 293 * on jQuery's append(). 294 * 295 * @param {String} item The html representation of a selection list item to be added. 296 */ 297 addItem: function(item) { 298 $('#' + this.id).append(item); 299 }, 300 301 /** 302 * This method removes all of the selection list view's items by removing all of its content in 303 * the DOM. This method is based on jQuery's empty(). 304 */ 305 removeAllItems: function() { 306 $('#' + this.id).empty(); 307 }, 308 309 /** 310 * Updates the the selection list view by re-rendering all of its child views, respectively its 311 * item views. 312 * 313 * @private 314 */ 315 renderUpdate: function() { 316 if(this.removeItemsOnUpdate || this.selectionMode === M.SINGLE_SELECTION_DIALOG || this.selectionMode === M.MULTIPLE_SELECTION_DIALOG) { 317 this.removeAllItems(); 318 319 if(this.label && !(this.selectionMode === M.SINGLE_SELECTION_DIALOG || this.selectionMode === M.MULTIPLE_SELECTION_DIALOG)) { 320 this.addItem('<legend>' + this.label + '</legend>'); 321 } else if(this.selectionMode === M.SINGLE_SELECTION_DIALOG || this.selectionMode === M.MULTIPLE_SELECTION_DIALOG) { 322 } 323 } 324 325 /* remove selection before applying new content */ 326 this.removeSelection(); 327 328 if(this.contentBinding) { 329 /* assign the value property to 'items' since this was automatically set by contentDidChange of M.View */ 330 var items = this.value; 331 for(var i in items) { 332 var item = items[i]; 333 var obj = null; 334 obj = M.SelectionListItemView.design({ 335 value: (item.value !== undefined && item.value !== null) ? item.value : '', 336 label: item.label ? item.label : ((item.value !== undefined && item.value !== null) ? item.value : ''), 337 parentView: this, 338 isSelected: item.isSelected 339 }); 340 if(this.selectionMode !== M.SINGLE_SELECTION_DIALOG && this.selectionMode !== M.MULTIPLE_SELECTION_DIALOG) { 341 obj.name = item.name ? item.name : (item.label ? item.label : (item.value ? item.value : '')); 342 } 343 344 this.addItem(obj.render()); 345 obj.theme(); 346 } 347 this.themeUpdate(); 348 } 349 }, 350 351 /** 352 * Triggers the rendering engine, jQuery mobile, to style the selection list. 353 * 354 * @private 355 */ 356 theme: function() { 357 if(this.selectionMode === M.SINGLE_SELECTION_DIALOG || this.selectionMode === M.MULTIPLE_SELECTION_DIALOG) { 358 $('#' + this.id).selectmenu(); 359 if(this.selectionMode === M.MULTIPLE_SELECTION_DIALOG && this.initialText && this.selection && this.selection.length === 0) { 360 $('#' + this.id + '_container').find('.ui-btn-text').html(this.initialText); 361 } 362 } else if(this.selectionMode !== M.SINGLE_SELECTION_DIALOG && this.selectionMode !== M.MULTIPLE_SELECTION_DIALOG) { 363 $('#' + this.id).controlgroup(); 364 } 365 }, 366 367 /** 368 * Triggers the rendering engine, jQuery mobile, to style the selection list. 369 * 370 * @private 371 */ 372 themeUpdate: function() { 373 if(this.selectionMode === M.SINGLE_SELECTION_DIALOG || this.selectionMode === M.MULTIPLE_SELECTION_DIALOG) { 374 $('#' + this.id).selectmenu('refresh'); 375 if(this.selectionMode === M.MULTIPLE_SELECTION_DIALOG && this.initialText && this.selection && this.selection.length === 0) { 376 $('#' + this.id + '_container').find('.ui-btn-text').html(this.initialText); 377 } else if(this.selectionMode === M.SINGLE_SELECTION_DIALOG && !this.selection) { 378 var that = this; 379 var item = M.ViewManager.getViewById($('#' + this.id).find('option:first-child').attr('id')); 380 that.setSelection(item.value); 381 } 382 } else if(this.selectionMode !== M.SINGLE_SELECTION_DIALOG && this.selectionMode !== M.MULTIPLE_SELECTION_DIALOG) { 383 $('#' + this.id).controlgroup(); 384 } 385 }, 386 387 /** 388 * Method to append css styles inline to the rendered selection list. 389 * 390 * @private 391 * @returns {String} The selection list's styling as html representation. 392 */ 393 style: function() { 394 var html = ''; 395 if(this.cssClass) { 396 html += ' class="' + this.cssClass + '"'; 397 } 398 return html; 399 }, 400 401 /** 402 * This method is called everytime a item is selected / clicked. If the selected item 403 * changed, the defined onSelect action is triggered. 404 * 405 * @param {String} id The id of the selected item. 406 * @param {Object} event The event. 407 * @param {Object} nextEvent The application-side event handler. 408 */ 409 itemSelected: function(id, event, nextEvent) { 410 var item = null; 411 412 if(this.selectionMode === M.SINGLE_SELECTION) { 413 item = M.ViewManager.getViewById($('input[name=' + (this.name ? this.name : this.id) + ']:checked').attr('id')); 414 415 if(item !== this.selection) { 416 this.selection = item; 417 418 if(nextEvent) { 419 M.EventDispatcher.callHandler(nextEvent, event, NO, [this.selection.value, this.selection]); 420 } 421 } 422 } else if(this.selectionMode === M.SINGLE_SELECTION_DIALOG) { 423 item = M.ViewManager.getViewById($('#' + this.id + ' :selected').attr('id')); 424 425 if(item !== this.selection) { 426 this.selection = item; 427 428 $('#' + this.id + '_container').find('.ui-btn-text').html(item.label ? item.label : item.value); 429 430 if(nextEvent) { 431 M.EventDispatcher.callHandler(nextEvent, event, NO, [this.selection.value, this.selection]); 432 } 433 } 434 } else if(this.selectionMode === M.MULTIPLE_SELECTION) { 435 var that = this; 436 this.selection = []; 437 $('#' + this.id).find('input:checked').each(function() { 438 that.selection.push(M.ViewManager.getViewById($(this).attr('id'))); 439 }); 440 441 var selectionValues = []; 442 for(var i in this.selection) { 443 selectionValues.push(this.selection[i].value); 444 } 445 446 if(nextEvent) { 447 M.EventDispatcher.callHandler(nextEvent, event, NO, [selectionValues, this.selection]); 448 } 449 } else if(this.selectionMode === M.MULTIPLE_SELECTION_DIALOG) { 450 var that = this; 451 this.selection = []; 452 $('#' + this.id).find(':selected').each(function() { 453 that.selection.push(M.ViewManager.getViewById($(this).attr('id'))); 454 }); 455 456 var selectionValues = []; 457 for(var i in this.selection) { 458 selectionValues.push(this.selection[i].value); 459 $('#' + this.id + '_container').find('.ui-btn-text').html(this.formatSelectionLabel(this.selection.length)); 460 } 461 462 /* if there is no more item selected, reset the initial text */ 463 if(this.selection.length === 0) { 464 this.themeUpdate(); 465 } 466 467 if(nextEvent) { 468 M.EventDispatcher.callHandler(nextEvent, event, NO, [selectionValues, this.selection]); 469 } 470 } 471 }, 472 473 /** 474 * This method returns the selected item's value(s) either as a String (single selection) 475 * or as an Array (multiple selection). 476 * 477 * @param {Boolean} returnObject Determines whether to return the selected item(s) as object or not. 478 * @returns {String|Object|Array} The selected item's value(s). 479 */ 480 getSelection: function(returnObject) { 481 if(this.selectionMode === M.SINGLE_SELECTION || this.selectionMode === M.SINGLE_SELECTION_DIALOG) { 482 if(this.selection) { 483 if(returnObject) { 484 return this.selection; 485 } else { 486 return this.selection.value; 487 } 488 } 489 } else { 490 if(this.selection) { 491 var selection = []; 492 _.each(this.selection, function(item) { 493 if(returnObject) { 494 selection.push(item); 495 } else { 496 selection.push(item.value); 497 } 498 }); 499 return selection; 500 } 501 return []; 502 } 503 }, 504 505 /** 506 * This method can be used to select items programmatically. The given parameter can either 507 * be a String (single selection) or an Array (multiple selection). 508 * 509 * @param {String|Array} selection The selection that should be applied to the selection list. 510 */ 511 setSelection: function(selection) { 512 var that = this; 513 if(this.selectionMode === M.SINGLE_SELECTION && (typeof(selection) === 'string' || typeof(selection) === 'number' || typeof(selection) === 'boolean')) { 514 $('#' + this.id).find('input').each(function() { 515 var item = M.ViewManager.getViewById($(this).attr('id')); 516 if(item.value == selection) { 517 that.removeSelection(); 518 item.isSelected = YES; 519 that.selection = item; 520 $(this).attr('checked', 'checked'); 521 $(this).siblings('label:first').addClass('ui-radio-on'); 522 $(this).siblings('label:first').removeClass('ui-radio-off'); 523 $(this).siblings('label:first').find('span .ui-icon-radio-off').addClass('ui-icon-radio-on'); 524 $(this).siblings('label:first').find('span .ui-icon-radio-off').removeClass('ui-icon-radio-off'); 525 } 526 }); 527 } else if(this.selectionMode === M.SINGLE_SELECTION_DIALOG && (typeof(selection) === 'string' || typeof(selection) === 'number' || typeof(selection) === 'boolean')) { 528 var didSetSelection = NO; 529 $('#' + this.id).find('option').each(function() { 530 var item = M.ViewManager.getViewById($(this).attr('id')); 531 if(item.value == selection) { 532 that.removeSelection(); 533 item.isSelected = YES; 534 that.selection = item; 535 $('#' + that.id).val(item.value); 536 didSetSelection = YES; 537 } 538 }); 539 if(didSetSelection) { 540 $('#' + this.id).selectmenu('refresh'); 541 } 542 } else if(typeof(selection) === 'object') { 543 if(this.selectionMode === M.MULTIPLE_SELECTION) { 544 var removedItems = NO; 545 $('#' + this.id).find('input').each(function() { 546 var item = M.ViewManager.getViewById($(this).attr('id')); 547 for(var i in selection) { 548 var selectionItem = selection[i]; 549 if(item.value == selectionItem) { 550 if(!removedItems) { 551 that.removeSelection(); 552 removedItems = YES; 553 } 554 item.isSelected = YES; 555 that.selection.push(item); 556 $(this).attr('checked', 'checked'); 557 $(this).siblings('label:first').removeClass('ui-checkbox-off'); 558 $(this).siblings('label:first').addClass('ui-checkbox-on'); 559 $(this).siblings('label:first').find('span .ui-icon-checkbox-off').addClass('ui-icon-checkbox-on'); 560 $(this).siblings('label:first').find('span .ui-icon-checkbox-off').removeClass('ui-icon-checkbox-off'); 561 } 562 } 563 }); 564 } else if(this.selectionMode === M.MULTIPLE_SELECTION_DIALOG) { 565 var removedItems = NO; 566 $('#' + this.id).find('option').each(function() { 567 var item = M.ViewManager.getViewById($(this).attr('id')); 568 for(var i in selection) { 569 var selectionItem = selection[i]; 570 if(item.value == selectionItem) { 571 if(!removedItems) { 572 that.removeSelection(); 573 removedItems = YES; 574 } 575 item.isSelected = YES; 576 that.selection.push(item); 577 $(this).attr('selected', 'selected'); 578 } 579 } 580 581 /* set the label */ 582 $('#' + that.id + '_container').find('.ui-btn-text').html(that.formatSelectionLabel(that.selection.length)); 583 }); 584 } 585 } 586 that.theme(); 587 }, 588 589 /** 590 * This method de-selects all of the selection list's items. 591 */ 592 removeSelection: function() { 593 var that = this; 594 595 if(this.selectionMode === M.SINGLE_SELECTION || this.selectionMode === M.SINGLE_SELECTION_DIALOG) { 596 this.selection = null; 597 } else { 598 this.selection = []; 599 } 600 601 if(this.selectionMode !== M.SINGLE_SELECTION_DIALOG && this.selectionMode !== M.MULTIPLE_SELECTION_DIALOG) { 602 $('#' + this.id).find('input').each(function() { 603 var item = M.ViewManager.getViewById($(this).attr('id')); 604 item.isSelected = NO; 605 $(this).removeAttr('checked'); 606 $(this).siblings('label:first').addClass('ui-' + that.selectionMode + '-off'); 607 $(this).siblings('label:first').removeClass('ui-' + that.selectionMode + '-on'); 608 $(this).siblings('label:first').find('span .ui-icon-' + that.selectionMode + '-on').addClass('ui-icon-' + that.selectionMode + '-off'); 609 $(this).siblings('label:first').find('span .ui-icon-' + that.selectionMode + '-on').removeClass('ui-icon-' + that.selectionMode + '-on'); 610 }); 611 } else { 612 $('#' + this.id).find('option').each(function() { 613 var item = M.ViewManager.getViewById($(this).attr('id')); 614 item.isSelected = NO; 615 }); 616 $('#' + this.id).val('').removeAttr('checked').removeAttr('selected'); 617 } 618 }, 619 620 /** 621 * This method can be used to reset the selection list. This basically discards 622 * all changes made to the selection by the user or any application-sided calls 623 * and applies the original state. 624 * 625 * The 'original state' can either be the bound content or the state, specified 626 * by the originally assigned child views. 627 */ 628 resetSelection: function() { 629 if(this.contentBinding) { 630 this.removeSelection(); 631 this.renderUpdate(); 632 } else { 633 this.contentBinding = {}; 634 this.contentBinding.target = this; 635 this.contentBinding.property = 'initialState'; 636 this.removeSelection(); 637 this.renderUpdate(); 638 this.contentBinding = null; 639 } 640 }, 641 642 /** 643 * We use this as alias for the form reset function view.clearValues() to reset the selection to its initial state 644 */ 645 clearValue: function(){ 646 this.resetSelection(); 647 }, 648 649 /** 650 * This method returns the selection list view's value. 651 * 652 * @returns {String|Array} The selected item's value(s). 653 */ 654 getValue: function() { 655 return this.getSelection(); 656 }, 657 658 /** 659 * This method is responsible for rendering the visual text for a selection list 660 * in the M.MULTIPLE_SELECTION_DIALOG mode. It's only parameter is a number, that 661 * specifies the number of selected options of this selection list. To customize 662 * the visual output of such a list, you will need to overwrite this method within 663 * the definition of the selection list in your application. 664 * 665 * @param {Number} v The number of selected options. 666 */ 667 formatSelectionLabel: function(v) { 668 return v + ' Object(s)'; 669 }, 670 671 /** 672 * This method disables the selection list by setting the disabled property of its 673 * html representation to true. 674 */ 675 disable: function() { 676 this.isEnabled = NO; 677 if(this.selectionMode === M.SINGLE_SELECTION || this.selectionMode === M.MULTIPLE_SELECTION) { 678 $('#' + this.id).find('input').each(function() { 679 $(this).checkboxradio('disable'); 680 }); 681 } else { 682 $('#' + this.id).select('disable'); 683 $('#' + this.id).each(function() { 684 $(this).attr('disabled', 'disabled'); 685 }); 686 } 687 }, 688 689 /** 690 * This method enables the selection list by setting the disabled property of its 691 * html representation to false. 692 */ 693 enable: function() { 694 this.isEnabled = YES; 695 if(this.selectionMode === M.SINGLE_SELECTION || this.selectionMode === M.MULTIPLE_SELECTION) { 696 $('#' + this.id).find('input').each(function() { 697 $(this).checkboxradio('enable'); 698 }); 699 } else { 700 $('#' + this.id).select('enable'); 701 $('#' + this.id).each(function() { 702 $(this).removeAttr('disabled'); 703 }); 704 } 705 } 706 707 });