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: 02.12.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 horizontal alignment. 14 * 15 * @type String 16 */ 17 M.HORIZONTAL = 'horizontal'; 18 19 /** 20 * A constant value for vertical alignment. 21 * 22 * @type String 23 */ 24 M.VERTICAL = 'vertical'; 25 26 27 /** 28 * @class 29 * 30 * A button group is a vertically or / and horizontally aligned group of buttons. There 31 * are basically three different types of a button group: 32 * 33 * - horizontally aligned buttons 34 * 1 - 2 - 3 35 * 36 * - vertically aligned buttons 37 * 1 38 * | 39 * 2 40 * | 41 * 3 42 * 43 * - horizontally and vertically aligned buttons 44 * 1 - 2 45 * | | 46 * 3 - 4 47 * 48 * @extends M.View 49 */ 50 M.ButtonGroupView = M.View.extend( 51 /** @scope M.ButtonGroupView.prototype */ { 52 53 /** 54 * The type of this object. 55 * 56 * @type String 57 */ 58 type: 'M.ButtonGroupView', 59 60 /** 61 * This property determines whether to render the button group horizontally 62 * or vertically. Default: horizontal. 63 * 64 * Possible values are: 65 * - M.HORIZONTAL: horizontal 66 * - M.VERTICAL: vertical 67 * 68 * @type String 69 */ 70 direction: M.HORIZONTAL, 71 72 /** 73 * Determines whether to display the button group view 'inset' or at full width. 74 * 75 * @type Boolean 76 */ 77 isInset: YES, 78 79 /** 80 * Determines whether to display the button group compact, i.e. without top/bottom 81 * margin. This property only is relevant in combination with multiple lines of 82 * buttons (c.p.: buttonsPerLine property). 83 * 84 * @type Boolean 85 */ 86 isCompact: YES, 87 88 /** 89 * This property, if set, defines how many buttons are rendered per line. If there 90 * are more buttons defined that fitting into one line, the following buttons are 91 * rendered into a new line. Make sure, the number of your buttons is divisible by 92 * the number of buttons per line, since only full lines are displayed. So if you 93 * for example specify 5 buttons and 2 buttons per line, the fifth button won't be 94 * visible. 95 * 96 * If e.g. 4 buttons are specified and this property is set to 2, the rendering will 97 * be as follows: 98 * 99 * 1 -- 2 100 * 3 -- 4 101 * 102 * @type Number 103 */ 104 buttonsPerLine: null, 105 106 /** 107 * This property is used to internally store the number of lines that are necessary 108 * to render all buttons according to the buttonsPerLine property. 109 * 110 * @private 111 * @type Number 112 */ 113 numberOfLines: null, 114 115 /** 116 * This property refers to the currently rendered line, if there is more than one. 117 * 118 * @private 119 * @type Number 120 */ 121 currentLine: null, 122 123 /** 124 * This property contains an array of html ids referring to the several lines of grouped 125 * buttons, if there is more than one at all. 126 * 127 * @private 128 * @type Array 129 */ 130 lines: null, 131 132 /** 133 * This property contains a reference to the currently selected button. 134 * 135 * @private 136 * @type Object 137 */ 138 activeButton: null, 139 140 /** 141 * This property determines whether the buttons of this button group are selectable or not. If 142 * set to YES, a click on one of the buttons will set this button as the currently active button 143 * and automatically change its styling to visualize its selection. 144 * 145 * @type Boolean 146 */ 147 isSelectable: YES, 148 149 /** 150 * This property specifies the recommended events for this type of view. 151 * 152 * @type Array 153 */ 154 recommendedEvents: ['change'], 155 156 /** 157 * Renders a button group as a div container and calls the renderChildViews 158 * method to render the included buttons. 159 * 160 * @private 161 * @returns {String} The button group view's html representation. 162 */ 163 render: function() { 164 /* check if multiple lines are necessary before rendering */ 165 if(this.childViews) { 166 var childViews = this.getChildViewsAsArray(); 167 if(this.buttonsPerLine && this.buttonsPerLine < childViews.length) { 168 var numberOfButtons = 0; 169 for(var i in childViews) { 170 if(this[childViews[i]] && this[childViews[i]].type === 'M.ButtonView') { 171 numberOfButtons = numberOfButtons + 1; 172 } 173 } 174 if(this.buttonsPerLine < numberOfButtons) { 175 this.numberOfLines = M.Math.round(numberOfButtons / this.buttonsPerLine, M.FLOOR); 176 } 177 } 178 } 179 180 /* if there are multiple lines, render multiple horizontally aligned button groups */ 181 if(this.numberOfLines) { 182 /* set the direction to horizontally, no matter what it was set to before */ 183 this.direction = M.HORIZONTAL; 184 185 /* this is a wrapper for the multiple button groups. 186 if it is not inset, assign css class 'ui-listview' for clearing the padding of the surrounding element */ 187 this.html += '<div id="' + this.id + '"'; 188 this.html += this.style(); 189 this.html += '>'; 190 191 /* create a button group for every line */ 192 this.lines = []; 193 for(var i = 0; i < this.numberOfLines; i++) { 194 this.currentLine = i + 1; 195 /* store current line in lines property for use in renderChildViews() */ 196 this.lines.push(this.id + '_' + i); 197 198 this.html += '<div data-role="controlgroup" href="#" id="' + this.id + '_' + i + '" data-type="' + this.direction + '"'; 199 200 /* if isCompact, assign specific margin, depending on line number (first, last, other) */ 201 if(!this.isInset || this.isCompact) { 202 if(i == 0) { 203 this.html += this.isInset ? ' style="margin-bottom:0px"' : ' style="margin:0px"'; 204 } else if(i > 0 && i < this.numberOfLines - 1) { 205 this.html += this.isInset ? ' style="margin:0px 0px 0px 0px"' : ' style="margin:0px"'; 206 } else if(i < this.numberOfLines) { 207 this.html += this.isInset ? ' style="margin-top:0px"' : ' style="margin:0px"'; 208 } 209 } 210 211 this.html += '>'; 212 213 /* render the buttons for the current line */ 214 this.renderChildViews(); 215 216 this.html += '</div>'; 217 } 218 this.html += '</div>'; 219 } else { 220 this.html += '<div data-role="controlgroup" href="#" id="' + this.id + '" data-type="' + this.direction + '"' + this.style() + '>'; 221 222 this.renderChildViews(); 223 224 this.html += '</div>'; 225 } 226 227 return this.html; 228 }, 229 230 /** 231 * Triggers render() on all children of type M.ButtonGroupView. 232 * 233 * @private 234 */ 235 renderChildViews: function() { 236 if(this.childViews) { 237 var childViews = this.getChildViewsAsArray(); 238 var currentButtonIndex = 0; 239 240 for(var i in childViews) { 241 if(this[childViews[i]] && this[childViews[i]].type === 'M.ButtonView') { 242 currentButtonIndex = currentButtonIndex + 1; 243 244 if(!this.numberOfLines || M.Math.round(currentButtonIndex / this.buttonsPerLine, M.CEIL) === this.currentLine) { 245 246 var button = this[childViews[i]]; 247 /* reset buttons html, to make sure it doesn't get rendered twice if this is multi button group */ 248 button.html = ''; 249 250 button.parentView = this; 251 button.internalEvents = { 252 tap: { 253 target: this, 254 action: 'buttonSelected' 255 } 256 } 257 258 /* if the buttons are horizontally aligned, compute their width depending on the number of buttons 259 and set the right margin to '-2px' since the jQuery mobile default would cause an ugly gap to 260 the right of the button group */ 261 if(this.direction === M.HORIZONTAL) { 262 button.cssStyle = 'margin-right:-2px;width:' + 100 / (this.numberOfLines ? this.buttonsPerLine : childViews.length) + '%'; 263 } 264 265 /* set the button's _name property */ 266 this[childViews[i]]._name = childViews[i]; 267 268 /* finally render the button and add it to the button groups html */ 269 this.html += this[childViews[i]].render(); 270 } 271 } else { 272 M.Logger.log('childview of button group is no button.', M.WARN); 273 } 274 } 275 } 276 }, 277 278 /** 279 * This method themes the button group and activates one of the included buttons 280 * if its isActive property is set. 281 * 282 * @private 283 */ 284 theme: function() { 285 /* if there are multiple lines of buttons, it's getting heavy */ 286 if(this.numberOfLines) { 287 288 /* iterate through all lines */ 289 for(var line in this.lines) { 290 line = parseInt(line); 291 292 /* style the current line */ 293 $('#' + this.lines[line]).controlgroup(); 294 var childViews = this.getChildViewsAsArray(); 295 var currentButtonIndex = 0; 296 297 /* if isCompact, iterate through all buttons */ 298 if(this.isCompact) { 299 for(var i in childViews) { 300 i = parseInt(i); 301 if(this[childViews[i]] && this[childViews[i]].type === 'M.ButtonView') { 302 currentButtonIndex = currentButtonIndex + 1; 303 var currentLine = M.Math.round(currentButtonIndex / this.buttonsPerLine, M.CEIL) - 1; 304 var button = this[childViews[i]]; 305 306 /* if the button belongs to the current line adjust its styling according to its position, 307 e.g. the first button in the first row gets the css class 'ui-corner-tl' (top left). */ 308 if(line === currentLine) { 309 310 /* first line */ 311 if(line === 0 && this.numberOfLines > 1) { 312 /* first button */ 313 if(currentButtonIndex === 1) { 314 $('#' + button.id).removeClass('ui-corner-left'); 315 if(this.isInset) { 316 $('#' + button.id).addClass('ui-corner-tl'); 317 } 318 /* last button */ 319 } else if(currentButtonIndex === this.buttonsPerLine) { 320 $('#' + button.id).removeClass('ui-corner-right'); 321 if(this.isInset) { 322 $('#' + button.id).addClass('ui-corner-tr'); 323 } 324 } 325 /* last line */ 326 } else if(line === this.numberOfLines - 1) { 327 /* first button */ 328 if(currentButtonIndex === (currentLine * this.buttonsPerLine) + 1) { 329 $('#' + button.id).removeClass('ui-corner-left'); 330 $('#' + button.id).addClass('ui-corner-bl'); 331 /* last button */ 332 } else if(currentButtonIndex === ((currentLine + 1) * this.buttonsPerLine)) { 333 $('#' + button.id).removeClass('ui-corner-right'); 334 $('#' + button.id).addClass('ui-corner-br'); 335 } 336 /* all other lines */ 337 } else { 338 /* first button */ 339 if(currentButtonIndex === (currentLine * this.buttonsPerLine) + 1) { 340 $('#' + button.id).removeClass('ui-corner-left'); 341 /* last button */ 342 } else if(currentButtonIndex === ((currentLine + 1) * this.buttonsPerLine)) { 343 $('#' + button.id).removeClass('ui-corner-right'); 344 } 345 } 346 } 347 } 348 } 349 } 350 } 351 /* if there is only on row, simply style that button group */ 352 } else { 353 $('#' + this.id).controlgroup(); 354 } 355 356 /* iterate through all buttons and activate on of them, according to the button's isActive property */ 357 if(this.childViews) { 358 var childViews = this.getChildViewsAsArray(); 359 for(var i in childViews) { 360 if(this[childViews[i]] && this[childViews[i]].type === 'M.ButtonView') { 361 var button = this[childViews[i]]; 362 if(button.isActive) { 363 this.setActiveButton(button.id); 364 } 365 } 366 } 367 } 368 }, 369 370 /** 371 * This method returns the currently selected button of this button group. If no 372 * button is selected, null is returned. 373 * 374 * @returns {M.ButtonView} The currently active button of this button group. 375 */ 376 getActiveButton: function() { 377 return this.activeButton; 378 }, 379 380 /** 381 * This method activates one button within the button group. 382 * 383 * @param {M.ButtonView, String} button The button to be set active or its id. 384 */ 385 setActiveButton: function(button) { 386 if(this.isSelectable) { 387 if(this.activeButton) { 388 this.activeButton.removeCssClass('ui-btn-active'); 389 this.activeButton.isActive = NO; 390 } 391 392 var obj = M.ViewManager.getViewById(button); 393 if(!obj) { 394 if(button && typeof(button) === 'object' && button.type === 'M.ButtonView') { 395 obj = button; 396 } 397 } 398 if(obj) { 399 obj.addCssClass('ui-btn-active'); 400 obj.isActive = YES; 401 this.activeButton = obj; 402 } 403 } 404 }, 405 406 /** 407 * This method activates one button within the button group at the given index. 408 * 409 * @param {Number} index The index of the button to be set active. 410 */ 411 setActiveButtonAtIndex: function(index) { 412 if(this.childViews) { 413 var childViews = this.getChildViewsAsArray(); 414 var button = this[childViews[index]]; 415 if(button && button.type === 'M.ButtonView') { 416 this.setActiveButton(button); 417 } 418 } 419 }, 420 421 /** 422 * This method is called everytime a button is activated / clicked. 423 * 424 * @private 425 * @param {String} id The id of the selected item. 426 * @param {Object} event The event. 427 * @param {Object} nextEvent The application-side event handler. 428 */ 429 buttonSelected: function(id, event, nextEvent) { 430 /* if selected button is disabled, do nothing */ 431 if(M.ViewManager.getViewById(id) && M.ViewManager.getViewById(id).type === 'M.ButtonView' && !M.ViewManager.getViewById(id).isEnabled) { 432 return; 433 } 434 435 if(!(this.activeButton && this.activeButton === M.ViewManager.getViewById(id))) { 436 if(this.isSelectable) { 437 if(this.activeButton) { 438 this.activeButton.removeCssClass('ui-btn-active'); 439 this.activeButton.isActive = NO; 440 } 441 442 var button = M.ViewManager.getViewById(id); 443 if(!button) { 444 if(id && typeof(id) === 'object' && id.type === 'M.ButtonView') { 445 button = id; 446 } 447 } 448 if(button) { 449 button.addCssClass('ui-btn-active'); 450 button.isActive = YES; 451 this.activeButton = button; 452 } 453 } 454 455 /* trigger change event for the button group */ 456 $('#' + this.id).trigger('change'); 457 } 458 459 /* delegate event to external handler, if specified */ 460 if(nextEvent) { 461 M.EventDispatcher.callHandler(nextEvent, event, YES); 462 } 463 }, 464 465 /** 466 * Applies some style-attributes to the button group. 467 * 468 * @private 469 * @returns {String} The button group's styling as html representation. 470 */ 471 style: function() { 472 var html = ''; 473 if(this.numberOfLines && !this.isInset) { 474 html += ' class="ui-listview'; 475 } 476 if(this.cssClass) { 477 html += html !== '' ? ' ' + this.cssClass : ' class="' + this.cssClass; 478 } 479 html += '"'; 480 return html; 481 } 482 483 });