Home | History | Annotate | Download | only in ntp
      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