OLD | NEW |
1 // Copyright (c) 2010 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2010 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 var MAX_APPS_PER_ROW = []; | 5 var MAX_APPS_PER_ROW = []; |
6 MAX_APPS_PER_ROW[LayoutMode.SMALL] = 4; | 6 MAX_APPS_PER_ROW[LayoutMode.SMALL] = 4; |
7 MAX_APPS_PER_ROW[LayoutMode.NORMAL] = 6; | 7 MAX_APPS_PER_ROW[LayoutMode.NORMAL] = 6; |
8 | 8 |
9 // The URL prefix used in the app link 'ping' attributes. | 9 // The URL prefix used in the app link 'ping' attributes. |
10 var PING_APP_LAUNCH_PREFIX = 'record-app-launch'; | 10 var PING_APP_LAUNCH_PREFIX = 'record-app-launch'; |
(...skipping 27 matching lines...) Expand all Loading... |
38 $('apps-create-shortcut-command-separator').style.display = | 38 $('apps-create-shortcut-command-separator').style.display = |
39 (data.disableCreateAppShortcut ? 'none' : ''); | 39 (data.disableCreateAppShortcut ? 'none' : ''); |
40 | 40 |
41 appsMiniview.textContent = ''; | 41 appsMiniview.textContent = ''; |
42 appsSectionContent.textContent = ''; | 42 appsSectionContent.textContent = ''; |
43 | 43 |
44 data.apps.sort(function(a,b) { | 44 data.apps.sort(function(a,b) { |
45 return a.app_launch_index - b.app_launch_index; | 45 return a.app_launch_index - b.app_launch_index; |
46 }); | 46 }); |
47 | 47 |
| 48 // Determines if the web store link should be detached and place in the |
| 49 // top right of the screen. |
| 50 apps.detachWebstoreEntry = |
| 51 !apps.showPromo && data.apps.length >= MAX_APPS_PER_ROW[layoutMode]; |
| 52 |
| 53 apps.data = data.apps; |
| 54 if (!apps.detachWebstoreEntry) |
| 55 apps.data.push('web-store-entry'); |
| 56 |
48 clearClosedMenu(apps.menu); | 57 clearClosedMenu(apps.menu); |
49 data.apps.forEach(function(app) { | 58 data.apps.forEach(function(app) { |
50 appsSectionContent.appendChild(apps.createElement(app)); | 59 appsSectionContent.appendChild(apps.createElement(app)); |
51 }); | 60 }); |
52 | 61 |
53 webStoreEntry = apps.createWebStoreElement(); | 62 webStoreEntry = apps.createWebStoreElement(); |
54 webStoreEntry.querySelector('a').setAttribute('ping', appsPromoPing); | 63 webStoreEntry.querySelector('a').setAttribute('ping', appsPromoPing); |
55 appsSectionContent.appendChild(webStoreEntry); | 64 appsSectionContent.appendChild(webStoreEntry); |
56 | 65 |
57 data.apps.slice(0, MAX_MINIVIEW_ITEMS).forEach(function(app) { | 66 data.apps.slice(0, MAX_MINIVIEW_ITEMS).forEach(function(app) { |
(...skipping 19 matching lines...) Expand all Loading... |
77 if (apps.showPromo) | 86 if (apps.showPromo) |
78 document.documentElement.classList.add('apps-promo-visible'); | 87 document.documentElement.classList.add('apps-promo-visible'); |
79 else | 88 else |
80 document.documentElement.classList.remove('apps-promo-visible'); | 89 document.documentElement.classList.remove('apps-promo-visible'); |
81 | 90 |
82 var appsPromoLink = $('apps-promo-link'); | 91 var appsPromoLink = $('apps-promo-link'); |
83 if (appsPromoLink) | 92 if (appsPromoLink) |
84 appsPromoLink.setAttribute('ping', appsPromoPing); | 93 appsPromoLink.setAttribute('ping', appsPromoPing); |
85 maybeDoneLoading(); | 94 maybeDoneLoading(); |
86 | 95 |
| 96 // Disable the animations when the app launcher is being (re)initailized. |
| 97 apps.layout({disableAnimations:true}); |
| 98 |
| 99 if (apps.detachWebstoreEntry) |
| 100 webStoreEntry.classList.add('loner'); |
| 101 else |
| 102 webStoreEntry.classList.remove('loner'); |
| 103 |
87 if (isDoneLoading()) { | 104 if (isDoneLoading()) { |
88 if (!apps.showPromo && data.apps.length >= MAX_APPS_PER_ROW[layoutMode]) | |
89 webStoreEntry.classList.add('loner'); | |
90 else | |
91 webStoreEntry.classList.remove('loner'); | |
92 | |
93 updateMiniviewClipping(appsMiniview); | 105 updateMiniviewClipping(appsMiniview); |
94 layoutSections(); | 106 layoutSections(); |
95 } | 107 } |
96 } | 108 } |
97 | 109 |
98 function appsPrefChangeCallback(data) { | 110 function appsPrefChangeCallback(data) { |
99 // Currently the only pref that is watched is the launch type. | 111 // Currently the only pref that is watched is the launch type. |
100 data.apps.forEach(function(app) { | 112 data.apps.forEach(function(app) { |
101 var appLink = document.querySelector('.app a[app-id=' + app['id'] + ']'); | 113 var appLink = document.querySelector('.app a[app-id=' + app['id'] + ']'); |
102 if (appLink) | 114 if (appLink) |
(...skipping 151 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
254 case 'apps-options-command': | 266 case 'apps-options-command': |
255 e.canExecute = currentApp && currentApp['options_url']; | 267 e.canExecute = currentApp && currentApp['options_url']; |
256 break; | 268 break; |
257 case 'apps-launch-command': | 269 case 'apps-launch-command': |
258 case 'apps-uninstall-command': | 270 case 'apps-uninstall-command': |
259 e.canExecute = true; | 271 e.canExecute = true; |
260 break; | 272 break; |
261 } | 273 } |
262 }); | 274 }); |
263 | 275 |
| 276 // Moves the element at position |from| in array |arr| to position |to|. |
| 277 function arrayMove(arr, from, to) { |
| 278 var element = arr.splice(from, 1); |
| 279 arr.splice(to, 0, element[0]); |
| 280 } |
| 281 |
264 return { | 282 return { |
265 loaded: false, | 283 loaded: false, |
266 | 284 |
267 menu: $('apps-menu'), | 285 menu: $('apps-menu'), |
268 | 286 |
269 showPromo: false, | 287 showPromo: false, |
270 | 288 |
| 289 detachWebstoreEntry: false, |
| 290 |
| 291 // The list of app ids, in order, of each app in the launcher. |
| 292 data_: null, |
| 293 get data() { return this.data_; }, |
| 294 set data(data) { |
| 295 this.data_ = data.map(function(app) { |
| 296 return app.id; |
| 297 }); |
| 298 this.invalidate_(); |
| 299 }, |
| 300 |
| 301 dirty_: true, |
| 302 invalidate_: function() { |
| 303 this.dirty_ = true; |
| 304 }, |
| 305 |
| 306 visible_: true, |
| 307 get visible() { |
| 308 return this.visible_; |
| 309 }, |
| 310 set visible(visible) { |
| 311 this.visible_ = visible; |
| 312 this.invalidate_(); |
| 313 }, |
| 314 |
| 315 // DragAndDropDelegate |
| 316 |
| 317 dragContainer: $('apps-content'), |
| 318 transitionsDuration: 200, |
| 319 |
| 320 get dragItem() { return this.dragItem_; }, |
| 321 set dragItem(dragItem) { |
| 322 if (this.dragItem_ != dragItem) { |
| 323 this.dragItem_ = dragItem; |
| 324 this.invalidate_(); |
| 325 } |
| 326 }, |
| 327 |
| 328 // The dimensions of each item in the app launcher. This calculates the |
| 329 // dimensions dynamically, so it should be called after creating the DOM. |
| 330 dimensions_: null, |
| 331 get dimensions() { |
| 332 if (this.dimensions_) |
| 333 return this.dimensions_; |
| 334 |
| 335 var app = this.dragContainer.firstChild; |
| 336 |
| 337 var width = app.offsetWidth; |
| 338 var height = app.offsetHeight; |
| 339 |
| 340 // If the apps haven't properly loaded yet, don't cache the result. |
| 341 if (app.offsetWidth == 0 || app.offsetHeight == 0) |
| 342 return {width:0, height:0}; |
| 343 |
| 344 var style = getComputedStyle(app); |
| 345 |
| 346 var marginWidth = |
| 347 parseInt(style.marginLeft) + parseInt(style.marginRight); |
| 348 var marginHeight = |
| 349 parseInt(style.marginTop) + parseInt(style.marginBottom); |
| 350 |
| 351 var borderWidth = parseInt(style.borderLeftWidth) + |
| 352 parseInt(style.borderRightWidth); |
| 353 var borderHeight = parseInt(style.borderTopWidth) + |
| 354 parseInt(style.borderBottomWidth); |
| 355 |
| 356 this.dimensions_ = { |
| 357 width: width + marginWidth + borderWidth, |
| 358 height: height + marginHeight + borderHeight |
| 359 }; |
| 360 |
| 361 return this.dimensions_; |
| 362 }, |
| 363 |
| 364 // Gets the item under the mouse event |e|. Returns null if there is no |
| 365 // item or if the item is not draggable. |
| 366 getItem: function(e) { |
| 367 var item = findAncestorByClass(e.target, 'app'); |
| 368 |
| 369 // You can't drag the web store launcher. |
| 370 if (item.classList.contains('web-store-entry')) |
| 371 return null; |
| 372 |
| 373 return item; |
| 374 }, |
| 375 |
| 376 // Returns true if |coordinates| point to a valid drop location. The |
| 377 // coordinates are relative to the drag container and the object should |
| 378 // have the 'x' and 'y' properties set. |
| 379 canDropOn: function(coordinates) { |
| 380 var cols = MAX_APPS_PER_ROW[layoutMode]; |
| 381 var rows = Math.ceil(this.data.length / cols); |
| 382 |
| 383 var bottom = rows * this.dimensions.height; |
| 384 var right = cols * this.dimensions.width; |
| 385 |
| 386 if (coordinates.x > right || coordinates.x < 0 || |
| 387 coordinates.y > bottom || coordinates.y < 0) |
| 388 return false; |
| 389 |
| 390 var position = this.getIndexAt_(coordinates); |
| 391 var appCount = this.data.length; |
| 392 |
| 393 if (!this.detachWebstoreEntry) |
| 394 appCount--; |
| 395 |
| 396 return position >= 0 && position < appCount; |
| 397 }, |
| 398 |
| 399 setDragPlaceholder: function(coordinates) { |
| 400 var position = this.getIndexAt_(coordinates); |
| 401 var appId = this.dragItem.querySelector('a').getAttribute('app-id'); |
| 402 var current = this.data.indexOf(appId); |
| 403 |
| 404 if (current == position || current < 0) |
| 405 return; |
| 406 |
| 407 arrayMove(this.data, current, position); |
| 408 this.invalidate_(); |
| 409 this.layout(); |
| 410 }, |
| 411 |
| 412 getIndexAt_: function(coordinates) { |
| 413 var x = coordinates.x; |
| 414 var y = coordinates.y; |
| 415 |
| 416 var w = this.dimensions.width; |
| 417 var h = this.dimensions.height; |
| 418 |
| 419 var availableWidth = this.dragContainer.offsetWidth; |
| 420 |
| 421 var row = Math.floor(y / h); |
| 422 var col = Math.floor(x / w); |
| 423 var index = Math.floor(availableWidth / w) * row + col; |
| 424 |
| 425 return index; |
| 426 }, |
| 427 |
| 428 saveDrag: function() { |
| 429 this.invalidate_(); |
| 430 this.layout(); |
| 431 |
| 432 var appIds = this.data.filter(function(id) { |
| 433 return id != 'web-store-entry'; |
| 434 }); |
| 435 |
| 436 // Wait until the transitions are complete before notifying the browser. |
| 437 // Otherwise, the apps will be re-rendered while still transitioning. |
| 438 setTimeout(function() { |
| 439 chrome.send('reorderApps', appIds); |
| 440 }, this.transitionsDuration + 10); |
| 441 }, |
| 442 |
| 443 layout: function(options) { |
| 444 options = options || {}; |
| 445 if (!this.dirty_ && options.force != true) |
| 446 return; |
| 447 |
| 448 try { |
| 449 var container = this.dragContainer; |
| 450 if (options.disableAnimations) |
| 451 container.setAttribute('launcher-animations', false); |
| 452 var d0 = Date.now(); |
| 453 this.layoutImpl_(); |
| 454 this.dirty_ = false; |
| 455 logEvent('apps.layout: ' + (Date.now() - d0)); |
| 456 |
| 457 } finally { |
| 458 if (options.disableAnimations) { |
| 459 // We need to re-enable animations asynchronously, so that the |
| 460 // animations are still disabled for this layout update. |
| 461 setTimeout(function() { |
| 462 container.setAttribute('launcher-animations', true); |
| 463 }, 0); |
| 464 } |
| 465 } |
| 466 }, |
| 467 |
| 468 layoutImpl_: function() { |
| 469 var apps = this.data; |
| 470 var rects = this.getLayoutRects_(apps.length); |
| 471 var appsContent = this.dragContainer; |
| 472 |
| 473 if (!this.visible) |
| 474 return; |
| 475 |
| 476 for (var i = 0; i < apps.length; i++) { |
| 477 var app = appsContent.querySelector('[app-id='+apps[i]+']').parentNode; |
| 478 |
| 479 // If the node is being dragged, don't try to place it in the grid. |
| 480 if (app == this.dragItem) |
| 481 continue; |
| 482 |
| 483 app.style.left = rects[i].left + 'px'; |
| 484 app.style.top = rects[i].top + 'px'; |
| 485 } |
| 486 |
| 487 // We need to set the container's height manually because the apps use |
| 488 // absolute positioning. |
| 489 var rows = Math.ceil(apps.length / MAX_APPS_PER_ROW[layoutMode]); |
| 490 appsContent.style.height = (rows * this.dimensions.height) + 'px'; |
| 491 }, |
| 492 |
| 493 getLayoutRects_: function(appCount) { |
| 494 var availableWidth = this.dragContainer.offsetWidth; |
| 495 var rtl = isRtl(); |
| 496 var rects = []; |
| 497 var w = this.dimensions.width; |
| 498 var h = this.dimensions.height; |
| 499 |
| 500 for (var i = 0; i < appCount; i++) { |
| 501 var row = Math.floor((w * i) / availableWidth); |
| 502 var top = row * h; |
| 503 var left = (w * i) % availableWidth; |
| 504 |
| 505 // Reflect the X axis if an RTL language is active. |
| 506 if (rtl) |
| 507 left = availableWidth - left - w; |
| 508 rects[i] = {left: left, top: top, row: row}; |
| 509 } |
| 510 return rects; |
| 511 }, |
| 512 |
271 createElement: function(app) { | 513 createElement: function(app) { |
272 var div = createElement(app); | 514 var div = createElement(app); |
273 var a = div.firstChild; | 515 var a = div.firstChild; |
274 | 516 |
275 a.onclick = handleClick; | 517 a.onclick = handleClick; |
276 a.setAttribute('ping', PING_APP_LAUNCH_PREFIX + '+' + this.showPromo); | 518 a.setAttribute('ping', PING_APP_LAUNCH_PREFIX + '+' + this.showPromo); |
277 a.style.backgroundImage = url(app['icon_big']); | 519 a.style.backgroundImage = url(app['icon_big']); |
278 if (hashParams['app-id'] == app['id']) { | 520 if (hashParams['app-id'] == app['id']) { |
279 div.setAttribute('new', 'new'); | 521 div.setAttribute('new', 'new'); |
280 // Delay changing the attribute a bit to let the page settle down a bit. | 522 // Delay changing the attribute a bit to let the page settle down a bit. |
(...skipping 56 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
337 | 579 |
338 return a; | 580 return a; |
339 }, | 581 }, |
340 | 582 |
341 createWebStoreElement: function() { | 583 createWebStoreElement: function() { |
342 var elm = createElement({ | 584 var elm = createElement({ |
343 'id': 'web-store-entry', | 585 'id': 'web-store-entry', |
344 'name': localStrings.getString('web_store_title'), | 586 'name': localStrings.getString('web_store_title'), |
345 'launch_url': localStrings.getString('web_store_url') | 587 'launch_url': localStrings.getString('web_store_url') |
346 }); | 588 }); |
347 elm.setAttribute('app-id', 'web-store-entry'); | 589 elm.classList.add('web-store-entry'); |
348 return elm; | 590 return elm; |
349 }, | 591 }, |
350 | 592 |
351 createWebStoreMiniElement: function() { | 593 createWebStoreMiniElement: function() { |
352 var span = document.createElement('span'); | 594 var span = document.createElement('span'); |
353 span.appendChild(this.createWebStoreClosedMenuElement()); | 595 span.appendChild(this.createWebStoreClosedMenuElement()); |
354 return span; | 596 return span; |
355 }, | 597 }, |
356 | 598 |
357 createWebStoreClosedMenuElement: function() { | 599 createWebStoreClosedMenuElement: function() { |
358 var a = document.createElement('a'); | 600 var a = document.createElement('a'); |
359 a.textContent = localStrings.getString('web_store_title'); | 601 a.textContent = localStrings.getString('web_store_title'); |
360 a.href = localStrings.getString('web_store_url'); | 602 a.href = localStrings.getString('web_store_url'); |
361 a.style.backgroundImage = url('chrome://theme/IDR_PRODUCT_LOGO_16'); | 603 a.style.backgroundImage = url('chrome://theme/IDR_PRODUCT_LOGO_16'); |
362 a.className = 'item'; | 604 a.className = 'item'; |
363 return a; | 605 return a; |
364 } | 606 } |
365 }; | 607 }; |
366 })(); | 608 })(); |
| 609 |
| 610 // Enable drag and drop reordering of the app launcher. |
| 611 var appDragAndDrop = new DragAndDropController(apps); |
OLD | NEW |