1 // ==========================================================================
  2 // Project:   The M-Project - Mobile HTML5 Application Framework
  3 // Copyright: (c) 2011 panacoda GmbH. All rights reserved.
  4 // Creator:   Dominik
  5 // Date:      09.08.2011
  6 // License:   Dual licensed under the MIT or GPL Version 2 licenses.
  7 //            http://github.com/mwaylabs/The-M-Project/blob/master/MIT-LICENSE
  8 //            http://github.com/mwaylabs/The-M-Project/blob/master/GPL-LICENSE
  9 // ==========================================================================
 10 
 11 /**
 12  * @class
 13  *
 14  * A dashboard view displays images and a corresponding text in a grid-like view
 15  * and serves as the homescreen of an application. By tapping on of the icons, a
 16  * user can access certain features of an app. By default, there are three icons
 17  * in a row and three rows per page possible. But you can easily adjust this to
 18  * your custom needs.
 19  *
 20  * @extends M.View
 21  */
 22 M.DashboardView = M.View.extend(
 23 /** @scope M.DashboardView.prototype */ {
 24 
 25     /**
 26      * The type of this object.
 27      *
 28      * @type String
 29      */
 30     type: 'M.DashboardView',
 31 
 32     /**
 33      * This property can be used to customize the number of items a dashboard
 34      * shows per line. By default this is set to three.
 35      *
 36      * @type Number
 37      */
 38     itemsPerLine: 3,
 39 
 40     /**
 41      * This property specifies the recommended events for this type of view.
 42      *
 43      * @type Array
 44      */
 45     recommendedEvents: ['click', 'tap'],
 46 
 47     /**
 48      * This property is used internally for storing the items of a dashboard, when using
 49      * the content binding feature.
 50      *
 51      * @private
 52      */
 53     items: [],
 54 
 55     /**
 56      * This property can be used to specify whether or not the dashboard can be re-arranged
 57      * by a user.
 58      *
 59      * @type Boolean
 60      */
 61     isEditable: NO,
 62 
 63     /**
 64      * This property is used internally to indicate whether the dashboard is currently in
 65      * edit mode or not.
 66      *
 67      * @private
 68      * @type Boolean
 69      */
 70     isInEditMode: NO,
 71 
 72     /**
 73      * This property defines the dashboard's name. This is used internally to identify
 74      * the dashboard inside the DOM.
 75      *
 76      * Note: If you are using more than one dashboard inside your application, make sure
 77      * you provide different names.
 78      *
 79      * @type String
 80      */
 81     name: 'dashboard',
 82 
 83     /**
 84      * This property is used internally to track the position of touch events.
 85      *
 86      * @private
 87      */
 88     touchPositions: null,
 89 
 90     /**
 91      * This property is used internally to know of what type the latest touch events was.
 92      *
 93      * @private
 94      */
 95     latestTouchEventType: null,
 96 
 97     /**
 98      * Renders a dashboard.
 99      *
100      * @private
101      * @returns {String} The dashboard view's html representation.
102      */
103     render: function() {
104         this.html += '<div id="' + this.id + '"' + this.style() + '>';
105         this.renderChildViews();
106         this.html += '</div>';
107 
108         /* clear floating */
109         this.html += '<div class="tmp-dashboard-line-clear"></div>';
110 
111         /* init the touchPositions property */
112         this.touchPositions = {};
113 
114         return this.html;
115     },
116 
117     renderChildViews: function() {
118         if(this.childViews) {
119             var childViews = this.getChildViewsAsArray();
120 
121             /* lets gather the html together */
122             for(var i in childViews) {
123                 /* set the dashboard item's _name and parentView property */
124                 this[childViews[i]].parentView = this;
125                 this[childViews[i]]._name = childViews[i];
126 
127                 this.html += this.renderDashboardItemView(this[childViews[i]], i);
128             }
129         }
130     },
131 
132     renderUpdate: function() {
133         if(this.contentBinding) {
134             this.removeAllItems();
135 
136             /* do we have something in locale storage? */
137             var values = localStorage.getItem(M.LOCAL_STORAGE_PREFIX + M.Application.name + M.LOCAL_STORAGE_SUFFIX + 'dashboard');
138             values = values ? JSON.parse(values) : null;
139 
140             /* get the items (if there is something in the LS and it fits the content bound values, use them) */
141             this.items = [];
142             var items = (values && this.value && values.length == this.value.length) ? this.sortItemsByValues(this.value, values) : this.value;
143             var html = '';
144 
145             /* lets gather the html together */
146             for(var i in items) {
147                 html += this.renderDashboardItemView(items[i], i);
148             }
149 
150             /* add the items to the DOM */
151             this.addItems(html);
152 
153             /* now the items are in DOM, finally register events */
154             for(var i in this.items) {
155                 this.items[i].registerEvents();
156             }
157         }
158     },
159 
160     /**
161      * This method adds a given html string, contain the dasboard's items, to the DOM.
162      *
163      * @param {String} item The html representation of the dashboard items to be added.
164      */
165     addItems: function(items) {
166         $('#' + this.id).append(items);
167     },
168 
169     /**
170      * This method removes all of the dashboard view's items by removing all of its content in the DOM. This
171      * method is based on jQuery's empty().
172      */
173     removeAllItems: function() {
174         $('#' + this.id).empty();
175     },
176 
177     renderDashboardItemView: function(item, itemIndex) {
178         if(item && item.value && item.icon) {
179             var obj = item.type === 'M.DashboardItemView' ? item : M.DashboardItemView.design({
180                 value: item.value ? item.value : '',
181                 icon: item.icon ? item.icon : '',
182                 label: item.label ? item.label : (item.value ? item.value : ''),
183                 parentView: this,
184                 events: item.events
185             });
186             var html = '';
187 
188             /* add item to array for later use */
189             this.items.push(obj);
190 
191             /* is new line starting? */
192             if(itemIndex % this.itemsPerLine === 0) {
193                 //html += '<div class="tmp-dashboard-line">';
194             }
195 
196             /* assign the desired width */
197             obj.cssStyle = 'width: ' + 100/this.itemsPerLine + '%';
198 
199             /* finally render the dashboard item and add it to the dashboard's html */
200             html += obj.render();
201 
202             /* is a line finished? */
203             if(itemIndex % this.itemsPerLine === this.itemsPerLine - 1) {
204                 //html += '</div><div class="tmp-dashboard-line-clear"></div>';
205             }
206 
207             /* return the html */
208             return html;
209         } else {
210             M.Logger.log('Childview of dashboard is no valid dashboard item.', M.WARN);
211         }
212     },
213 
214     /**
215      * This method is used internally for dispatching the tap event for a dashboard view. If the
216      * dashboard view is in edit mode, we do not dispatch the event to the application.
217      *
218      * @param {String} id The DOM id of the event target.
219      * @param {Object} event The DOM event.
220      * @param {Object} nextEvent The next event (external event), if specified.
221      *
222      * @private
223      */
224     dispatchTapEvent: function(id, event, nextEvent) {
225         /* now first call special handler for this item */
226         if(nextEvent) {
227             M.EventDispatcher.callHandler(nextEvent, event, YES);
228         }
229 
230         /* now call global tap-event handler (if set) */
231         if(this.events && this.events.tap) {
232             M.EventDispatcher.callHandler(this.events.tap, event, YES);
233         }
234 
235         /* now store timestamp for last tap event to kill a possible false taphold event */
236         this.latestTapEventTimestamp = +new Date();
237     },
238 
239     /**
240      * This method is automatically called when a taphold event is triggered for one
241      * of the dashboard's
242      */
243     editDashboard: function(id, event, nextEvent) {
244         this.touchPositions.touchstart = {};
245         if(!this.isEditable || this.latestTapEventTimestamp > +new Date() - 500) {
246             return;
247         }
248 
249         if(this.isInEditMode && event) {
250             this.stopEditMode();
251         } else if((!this.isInEditMode && event) || (this.isInEditMode && !event)) {
252             M.EventDispatcher.unregisterEvents(this.id);
253             this.isInEditMode = YES;
254             _.each(this.items, function(item) {
255                 item.addCssClass('rotate' + M.Math.random(1, 2));
256                 M.EventDispatcher.unregisterEvents(item.id);
257                 if($.support.touch) {
258                     M.EventDispatcher.registerEvent(
259                         'touchstart',
260                         item.id,
261                         {
262                             target: item.parentView,
263                             action: 'editTouchStart'
264                         },
265                         item.recommendedEvents
266                     );
267                     M.EventDispatcher.registerEvent(
268                         'touchend',
269                         item.id,
270                         {
271                             target: item.parentView,
272                             action: 'editTouchEnd'
273                         },
274                         item.recommendedEvents
275                     );
276                     M.EventDispatcher.registerEvent(
277                         'touchmove',
278                         item.id,
279                         {
280                             target: item.parentView,
281                             action: 'editTouchMove'
282                         },
283                         item.recommendedEvents
284                     );
285                 } else {
286                     M.EventDispatcher.registerEvent(
287                         'mousedown',
288                         item.id,
289                         {
290                             target: item.parentView,
291                             action: 'editMouseDown'
292                         },
293                         item.recommendedEvents
294                     );
295                     M.EventDispatcher.registerEvent(
296                         'mouseup',
297                         item.id,
298                         {
299                             target: item.parentView,
300                             action: 'editMouseUp'
301                         },
302                         item.recommendedEvents
303                     );
304                 }
305             });
306         }
307     },
308 
309     stopEditMode: function() {
310         this.isInEditMode = NO;
311         _.each(this.items, function(item) {
312             item.removeCssClass('rotate1');
313             item.removeCssClass('rotate2');
314             M.EventDispatcher.unregisterEvents(item.id);
315             item.registerEvents();
316         });
317     },
318 
319     setValue: function(items) {
320         this.value = items;
321         var values = [];
322         _.each(items, function(item) {
323             values.push(item.value);
324         });
325         if(localStorage) {
326             localStorage.setItem(M.LOCAL_STORAGE_PREFIX + M.Application.name + M.LOCAL_STORAGE_SUFFIX + 'dashboard', JSON.stringify(values));
327         }
328     },
329 
330     sortItemsByValues: function(items, values) {
331         var itemsSorted = [];
332         _.each(values, function(value) {
333             _.each(items, function(item) {
334                 if(item.value === value) {
335                     itemsSorted.push(item);
336                 }
337             });
338         });
339         return itemsSorted;
340     },
341 
342     editTouchStart: function(id, event) {
343         this.latestTouchEventType = 'touchstart';
344         var latest = event.originalEvent ? (event.originalEvent.changedTouches ? event.originalEvent.changedTouches[0] : null) : null;
345         
346         this.touchPositions.touchstart = {
347             x: latest.clientX,
348             y: latest.clientY,
349             date: +new Date()
350         };
351 
352         var that = this;
353         window.setTimeout(function() {
354             if(that.latestTouchEventType === 'touchstart') {
355                 that.stopEditMode();
356             }
357         }, 750);
358     },
359 
360     editTouchMove: function(id, event) {
361         this.latestTouchEventType = 'touchmove';
362         var latest = event.originalEvent ? (event.originalEvent.changedTouches ? event.originalEvent.changedTouches[0] : null) : null;
363 
364         if(latest) {
365             var left = latest.pageX - parseInt($('#' + id).css('width')) / 2;
366             var top = latest.pageY - parseInt($('#' + id).css('height')) / 2;
367             $('#' + id).css('position', 'absolute');
368             $('#' + id).css('left', left + 'px');
369             $('#' + id).css('top', top + 'px');
370 
371             /* if end event is within certain radius of start event and it took a certain time, and editing */
372             /*if(this.touchPositions.touchstart) {
373                 if(this.touchPositions.touchstart.date < +new Date() - 1500) {
374                     if(Math.abs(this.touchPositions.touchstart.x - latest.clientX) < 30 && Math.abs(this.touchPositions.touchstart.y - latest.clientY) < 30) {
375                         this.stopEditMode();
376                         this.editTouchEnd(id, event);
377                     }
378                 }
379             }*/
380         }
381     },
382 
383     editTouchEnd: function(id, event) {
384         this.latestTouchEventType = 'touchend';
385         var latest = event.originalEvent ? (event.originalEvent.changedTouches ? event.originalEvent.changedTouches[0] : null) : null;
386         
387         if(event.currentTarget.id) {
388             var items = [];
389             _.each(this.items, function(item) {
390                 items.push({
391                     id: item.id,
392                     x: $('#' + item.id).position().left,
393                     y: $('#' + item.id).position().top,
394                     item: item
395                 });
396                 items.sort(function(a, b) {
397                     /* assume they are in one row */
398                     if(Math.abs(a.y - b.y) < 30) {
399                         if(a.x < b.x) {
400                             return -1;
401                         } else {
402                             return 1;
403                         }
404                     /* otherwise */
405                     } else {
406                         if(a.y < b.y) {
407                             return -1;
408                         } else {
409                             return 1;
410                         }
411                     }
412                 });
413             });
414             var objs = [];
415             _.each(items, function(item) {
416                 objs.push(item.item);
417             });
418             this.setValue(objs);
419             this.renderUpdate();
420 
421             if(this.isInEditMode) {
422                 this.editDashboard();
423             }
424         }
425     },
426 
427     editMouseDown: function(id, event) {
428         this.latestTouchEventType = 'mousedown';
429 
430         this.touchPositions.touchstart = {
431             x: event.clientX,
432             y: event.clientY,
433             date: +new Date()
434         };
435 
436         /* enable mouse move for selected item */
437         M.EventDispatcher.registerEvent(
438             'mousemove',
439             id,
440             {
441                 target: this,
442                 action: 'editMouseMove'
443             },
444             M.ViewManager.getViewById(id).recommendedEvents
445         );
446 
447         var that = this;
448         window.setTimeout(function() {
449             if(that.latestTouchEventType === 'mousedown') {
450                 that.stopEditMode();
451             }
452         }, 750);
453     },
454 
455     editMouseMove: function(id, event) {
456         this.latestTouchEventType = 'mousemove';
457 
458         var left = event.pageX - parseInt($('#' + id).css('width')) / 2;
459         var top = event.pageY - parseInt($('#' + id).css('height')) / 2;
460         $('#' + id).css('position', 'absolute');
461         $('#' + id).css('left', left + 'px');
462         $('#' + id).css('top', top + 'px');
463 
464         /* if end event is within certain radius of start event and it took a certain time, and editing */
465         /*if(this.touchPositions.touchstart) {
466             if(this.touchPositions.touchstart.date < +new Date() - 1500) {
467                 if(Math.abs(this.touchPositions.touchstart.x - latest.clientX) < 30 && Math.abs(this.touchPositions.touchstart.y - latest.clientY) < 30) {
468                     this.stopEditMode();
469                     this.editTouchEnd(id, event);
470                 }
471             }
472         }*/
473     },
474 
475     editMouseUp: function(id, event) {
476         this.latestTouchEventType = 'mouseup';
477 
478         if(event.currentTarget.id) {
479             var items = [];
480             _.each(this.items, function(item) {
481 
482                 /* disable mouse move for all item */
483                 M.EventDispatcher.unregisterEvent('mousemove', item.id);
484 
485                 items.push({
486                     id: item.id,
487                     x: $('#' + item.id).position().left,
488                     y: $('#' + item.id).position().top,
489                     item: item
490                 });
491                 items.sort(function(a, b) {
492                     /* assume they are in one row */
493                     if(Math.abs(a.y - b.y) < 30) {
494                         if(a.x < b.x) {
495                             return -1;
496                         } else {
497                             return 1;
498                         }
499                     /* otherwise */
500                     } else {
501                         if(a.y < b.y) {
502                             return -1;
503                         } else {
504                             return 1;
505                         }
506                     }
507                 });
508             });
509             var objs = [];
510             _.each(items, function(item) {
511                 objs.push(item.item);
512             });
513             this.setValue(objs);
514             this.renderUpdate();
515 
516             if(this.isInEditMode) {
517                 this.editDashboard();
518             }
519         }
520     },
521 
522     /**
523      * Applies some style-attributes to the dashboard view.
524      *
525      * @private
526      * @returns {String} The dashboard's styling as html representation.
527      */
528     style: function() {
529         var html = '';
530         if(this.cssClass) {
531             html += ' class="tmp-dashboard ' + this.cssClass + '"';
532         }
533         return html;
534     }
535 
536 });