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