1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 var MAX_APPS_PER_ROW = []; 6 MAX_APPS_PER_ROW[LayoutMode.SMALL] = 4; 7 MAX_APPS_PER_ROW[LayoutMode.NORMAL] = 6; 8 9 function getAppsCallback(data) { 10 logEvent('received apps'); 11 12 // In the case of prefchange-triggered updates, we don't receive this flag. 13 // Just leave it set as it was before in that case. 14 if ('showPromo' in data) 15 apps.showPromo = data.showPromo; 16 17 var appsSection = $('apps'); 18 var appsSectionContent = $('apps-content'); 19 var appsMiniview = appsSection.getElementsByClassName('miniview')[0]; 20 var appsPromo = $('apps-promo'); 21 var appsPromoLink = $('apps-promo-link'); 22 var appsPromoPing = APP_LAUNCH_URL.PING_WEBSTORE + '+' + apps.showPromo; 23 var webStoreEntry, webStoreMiniEntry; 24 25 // Hide menu options that are not supported on the OS or windowing system. 26 27 // The "Launch as Window" menu option. 28 $('apps-launch-type-window-menu-item').hidden = data.disableAppWindowLaunch; 29 30 // The "Create App Shortcut" menu option. 31 $('apps-create-shortcut-command-menu-item').hidden = 32 $('apps-create-shortcut-command-separator').hidden = 33 data.disableCreateAppShortcut; 34 35 // Hide the context menu, if there is any open. 36 cr.ui.contextMenuHandler.hideMenu(); 37 38 appsMiniview.textContent = ''; 39 appsSectionContent.textContent = ''; 40 41 data.apps.sort(function(a,b) { 42 return a.app_launch_index - b.app_launch_index; 43 }); 44 45 // Determines if the web store link should be detached and place in the 46 // top right of the screen. 47 apps.detachWebstoreEntry = 48 !apps.showPromo && data.apps.length >= MAX_APPS_PER_ROW[layoutMode]; 49 50 markNewApps(data.apps); 51 apps.data = data.apps; 52 53 clearClosedMenu(apps.menu); 54 55 // We wait for the app icons to load before displaying them, but never wait 56 // longer than 200ms. 57 apps.loadedImages = 0; 58 apps.imageTimer = setTimeout(apps.showImages.bind(apps), 200); 59 60 data.apps.forEach(function(app) { 61 appsSectionContent.appendChild(apps.createElement(app)); 62 }); 63 64 if (data.showPromo) { 65 // Add the promo content... 66 $('apps-promo-heading').textContent = data.promoHeader; 67 appsPromoLink.href = data.promoLink; 68 appsPromoLink.textContent = data.promoButton; 69 appsPromoLink.ping = appsPromoPing; 70 $('apps-promo-hide').textContent = data.promoExpire; 71 72 // ... then display the promo. 73 document.documentElement.classList.add('apps-promo-visible'); 74 } else { 75 document.documentElement.classList.remove('apps-promo-visible'); 76 } 77 78 // Only show the web store entry if there are apps installed, since the promo 79 // is sufficient otherwise. 80 if (data.apps.length > 0) { 81 webStoreEntry = apps.createWebStoreElement(); 82 webStoreEntry.querySelector('a').ping = appsPromoPing; 83 appsSectionContent.appendChild(webStoreEntry); 84 if (apps.detachWebstoreEntry) { 85 webStoreEntry.classList.add('loner'); 86 } else { 87 webStoreEntry.classList.remove('loner'); 88 apps.data.push('web-store-entry'); 89 } 90 } 91 92 data.apps.slice(0, MAX_MINIVIEW_ITEMS).forEach(function(app) { 93 appsMiniview.appendChild(apps.createMiniviewElement(app)); 94 addClosedMenuEntryWithLink(apps.menu, apps.createClosedMenuElement(app)); 95 }); 96 if (data.apps.length < MAX_MINIVIEW_ITEMS) { 97 webStoreMiniEntry = apps.createWebStoreMiniElement(); 98 webStoreMiniEntry.querySelector('a').ping = appsPromoPing; 99 appsMiniview.appendChild(webStoreMiniEntry); 100 addClosedMenuEntryWithLink(apps.menu, 101 apps.createWebStoreClosedMenuElement()); 102 } 103 104 if (!data.showLauncher) 105 hideSection(Section.APPS); 106 else 107 appsSection.classList.remove('disabled'); 108 109 addClosedMenuFooter(apps.menu, 'apps', MENU_APPS, Section.APPS); 110 111 apps.loaded = true; 112 113 if (appsPromoLink) 114 appsPromoLink.ping = appsPromoPing; 115 maybeDoneLoading(); 116 117 // Disable the animations when the app launcher is being (re)initailized. 118 apps.layout({disableAnimations:true}); 119 120 if (isDoneLoading()) { 121 updateMiniviewClipping(appsMiniview); 122 layoutSections(); 123 } 124 } 125 126 function markNewApps(data) { 127 var oldData = apps.data; 128 data.forEach(function(app) { 129 if (hashParams['app-id'] == app['id']) { 130 delete hashParams['app-id']; 131 app.isNew = true; 132 } else if (oldData && 133 !oldData.some(function(id) { return id == app.id; })) { 134 app.isNew = true; 135 } else { 136 app.isNew = false; 137 } 138 }); 139 } 140 141 function appsPrefChangeCallback(data) { 142 // Currently the only pref that is watched is the launch type. 143 data.apps.forEach(function(app) { 144 var appLink = document.querySelector('.app a[app-id=' + app['id'] + ']'); 145 if (appLink) 146 appLink.setAttribute('launch-type', app['launch_type']); 147 }); 148 } 149 150 // Launches the specified app using the APP_LAUNCH_NTP_APP_RE_ENABLE histogram. 151 // This should only be invoked from the AppLauncherHandler. 152 function launchAppAfterEnable(appId) { 153 chrome.send('launchApp', [appId, APP_LAUNCH.NTP_APP_RE_ENABLE]); 154 } 155 156 var apps = (function() { 157 158 function createElement(app) { 159 var div = document.createElement('div'); 160 div.className = 'app'; 161 162 var a = div.appendChild(document.createElement('a')); 163 a.setAttribute('app-id', app['id']); 164 a.setAttribute('launch-type', app['launch_type']); 165 a.draggable = false; 166 a.xtitle = a.textContent = app['name']; 167 a.href = app['launch_url']; 168 169 return div; 170 } 171 172 /** 173 * Launches an application. 174 * @param {string} appId Application to launch. 175 * @param {MouseEvent} opt_mouseEvent Mouse event from the click that 176 * triggered the launch, used to detect modifier keys that change 177 * the tab's disposition. 178 */ 179 function launchApp(appId, opt_mouseEvent) { 180 var args = [appId, getAppLaunchType()]; 181 if (opt_mouseEvent) { 182 // Launch came from a click - add details of the click 183 // Otherwise it came from a 'command' event from elsewhere in the UI. 184 args.push(opt_mouseEvent.altKey, opt_mouseEvent.ctrlKey, 185 opt_mouseEvent.metaKey, opt_mouseEvent.shiftKey, 186 opt_mouseEvent.button); 187 } 188 chrome.send('launchApp', args); 189 } 190 191 function isAppSectionMaximized() { 192 return getAppLaunchType() == APP_LAUNCH.NTP_APPS_MAXIMIZED && 193 !$('apps').classList.contains('disabled'); 194 } 195 196 function isAppsMenu(node) { 197 return node.id == 'apps-menu'; 198 } 199 200 function getAppLaunchType() { 201 // We determine if the apps section is maximized, collapsed or in menu mode 202 // based on the class of the apps section. 203 if ($('apps').classList.contains('menu')) 204 return APP_LAUNCH.NTP_APPS_MENU; 205 else if ($('apps').classList.contains('collapsed')) 206 return APP_LAUNCH.NTP_APPS_COLLAPSED; 207 else 208 return APP_LAUNCH.NTP_APPS_MAXIMIZED; 209 } 210 211 /** 212 * @this {!HTMLAnchorElement} 213 */ 214 function handleClick(e) { 215 var appId = e.currentTarget.getAttribute('app-id'); 216 if (!appDragAndDrop.isDragging()) 217 launchApp(appId, e); 218 return false; 219 } 220 221 // Keep in sync with LaunchType in extension_prefs.h 222 var LaunchType = { 223 LAUNCH_PINNED: 0, 224 LAUNCH_REGULAR: 1, 225 LAUNCH_FULLSCREEN: 2, 226 LAUNCH_WINDOW: 3 227 }; 228 229 // Keep in sync with LaunchContainer in extension_constants.h 230 var LaunchContainer = { 231 LAUNCH_WINDOW: 0, 232 LAUNCH_PANEL: 1, 233 LAUNCH_TAB: 2 234 }; 235 236 var currentApp; 237 var promoHasBeenSeen = false; 238 239 function addContextMenu(el, app) { 240 el.addEventListener('contextmenu', cr.ui.contextMenuHandler); 241 el.addEventListener('keydown', cr.ui.contextMenuHandler); 242 el.addEventListener('keyup', cr.ui.contextMenuHandler); 243 244 Object.defineProperty(el, 'contextMenu', { 245 get: function() { 246 currentApp = app; 247 248 $('apps-launch-command').label = app['name']; 249 $('apps-options-command').canExecuteChange(); 250 251 var launchTypeEl; 252 if (el.getAttribute('app-id') === app['id']) { 253 launchTypeEl = el; 254 } else { 255 appLinkSel = 'a[app-id=' + app['id'] + ']'; 256 launchTypeEl = el.querySelector(appLinkSel); 257 } 258 259 var launchType = launchTypeEl.getAttribute('launch-type'); 260 var launchContainer = app['launch_container']; 261 var isPanel = launchContainer == LaunchContainer.LAUNCH_PANEL; 262 263 // Update the commands related to the launch type. 264 var launchTypeIds = ['apps-launch-type-pinned', 265 'apps-launch-type-regular', 266 'apps-launch-type-fullscreen', 267 'apps-launch-type-window']; 268 launchTypeIds.forEach(function(id) { 269 var command = $(id); 270 command.disabled = isPanel; 271 command.checked = !isPanel && 272 launchType == command.getAttribute('launch-type'); 273 }); 274 275 return $('app-context-menu'); 276 } 277 }); 278 } 279 280 document.addEventListener('command', function(e) { 281 if (!currentApp) 282 return; 283 284 var commandId = e.command.id; 285 switch (commandId) { 286 case 'apps-options-command': 287 window.location = currentApp['options_url']; 288 break; 289 case 'apps-launch-command': 290 launchApp(currentApp['id']); 291 break; 292 case 'apps-uninstall-command': 293 chrome.send('uninstallApp', [currentApp['id']]); 294 break; 295 case 'apps-create-shortcut-command': 296 chrome.send('createAppShortcut', [currentApp['id']]); 297 break; 298 case 'apps-launch-type-pinned': 299 case 'apps-launch-type-regular': 300 case 'apps-launch-type-fullscreen': 301 case 'apps-launch-type-window': 302 chrome.send('setLaunchType', 303 [currentApp['id'], 304 Number(e.command.getAttribute('launch-type'))]); 305 break; 306 } 307 }); 308 309 document.addEventListener('canExecute', function(e) { 310 switch (e.command.id) { 311 case 'apps-options-command': 312 e.canExecute = currentApp && currentApp['options_url']; 313 break; 314 case 'apps-launch-command': 315 e.canExecute = true; 316 break; 317 case 'apps-uninstall-command': 318 e.canExecute = !currentApp['can_uninstall']; 319 break; 320 } 321 }); 322 323 // Moves the element at position |from| in array |arr| to position |to|. 324 function arrayMove(arr, from, to) { 325 var element = arr.splice(from, 1); 326 arr.splice(to, 0, element[0]); 327 } 328 329 // The autoscroll rate during drag and drop, in px per second. 330 var APP_AUTOSCROLL_RATE = 400; 331 332 return { 333 loaded: false, 334 335 menu: $('apps-menu'), 336 337 showPromo: false, 338 339 detachWebstoreEntry: false, 340 341 scrollMouseXY_: null, 342 343 scrollListener_: null, 344 345 // The list of app ids, in order, of each app in the launcher. 346 data_: null, 347 get data() { return this.data_; }, 348 set data(data) { 349 this.data_ = data.map(function(app) { 350 return app.id; 351 }); 352 this.invalidate_(); 353 }, 354 355 dirty_: true, 356 invalidate_: function() { 357 this.dirty_ = true; 358 }, 359 360 visible_: true, 361 get visible() { 362 return this.visible_; 363 }, 364 set visible(visible) { 365 this.visible_ = visible; 366 this.invalidate_(); 367 }, 368 369 maybePingPromoSeen_: function() { 370 if (promoHasBeenSeen || !this.showPromo || !isAppSectionMaximized()) 371 return; 372 373 promoHasBeenSeen = true; 374 chrome.send('promoSeen', []); 375 }, 376 377 // DragAndDropDelegate 378 379 dragContainer: $('apps-content'), 380 transitionsDuration: 200, 381 382 get dragItem() { return this.dragItem_; }, 383 set dragItem(dragItem) { 384 if (this.dragItem_ != dragItem) { 385 this.dragItem_ = dragItem; 386 this.invalidate_(); 387 } 388 }, 389 390 // The dimensions of each item in the app launcher. 391 dimensions_: null, 392 get dimensions() { 393 if (this.dimensions_) 394 return this.dimensions_; 395 396 var width = 124; 397 var height = 136; 398 399 var marginWidth = 6; 400 var marginHeight = 10; 401 402 var borderWidth = 0; 403 var borderHeight = 0; 404 405 this.dimensions_ = { 406 width: width + marginWidth + borderWidth, 407 height: height + marginHeight + borderHeight 408 }; 409 410 return this.dimensions_; 411 }, 412 413 // Gets the item under the mouse event |e|. Returns null if there is no 414 // item or if the item is not draggable. 415 getItem: function(e) { 416 var item = findAncestorByClass(e.target, 'app'); 417 418 // You can't drag the web store launcher. 419 if (item && item.classList.contains('web-store-entry')) 420 return null; 421 422 return item; 423 }, 424 425 // Returns true if |coordinates| point to a valid drop location. The 426 // coordinates are relative to the drag container and the object should 427 // have the 'x' and 'y' properties set. 428 canDropOn: function(coordinates) { 429 var cols = MAX_APPS_PER_ROW[layoutMode]; 430 var rows = Math.ceil(this.data.length / cols); 431 432 var bottom = rows * this.dimensions.height; 433 var right = cols * this.dimensions.width; 434 435 if (coordinates.x >= right || coordinates.x < 0 || 436 coordinates.y >= bottom || coordinates.y < 0) 437 return false; 438 439 var position = this.getIndexAt_(coordinates); 440 var appCount = this.data.length; 441 442 if (!this.detachWebstoreEntry) 443 appCount--; 444 445 return position >= 0 && position < appCount; 446 }, 447 448 setDragPlaceholder: function(coordinates) { 449 var position = this.getIndexAt_(coordinates); 450 var appId = this.dragItem.querySelector('a').getAttribute('app-id'); 451 var current = this.data.indexOf(appId); 452 453 if (current == position || current < 0) 454 return; 455 456 arrayMove(this.data, current, position); 457 this.invalidate_(); 458 this.layout(); 459 }, 460 461 getIndexAt_: function(coordinates) { 462 var w = this.dimensions.width; 463 var h = this.dimensions.height; 464 465 var appsPerRow = MAX_APPS_PER_ROW[layoutMode]; 466 467 var row = Math.floor(coordinates.y / h); 468 var col = Math.floor(coordinates.x / w); 469 var index = appsPerRow * row + col; 470 471 var appCount = this.data.length; 472 var rows = Math.ceil(appCount / appsPerRow); 473 474 // Rather than making the free space on the last row invalid, we 475 // map it to the last valid position. 476 if (index >= appCount && index < appsPerRow * rows) 477 return appCount-1; 478 479 return index; 480 }, 481 482 scrollPage: function(xy) { 483 var rect = this.dragContainer.getBoundingClientRect(); 484 485 // Here, we calculate the visible boundaries of the app launcher, which 486 // are then used to determine when we should auto-scroll. 487 var top = $('apps').getBoundingClientRect().bottom; 488 var bottomFudge = 15; // Fudge factor due to a gradient mask. 489 var bottom = top + maxiviewVisibleHeight - bottomFudge; 490 var left = rect.left + window.scrollX; 491 var right = Math.min(window.innerWidth, rect.left + rect.width); 492 493 var dy = Math.min(0, xy.y - top) + Math.max(0, xy.y - bottom); 494 var dx = Math.min(0, xy.x - left) + Math.max(0, xy.x - right); 495 496 if (dx == 0 && dy == 0) { 497 this.stopScroll_(); 498 return; 499 } 500 501 // If we scroll the page directly from this method, it may be choppy and 502 // inconsistent. Instead, we loop using animation frames, and scroll at a 503 // speed that's independent of how many times this method is called. 504 this.scrollMouseXY_ = {dx: dx, dy: dy}; 505 506 if (!this.scrollListener_) { 507 this.scrollListener_ = this.scrollImpl_.bind(this); 508 this.scrollStep_(); 509 } 510 }, 511 512 scrollStep_: function() { 513 this.scrollStart_ = Date.now(); 514 window.webkitRequestAnimationFrame(this.scrollListener_); 515 }, 516 517 scrollImpl_: function(time) { 518 if (!appDragAndDrop.isDragging()) { 519 this.stopScroll_(); 520 return; 521 } 522 523 if (!this.scrollMouseXY_) 524 return; 525 526 var step = time - this.scrollStart_; 527 528 window.scrollBy( 529 this.calcScroll_(this.scrollMouseXY_.dx, step), 530 this.calcScroll_(this.scrollMouseXY_.dy, step)); 531 532 this.scrollStep_(); 533 }, 534 535 calcScroll_: function(delta, step) { 536 if (delta == 0) 537 return 0; 538 539 // Increase the multiplier for every 50px the mouse is beyond the edge. 540 var sign = delta > 0 ? 1 : -1; 541 var scalar = APP_AUTOSCROLL_RATE * step / 1000; 542 var multiplier = Math.floor(Math.abs(delta) / 50) + 1; 543 544 return sign * scalar * multiplier; 545 }, 546 547 stopScroll_: function() { 548 this.scrollListener_ = null; 549 this.scrollMouseXY_ = null; 550 }, 551 552 saveDrag: function(draggedItem) { 553 this.invalidate_(); 554 this.layout(); 555 556 var draggedAppId = draggedItem.querySelector('a').getAttribute('app-id'); 557 var appIds = this.data.filter(function(id) { 558 return id != 'web-store-entry'; 559 }); 560 561 // Wait until the transitions are complete before notifying the browser. 562 // Otherwise, the apps will be re-rendered while still transitioning. 563 setTimeout(function() { 564 chrome.send('reorderApps', [draggedAppId, appIds]); 565 }, this.transitionsDuration + 10); 566 }, 567 568 layout: function(options) { 569 options = options || {}; 570 if (!this.dirty_ && options.force != true) 571 return; 572 573 try { 574 var container = this.dragContainer; 575 if (options.disableAnimations) 576 container.setAttribute('launcher-animations', false); 577 var d0 = Date.now(); 578 this.layoutImpl_(); 579 this.dirty_ = false; 580 logEvent('apps.layout: ' + (Date.now() - d0)); 581 582 } finally { 583 if (options.disableAnimations) { 584 // We need to re-enable animations asynchronously, so that the 585 // animations are still disabled for this layout update. 586 setTimeout(function() { 587 container.setAttribute('launcher-animations', true); 588 }, 0); 589 } 590 } 591 }, 592 593 layoutImpl_: function() { 594 var apps = this.data || []; 595 var rects = this.getLayoutRects_(apps.length); 596 var appsContent = this.dragContainer; 597 598 // Ping the PROMO_SEEN histogram only when the promo is maximized, and 599 // maximum once per NTP load. 600 this.maybePingPromoSeen_(); 601 602 if (!this.visible) 603 return; 604 605 for (var i = 0; i < apps.length; i++) { 606 var app = appsContent.querySelector('[app-id='+apps[i]+']').parentNode; 607 608 // If the node is being dragged, don't try to place it in the grid. 609 if (app == this.dragItem) 610 continue; 611 612 app.style.left = rects[i].left + 'px'; 613 app.style.top = rects[i].top + 'px'; 614 } 615 616 // We need to set the container's height manually because the apps use 617 // absolute positioning. 618 var rows = Math.ceil(apps.length / MAX_APPS_PER_ROW[layoutMode]); 619 appsContent.style.height = (rows * this.dimensions.height) + 'px'; 620 }, 621 622 getLayoutRects_: function(appCount) { 623 var availableWidth = this.dragContainer.offsetWidth; 624 var rtl = isRtl(); 625 var rects = []; 626 var w = this.dimensions.width; 627 var h = this.dimensions.height; 628 var appsPerRow = MAX_APPS_PER_ROW[layoutMode]; 629 630 for (var i = 0; i < appCount; i++) { 631 var top = Math.floor(i / appsPerRow) * h; 632 var left = (i % appsPerRow) * w; 633 634 // Reflect the X axis if an RTL language is active. 635 if (rtl) 636 left = availableWidth - left - w; 637 rects[i] = {left: left, top: top}; 638 } 639 return rects; 640 }, 641 642 get loadedImages() { 643 return this.loadedImages_; 644 }, 645 646 set loadedImages(value) { 647 this.loadedImages_ = value; 648 if (this.loadedImages_ == 0) 649 return; 650 651 // Each application icon is loaded asynchronously. Here, we display 652 // the icons once they've all been loaded to make it look nicer. 653 if (this.loadedImages_ == this.data.length) { 654 this.showImages(); 655 return; 656 } 657 658 // We won't actually have the visible height until the sections have 659 // been layed out. 660 if (!maxiviewVisibleHeight) 661 return; 662 663 // If we know the visible height of the maxiview, then we can don't need 664 // to wait for all the icons. Instead, we wait until the visible portion 665 // have been loaded. 666 var appsPerRow = MAX_APPS_PER_ROW[layoutMode]; 667 var rows = Math.ceil(maxiviewVisibleHeight / this.dimensions.height); 668 var count = Math.min(appsPerRow * rows, this.data.length); 669 if (this.loadedImages_ == count) { 670 this.showImages(); 671 return; 672 } 673 }, 674 675 showImages: function() { 676 $('apps-content').classList.add('visible'); 677 clearTimeout(this.imageTimer); 678 }, 679 680 createElement: function(app) { 681 var div = createElement(app); 682 var a = div.firstChild; 683 684 a.onclick = handleClick; 685 a.ping = getAppPingUrl( 686 'PING_BY_ID', this.showPromo, 'NTP_APPS_MAXIMIZED'); 687 a.style.backgroundImage = url(app['icon_big']); 688 if (app.isNew) { 689 div.setAttribute('new', 'new'); 690 // Delay changing the attribute a bit to let the page settle down a bit. 691 setTimeout(function() { 692 // Make sure the new icon is scrolled into view. 693 document.body.scrollTop = document.body.scrollHeight; 694 695 // This will trigger the 'bounce' animation defined in apps.css. 696 div.setAttribute('new', 'installed'); 697 }, 500); 698 div.addEventListener('webkitAnimationEnd', function(e) { 699 div.removeAttribute('new'); 700 }); 701 } 702 703 // CSS background images don't fire 'load' events, so we use an Image. 704 var img = new Image(); 705 img.onload = function() { this.loadedImages++; }.bind(this); 706 img.src = app['icon_big']; 707 708 var settingsButton = div.appendChild(new cr.ui.ContextMenuButton); 709 settingsButton.className = 'app-settings'; 710 settingsButton.title = localStrings.getString('appsettings'); 711 712 addContextMenu(div, app); 713 714 return div; 715 }, 716 717 createMiniviewElement: function(app) { 718 var span = document.createElement('span'); 719 var a = span.appendChild(document.createElement('a')); 720 721 a.setAttribute('app-id', app['id']); 722 a.textContent = app['name']; 723 a.href = app['launch_url']; 724 a.onclick = handleClick; 725 a.ping = getAppPingUrl( 726 'PING_BY_ID', this.showPromo, 'NTP_APPS_COLLAPSED'); 727 a.style.backgroundImage = url(app['icon_small']); 728 a.className = 'item'; 729 span.appendChild(a); 730 731 addContextMenu(span, app); 732 733 return span; 734 }, 735 736 createClosedMenuElement: function(app) { 737 var a = document.createElement('a'); 738 a.setAttribute('app-id', app['id']); 739 a.textContent = app['name']; 740 a.href = app['launch_url']; 741 a.onclick = handleClick; 742 a.ping = getAppPingUrl( 743 'PING_BY_ID', this.showPromo, 'NTP_APPS_MENU'); 744 a.style.backgroundImage = url(app['icon_small']); 745 a.className = 'item'; 746 747 addContextMenu(a, app); 748 749 return a; 750 }, 751 752 createWebStoreElement: function() { 753 var elm = createElement({ 754 'id': 'web-store-entry', 755 'name': localStrings.getString('web_store_title'), 756 'launch_url': localStrings.getString('web_store_url') 757 }); 758 elm.classList.add('web-store-entry'); 759 return elm; 760 }, 761 762 createWebStoreMiniElement: function() { 763 var span = document.createElement('span'); 764 span.appendChild(this.createWebStoreClosedMenuElement()); 765 return span; 766 }, 767 768 createWebStoreClosedMenuElement: function() { 769 var a = document.createElement('a'); 770 a.textContent = localStrings.getString('web_store_title'); 771 a.href = localStrings.getString('web_store_url'); 772 a.style.backgroundImage = url('chrome://theme/IDR_PRODUCT_LOGO_16'); 773 a.className = 'item'; 774 return a; 775 } 776 }; 777 })(); 778 779 // Enable drag and drop reordering of the app launcher. 780 var appDragAndDrop = new DragAndDropController(apps); 781