1 // Copyright (c) 2012 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.
5 cr.define('ntp', function() {
9 // The histogram buckets (keep in sync with extension_constants.h).
10 NTP_APPS_MAXIMIZED: 0,
11 NTP_APPS_COLLAPSED: 1,
14 NTP_APP_RE_ENABLE: 16,
15 NTP_WEBSTORE_FOOTER: 18,
16 NTP_WEBSTORE_PLUS_ICON: 19,
19 // Histogram buckets for UMA tracking of where a DnD drop came from.
23 MOST_VISITED_PANE: 2, // Deprecated.
24 BOOKMARKS_PANE: 3, // Deprecated.
27 var DRAG_SOURCE_LIMIT = DRAG_SOURCE.OUTSIDE_NTP + 1;
30 * App context menu. The class is designed to be used as a singleton with
31 * the app that is currently showing a context menu stored in this.app_.
34 function AppContextMenu() {
35 this.__proto__ = AppContextMenu.prototype;
38 cr.addSingletonGetter(AppContextMenu);
40 AppContextMenu.prototype = {
41 initialize: function() {
42 var menu = new cr.ui.Menu;
43 cr.ui.decorate(menu, cr.ui.Menu);
44 menu.classList.add('app-context-menu');
47 this.launch_ = this.appendMenuItem_();
48 this.launch_.addEventListener('activate', this.onLaunch_.bind(this));
50 menu.appendChild(cr.ui.MenuItem.createSeparator());
51 this.launchRegularTab_ = this.appendMenuItem_('applaunchtyperegular');
52 this.launchPinnedTab_ = this.appendMenuItem_('applaunchtypepinned');
53 if (loadTimeData.getBoolean('canHostedAppsOpenInWindows'))
54 this.launchNewWindow_ = this.appendMenuItem_('applaunchtypewindow');
55 this.launchFullscreen_ = this.appendMenuItem_('applaunchtypefullscreen');
58 this.forAllLaunchTypes_(function(launchTypeButton, id) {
59 launchTypeButton.addEventListener('activate',
60 self.onLaunchTypeChanged_.bind(self));
63 this.launchTypeMenuSeparator_ = cr.ui.MenuItem.createSeparator();
64 menu.appendChild(this.launchTypeMenuSeparator_);
65 this.options_ = this.appendMenuItem_('appoptions');
66 this.uninstall_ = this.appendMenuItem_('appuninstall');
68 if (loadTimeData.getBoolean('canShowAppInfoDialog')) {
69 this.appinfo_ = this.appendMenuItem_('appinfodialog');
70 this.appinfo_.addEventListener('activate',
71 this.onShowAppInfo_.bind(this));
73 this.details_ = this.appendMenuItem_('appdetails');
74 this.details_.addEventListener('activate',
75 this.onShowDetails_.bind(this));
78 this.options_.addEventListener('activate',
79 this.onShowOptions_.bind(this));
80 this.uninstall_.addEventListener('activate',
81 this.onUninstall_.bind(this));
84 this.createShortcutSeparator_ =
85 menu.appendChild(cr.ui.MenuItem.createSeparator());
86 this.createShortcut_ = this.appendMenuItem_('appcreateshortcut');
87 this.createShortcut_.addEventListener(
88 'activate', this.onCreateShortcut_.bind(this));
91 document.body.appendChild(menu);
95 * Appends a menu item to |this.menu|.
96 * @param {string=} opt_textId If defined, the ID for the localized string
97 * that acts as the item's label.
99 appendMenuItem_: function(opt_textId) {
100 var button = cr.doc.createElement('button');
101 this.menu.appendChild(button);
102 cr.ui.decorate(button, cr.ui.MenuItem);
104 button.textContent = loadTimeData.getString(opt_textId);
109 * Iterates over all the launch type menu items.
110 * @param {function(cr.ui.MenuItem, number)} f The function to call for each
111 * menu item. The parameters to the function include the menu item and
112 * the associated launch ID.
114 forAllLaunchTypes_: function(f) {
115 // Order matters: index matches launchType id.
116 var launchTypes = [this.launchPinnedTab_,
117 this.launchRegularTab_,
118 this.launchFullscreen_,
119 this.launchNewWindow_];
121 for (var i = 0; i < launchTypes.length; ++i) {
125 f(launchTypes[i], i);
130 * Does all the necessary setup to show the menu for the given app.
131 * @param {App} app The App object that will be showing a context menu.
133 setupForApp: function(app) {
136 this.launch_.textContent = app.appData.title;
138 var launchTypeWindow = this.launchNewWindow_;
139 var hasLaunchType = false;
140 this.forAllLaunchTypes_(function(launchTypeButton, id) {
141 launchTypeButton.disabled = false;
142 launchTypeButton.checked = app.appData.launch_type == id;
143 // There are three cases when a launch type is hidden:
144 // 1. packaged apps hide all launch types
145 // 2. canHostedAppsOpenInWindows is false and type is launchTypeWindow
146 // 3. enableNewBookmarkApps is true and type is anything except
148 launchTypeButton.hidden = app.appData.packagedApp ||
149 (!loadTimeData.getBoolean('canHostedAppsOpenInWindows') &&
150 launchTypeButton == launchTypeWindow) ||
151 (loadTimeData.getBoolean('enableNewBookmarkApps') &&
152 launchTypeButton != launchTypeWindow);
153 if (!launchTypeButton.hidden) hasLaunchType = true;
156 this.launchTypeMenuSeparator_.hidden =
157 app.appData.packagedApp || !hasLaunchType;
159 this.options_.disabled = !app.appData.optionsUrl || !app.appData.enabled;
161 this.details_.disabled = !app.appData.detailsUrl;
162 this.uninstall_.disabled = !app.appData.mayDisable;
165 // On Windows and Linux, these should always be visible. On ChromeOS,
166 // they are never created. On Mac, shortcuts can only be created for
167 // new-style packaged apps, so hide the menu item.
168 this.createShortcutSeparator_.hidden = this.createShortcut_.hidden =
169 !app.appData.packagedApp;
174 * Handlers for menu item activation.
175 * @param {Event} e The activation event.
178 onLaunch_: function(e) {
179 chrome.send('launchApp', [this.app_.appId, APP_LAUNCH.NTP_APPS_MENU]);
181 onLaunchTypeChanged_: function(e) {
182 var pressed = e.currentTarget;
184 var targetLaunchType = pressed;
185 // When bookmark apps are enabled, hosted apps can only toggle between
186 // open as window and open as tab.
187 if (loadTimeData.getBoolean('enableNewBookmarkApps')) {
188 targetLaunchType = this.launchNewWindow_.checked ?
189 this.launchRegularTab_ : this.launchNewWindow_;
191 this.forAllLaunchTypes_(function(launchTypeButton, id) {
192 if (launchTypeButton == targetLaunchType) {
193 chrome.send('setLaunchType', [app.appId, id]);
194 // Manually update the launch type. We will only get
195 // appsPrefChangeCallback calls after changes to other NTP instances.
196 app.appData.launch_type = id;
200 onShowOptions_: function(e) {
201 window.location = this.app_.appData.optionsUrl;
203 onShowDetails_: function(e) {
204 var url = this.app_.appData.detailsUrl;
205 url = appendParam(url, 'utm_source', 'chrome-ntp-launcher');
206 window.location = url;
208 onUninstall_: function(e) {
209 chrome.send('uninstallApp', [this.app_.appData.id]);
211 onCreateShortcut_: function(e) {
212 chrome.send('createAppShortcut', [this.app_.appData.id]);
214 onShowAppInfo_: function(e) {
215 chrome.send('showAppInfo', [this.app_.appData.id]);
220 * Creates a new App object.
221 * @param {Object} appData The data object that describes the app.
223 * @extends {HTMLDivElement}
225 function App(appData) {
226 var el = cr.doc.createElement('div');
227 el.__proto__ = App.prototype;
228 el.initialize(appData);
234 __proto__: HTMLDivElement.prototype,
237 * Initialize the app object.
238 * @param {Object} appData The data object that describes the app.
240 initialize: function(appData) {
241 this.appData = appData;
242 assert(this.appData_.id, 'Got an app without an ID');
243 this.id = this.appData_.id;
244 this.setAttribute('role', 'menuitem');
246 this.className = 'app focusable';
248 if (!this.appData_.icon_big_exists && this.appData_.icon_small_exists)
249 this.useSmallIcon_ = true;
251 this.appContents_ = this.useSmallIcon_ ?
252 $('app-small-icon-template').cloneNode(true) :
253 $('app-large-icon-template').cloneNode(true);
254 this.appContents_.id = '';
255 this.appendChild(this.appContents_);
257 this.appImgContainer_ = /** @type {HTMLElement} */(
258 this.querySelector('.app-img-container'));
259 this.appImg_ = this.appImgContainer_.querySelector('img');
262 if (this.useSmallIcon_) {
263 this.imgDiv_ = /** @type {HTMLElement} */(
264 this.querySelector('.app-icon-div'));
265 this.addLaunchClickTarget_(this.imgDiv_);
266 this.imgDiv_.title = this.appData_.full_name;
267 chrome.send('getAppIconDominantColor', [this.id]);
269 this.addLaunchClickTarget_(this.appImgContainer_);
270 this.appImgContainer_.title = this.appData_.full_name;
273 // The app's full name is shown in the tooltip, whereas the short name
274 // is used for the label.
275 var appSpan = /** @type {HTMLElement} */(
276 this.appContents_.querySelector('.title'));
277 appSpan.textContent = this.appData_.title;
278 appSpan.title = this.appData_.full_name;
279 this.addLaunchClickTarget_(appSpan);
281 this.addEventListener('keydown', cr.ui.contextMenuHandler);
282 this.addEventListener('keyup', cr.ui.contextMenuHandler);
284 // This hack is here so that appContents.contextMenu will be the same as
287 this.appContents_.__defineGetter__('contextMenu', function() {
288 return self.contextMenu;
291 if (!this.appData_.kioskMode) {
292 this.appContents_.addEventListener('contextmenu',
293 cr.ui.contextMenuHandler);
296 this.addEventListener('mousedown', this.onMousedown_, true);
297 this.addEventListener('keydown', this.onKeydown_);
298 this.addEventListener('keyup', this.onKeyup_);
302 * Sets the color of the favicon dominant color bar.
303 * @param {string} color The css-parsable value for the color.
305 set stripeColor(color) {
306 this.querySelector('.color-stripe').style.backgroundColor = color;
310 * Removes the app tile from the page. Should be called after the app has
313 remove: function(opt_animate) {
314 // Unset the ID immediately, because the app is already gone. But leave
315 // the tile on the page as it animates out.
317 this.tile.doRemove(opt_animate);
321 * Set the URL of the icon from |appData_|. This won't actually show the
322 * icon until loadIcon() is called (for performance reasons; we don't want
323 * to load icons until we have to).
325 setIcon: function() {
326 var src = this.useSmallIcon_ ? this.appData_.icon_small :
327 this.appData_.icon_big;
328 if (!this.appData_.enabled ||
329 (!this.appData_.offlineEnabled && !navigator.onLine)) {
330 src += '?grayscale=true';
333 this.appImgSrc_ = src;
334 this.classList.add('icon-loading');
338 * Shows the icon for the app. That is, it causes chrome to load the app
341 loadIcon: function() {
342 if (this.appImgSrc_) {
343 this.appImg_.src = this.appImgSrc_;
344 this.appImg_.classList.remove('invisible');
345 this.appImgSrc_ = null;
348 this.classList.remove('icon-loading');
352 * Set the size and position of the app tile.
353 * @param {number} size The total size of |this|.
354 * @param {number} x The x-position.
355 * @param {number} y The y-position.
358 setBounds: function(size, x, y) {
359 var imgSize = size * APP_IMG_SIZE_FRACTION;
360 this.appImgContainer_.style.width = this.appImgContainer_.style.height =
361 toCssPx(this.useSmallIcon_ ? 16 : imgSize);
362 if (this.useSmallIcon_) {
363 // 3/4 is the ratio of 96px to 128px (the used height and full height
364 // of icons in apps).
365 var iconSize = imgSize * 3 / 4;
366 // The -2 is for the div border to improve the visual alignment for the
368 this.imgDiv_.style.width = this.imgDiv_.style.height =
369 toCssPx(iconSize - 2);
370 // Margins set to get the icon placement right and the text to line up.
371 this.imgDiv_.style.marginTop = this.imgDiv_.style.marginBottom =
372 toCssPx((imgSize - iconSize) / 2);
375 this.style.width = this.style.height = toCssPx(size);
376 this.style.left = toCssPx(x);
377 this.style.right = toCssPx(x);
378 this.style.top = toCssPx(y);
382 * Invoked when an app is clicked.
383 * @param {Event} e The click event.
386 onClick_: function(e) {
387 var url = !this.appData_.is_webstore ? '' :
388 appendParam(this.appData_.url,
392 chrome.send('launchApp',
393 [this.appId, APP_LAUNCH.NTP_APPS_MAXIMIZED, url,
394 e.button, e.altKey, e.ctrlKey, e.metaKey, e.shiftKey]);
396 // Don't allow the click to trigger a link or anything
401 * Invoked when the user presses a key while the app is focused.
402 * @param {Event} e The key event.
405 onKeydown_: function(e) {
406 if (e.keyIdentifier == 'Enter') {
407 chrome.send('launchApp',
408 [this.appId, APP_LAUNCH.NTP_APPS_MAXIMIZED, '',
409 0, e.altKey, e.ctrlKey, e.metaKey, e.shiftKey]);
413 this.onKeyboardUsed_(e.keyCode);
417 * Invoked when the user releases a key while the app is focused.
418 * @param {Event} e The key event.
421 onKeyup_: function(e) {
422 this.onKeyboardUsed_(e.keyCode);
426 * Called when the keyboard has been used (key down or up). The .click-focus
427 * hack is removed if the user presses a key that can change focus.
428 * @param {number} keyCode The key code of the keyboard event.
431 onKeyboardUsed_: function(keyCode) {
434 case 37: // Left arrow.
435 case 38: // Up arrow.
436 case 39: // Right arrow.
437 case 40: // Down arrow.
438 this.classList.remove('click-focus');
443 * Adds a node to the list of targets that will launch the app. This list
444 * is also used in onMousedown to determine whether the app contents should
445 * be shown as active (if we don't do this, then clicking anywhere in
446 * appContents, even a part that is outside the ideally clickable region,
447 * will cause the app icon to look active).
448 * @param {HTMLElement} node The node that should be clickable.
450 addLaunchClickTarget_: function(node) {
451 node.classList.add('launch-click-target');
452 node.addEventListener('click', this.onClick_.bind(this));
456 * Handler for mousedown on the App. Adds a class that allows us to
457 * not display as :active for right clicks (specifically, don't pulse on
458 * these occasions). Also, we don't pulse for clicks that aren't within the
460 * @param {Event} e The mousedown event.
462 onMousedown_: function(e) {
463 // If the current platform uses middle click to autoscroll and this
464 // mousedown isn't handled, onClick_() will never fire. crbug.com/142939
469 !findAncestorByClass(/** @type {Element} */(e.target),
470 'launch-click-target')) {
471 this.appContents_.classList.add('suppress-active');
473 this.appContents_.classList.remove('suppress-active');
476 // This class is here so we don't show the focus state for apps that
477 // gain keyboard focus via mouse clicking.
478 this.classList.add('click-focus');
482 * Change the appData and update the appearance of the app.
483 * @param {AppInfo} appData The new data object that describes the app.
485 replaceAppData: function(appData) {
486 this.appData_ = appData;
492 * The data and preferences for this app.
496 this.appData_ = data;
499 return this.appData_;
503 return this.appData_.id;
507 * Returns a pointer to the context menu for this app. All apps share the
508 * singleton AppContextMenu. This function is called by the
509 * ContextMenuHandler in response to the 'contextmenu' event.
513 var menu = AppContextMenu.getInstance();
514 menu.setupForApp(this);
519 * Returns whether this element can be 'removed' from chrome (i.e. whether
520 * the user can drag it onto the trash and expect something to happen).
521 * @return {boolean} True if the app can be uninstalled.
523 canBeRemoved: function() {
524 return this.appData_.mayDisable;
528 * Uninstalls the app after it's been dropped on the trash.
530 removeFromChrome: function() {
531 chrome.send('uninstallApp', [this.appData_.id, true]);
532 this.tile.tilePage.removeTile(this.tile, true);
536 * Called when a drag is starting on the tile. Updates dataTransfer with
537 * data for this tile.
539 setDragData: function(dataTransfer) {
540 dataTransfer.setData('Text', this.appData_.title);
541 dataTransfer.setData('URL', this.appData_.url);
545 var TilePage = ntp.TilePage;
547 // The fraction of the app tile size that the icon uses.
548 var APP_IMG_SIZE_FRACTION = 4 / 5;
550 var appsPageGridValues = {
551 // The fewest tiles we will show in a row.
553 // The most tiles we will show in a row.
556 // The smallest a tile can be.
557 minTileWidth: 64 / APP_IMG_SIZE_FRACTION,
558 // The biggest a tile can be.
559 maxTileWidth: 128 / APP_IMG_SIZE_FRACTION,
561 // The padding between tiles, as a fraction of the tile width.
562 tileSpacingFraction: 1 / 8,
564 TilePage.initGridValues(appsPageGridValues);
567 * Creates a new AppsPage object.
569 * @extends {TilePage}
571 function AppsPage() {
572 var el = new TilePage(appsPageGridValues);
573 el.__proto__ = AppsPage.prototype;
579 AppsPage.prototype = {
580 __proto__: TilePage.prototype,
582 initialize: function() {
583 this.classList.add('apps-page');
585 this.addEventListener('cardselected', this.onCardSelected_);
587 this.addEventListener('tilePage:tile_added', this.onTileAdded_);
589 this.content_.addEventListener('scroll', this.onScroll_.bind(this));
593 * Highlight a newly installed app as it's added to the NTP.
594 * @param {AppInfo} appData The data object that describes the app.
596 insertAndHighlightApp: function(appData) {
597 ntp.getCardSlider().selectCardByValue(this);
598 this.content_.scrollTop = this.content_.scrollHeight;
599 this.insertApp(appData, true);
603 * Similar to appendApp, but it respects the app_launch_ordinal field of
605 * @param {Object} appData The data that describes the app.
606 * @param {boolean} animate Whether to animate the insertion.
608 insertApp: function(appData, animate) {
609 var index = this.tileElements_.length;
610 for (var i = 0; i < this.tileElements_.length; i++) {
611 if (appData.app_launch_ordinal <
612 this.tileElements_[i].firstChild.appData.app_launch_ordinal) {
618 this.addTileAt(new App(appData), index, animate);
622 * Handler for 'cardselected' event, fired when |this| is selected. The
623 * first time this is called, we load all the app icons.
626 onCardSelected_: function(e) {
627 var apps = this.querySelectorAll('.app.icon-loading');
628 for (var i = 0; i < apps.length; i++) {
634 * Handler for tile additions to this page.
635 * @param {Event} e The tilePage:tile_added event.
637 onTileAdded_: function(e) {
638 assert(e.currentTarget == this);
639 assert(e.addedTile.firstChild instanceof App);
640 if (this.classList.contains('selected-card'))
641 e.addedTile.firstChild.loadIcon();
645 * A handler for when the apps page is scrolled (then we need to reposition
649 onScroll_: function(e) {
652 for (var i = 0; i < this.tileElements_.length; i++) {
653 var app = this.tileElements_[i].firstChild;
654 assert(app instanceof App);
659 doDragOver: function(e) {
660 // Only animatedly re-arrange if the user is currently dragging an app.
661 var tile = ntp.getCurrentlyDraggingTile();
662 if (tile && tile.querySelector('.app')) {
663 TilePage.prototype.doDragOver.call(this, e);
666 this.setDropEffect(e.dataTransfer);
671 shouldAcceptDrag: function(e) {
672 if (ntp.getCurrentlyDraggingTile())
674 if (!e.dataTransfer || !e.dataTransfer.types)
676 return Array.prototype.indexOf.call(e.dataTransfer.types,
677 'text/uri-list') != -1;
681 addDragData: function(dataTransfer, index) {
683 var currentlyDraggingTile = ntp.getCurrentlyDraggingTile();
684 if (currentlyDraggingTile) {
685 var tileContents = currentlyDraggingTile.firstChild;
686 if (tileContents.classList.contains('app')) {
687 var originalPage = currentlyDraggingTile.tilePage;
688 var samePageDrag = originalPage == this;
689 sourceId = samePageDrag ? DRAG_SOURCE.SAME_APPS_PANE :
690 DRAG_SOURCE.OTHER_APPS_PANE;
691 this.tileGrid_.insertBefore(currentlyDraggingTile,
692 this.tileElements_[index]);
693 this.tileMoved(currentlyDraggingTile);
695 originalPage.fireRemovedEvent(currentlyDraggingTile, index, true);
696 this.fireAddedEvent(currentlyDraggingTile, index, true);
700 this.addOutsideData_(dataTransfer);
701 sourceId = DRAG_SOURCE.OUTSIDE_NTP;
704 assert(sourceId != -1);
705 chrome.send('metricsHandler:recordInHistogram',
706 ['NewTabPage.AppsPageDragSource', sourceId, DRAG_SOURCE_LIMIT]);
710 * Adds drag data that has been dropped from a source that is not a tile.
711 * @param {Object} dataTransfer The data transfer object that holds drop
715 addOutsideData_: function(dataTransfer) {
716 var url = dataTransfer.getData('url');
719 // If the dataTransfer has html data, use that html's text contents as the
720 // title of the new link.
721 var html = dataTransfer.getData('text/html');
724 // It's important that we don't attach this node to the document
725 // because it might contain scripts.
726 var node = this.ownerDocument.createElement('div');
727 node.innerHTML = html;
728 title = node.textContent;
731 // Make sure title is >=1 and <=45 characters for Chrome app limits.
734 if (title.length > 45)
735 title = title.substring(0, 45);
736 var data = {url: url, title: title};
738 // Synthesize an app.
739 this.generateAppForLink(data);
743 * Creates a new crx-less app manifest and installs it.
744 * @param {Object} data The data object describing the link. Must have |url|
745 * and |title| members.
747 generateAppForLink: function(data) {
748 assert(data.url != undefined);
749 assert(data.title != undefined);
750 var pageIndex = ntp.getAppsPageIndex(this);
751 chrome.send('generateAppForLink', [data.url, data.title, pageIndex]);
755 tileMoved: function(draggedTile) {
756 if (!(draggedTile.firstChild instanceof App))
759 var pageIndex = ntp.getAppsPageIndex(this);
760 chrome.send('setPageIndex', [draggedTile.firstChild.appId, pageIndex]);
763 for (var i = 0; i < this.tileElements_.length; i++) {
764 var tileContents = this.tileElements_[i].firstChild;
765 if (tileContents instanceof App)
766 appIds.push(tileContents.appId);
769 chrome.send('reorderApps', [draggedTile.firstChild.appId, appIds]);
773 setDropEffect: function(dataTransfer) {
774 var tile = ntp.getCurrentlyDraggingTile();
775 if (tile && tile.querySelector('.app'))
776 ntp.setCurrentDropEffect(dataTransfer, 'move');
778 ntp.setCurrentDropEffect(dataTransfer, 'copy');
783 * Launches the specified app using the APP_LAUNCH_NTP_APP_RE_ENABLE
784 * histogram. This should only be invoked from the AppLauncherHandler.
785 * @param {string} appId The ID of the app.
787 function launchAppAfterEnable(appId) {
788 chrome.send('launchApp', [appId, APP_LAUNCH.NTP_APP_RE_ENABLE]);
792 APP_LAUNCH: APP_LAUNCH,
794 launchAppAfterEnable: launchAppAfterEnable,