• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
5var MAX_APPS_PER_ROW = [];
6MAX_APPS_PER_ROW[LayoutMode.SMALL] = 4;
7MAX_APPS_PER_ROW[LayoutMode.NORMAL] = 6;
8
9function 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
126function 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
141function 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.
152function launchAppAfterEnable(appId) {
153  chrome.send('launchApp', [appId, APP_LAUNCH.NTP_APP_RE_ENABLE]);
154}
155
156var 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.
780var appDragAndDrop = new DragAndDropController(apps);
781