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