| OLD | NEW |
| (Empty) |
| 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').style.background = | |
| 71 "url('" + data.promoLogo + "') no-repeat"; | |
| 72 $('apps-promo-hide').textContent = data.promoExpire; | |
| 73 | |
| 74 // ... then display the promo. | |
| 75 document.documentElement.classList.add('apps-promo-visible'); | |
| 76 } else { | |
| 77 document.documentElement.classList.remove('apps-promo-visible'); | |
| 78 } | |
| 79 | |
| 80 // Only show the web store entry if there are apps installed or the promo | |
| 81 // is not available. | |
| 82 if (data.apps.length > 0 || !data.showPromo) { | |
| 83 webStoreEntry = apps.createWebStoreElement(); | |
| 84 webStoreEntry.querySelector('a').ping = appsPromoPing; | |
| 85 appsSectionContent.appendChild(webStoreEntry); | |
| 86 if (apps.detachWebstoreEntry) { | |
| 87 webStoreEntry.classList.add('loner'); | |
| 88 } else { | |
| 89 webStoreEntry.classList.remove('loner'); | |
| 90 apps.data.push('web-store-entry'); | |
| 91 } | |
| 92 } | |
| 93 | |
| 94 data.apps.slice(0, MAX_MINIVIEW_ITEMS).forEach(function(app) { | |
| 95 appsMiniview.appendChild(apps.createMiniviewElement(app)); | |
| 96 addClosedMenuEntryWithLink(apps.menu, apps.createClosedMenuElement(app)); | |
| 97 }); | |
| 98 if (data.apps.length < MAX_MINIVIEW_ITEMS) { | |
| 99 webStoreMiniEntry = apps.createWebStoreMiniElement(); | |
| 100 webStoreMiniEntry.querySelector('a').ping = appsPromoPing; | |
| 101 appsMiniview.appendChild(webStoreMiniEntry); | |
| 102 addClosedMenuEntryWithLink(apps.menu, | |
| 103 apps.createWebStoreClosedMenuElement()); | |
| 104 } | |
| 105 | |
| 106 if (!data.showLauncher) | |
| 107 hideSection(Section.APPS); | |
| 108 else | |
| 109 appsSection.classList.remove('disabled'); | |
| 110 | |
| 111 addClosedMenuFooter(apps.menu, 'apps', MENU_APPS, Section.APPS); | |
| 112 | |
| 113 apps.loaded = true; | |
| 114 | |
| 115 if (appsPromoLink) | |
| 116 appsPromoLink.ping = appsPromoPing; | |
| 117 maybeDoneLoading(); | |
| 118 | |
| 119 // Disable the animations when the app launcher is being (re)initailized. | |
| 120 apps.layout({disableAnimations:true}); | |
| 121 | |
| 122 if (isDoneLoading()) { | |
| 123 updateMiniviewClipping(appsMiniview); | |
| 124 layoutSections(); | |
| 125 } | |
| 126 } | |
| 127 | |
| 128 function markNewApps(data) { | |
| 129 var oldData = apps.data; | |
| 130 data.forEach(function(app) { | |
| 131 if (hashParams['app-id'] == app['id']) { | |
| 132 delete hashParams['app-id']; | |
| 133 app.isNew = true; | |
| 134 } else if (oldData && | |
| 135 !oldData.some(function(id) { return id == app.id; })) { | |
| 136 app.isNew = true; | |
| 137 } else { | |
| 138 app.isNew = false; | |
| 139 } | |
| 140 }); | |
| 141 } | |
| 142 | |
| 143 function appsPrefChangeCallback(data) { | |
| 144 // Currently the only pref that is watched is the launch type. | |
| 145 data.apps.forEach(function(app) { | |
| 146 var appLink = document.querySelector('.app a[app-id=' + app['id'] + ']'); | |
| 147 if (appLink) | |
| 148 appLink.setAttribute('launch-type', app['launch_type']); | |
| 149 }); | |
| 150 } | |
| 151 | |
| 152 function appNotificationChanged(id, lastNotification) { | |
| 153 // TODO(asargent/finnur): Don't update all apps at once, do it in a more | |
| 154 // fine grained way. | |
| 155 chrome.send('getApps'); | |
| 156 } | |
| 157 | |
| 158 // Launches the specified app using the APP_LAUNCH_NTP_APP_RE_ENABLE histogram. | |
| 159 // This should only be invoked from the AppLauncherHandler. | |
| 160 function launchAppAfterEnable(appId) { | |
| 161 chrome.send('launchApp', [appId, APP_LAUNCH.NTP_APP_RE_ENABLE]); | |
| 162 } | |
| 163 | |
| 164 // Shows the notification bubble for a given app (the one clicked on). | |
| 165 function showNotificationBubble(event) { | |
| 166 var item = findAncestorByClass(event.target, 'app-anchor'); | |
| 167 var title = item.getAttribute('notification-title'); | |
| 168 var message = item.getAttribute('notification-message'); | |
| 169 var link = item.getAttribute('notification-link'); | |
| 170 var link_message = item.getAttribute('notification-link-message'); | |
| 171 | |
| 172 if (!title || !message) | |
| 173 return; | |
| 174 | |
| 175 // Set the content to the right text. | |
| 176 $('app-notification-title').textContent = title; | |
| 177 $('app-notification-message').textContent = message; | |
| 178 $('app-notification-link').href = link; | |
| 179 $('app-notification-link').textContent = link_message; | |
| 180 | |
| 181 var target = event.target; | |
| 182 while (target.parentElement && target.tagName != "A") { | |
| 183 target = target.parentElement; | |
| 184 } | |
| 185 | |
| 186 // Move the bubble to the right location. | |
| 187 var bubble = $('app-notification-bubble'); | |
| 188 var x = target.parentElement.offsetLeft + | |
| 189 target.parentElement.offsetWidth - 20; | |
| 190 var y = target.parentElement.offsetTop + 20; | |
| 191 bubble.style.left = x + "px"; | |
| 192 bubble.style.top = y + "px"; | |
| 193 | |
| 194 // Move the arrow and shadow to the right location. | |
| 195 var arrow_container = $('arrow-container'); | |
| 196 y += 26; | |
| 197 x -= arrow_container.style.width + 25; | |
| 198 arrow_container.style.left = x + "px"; | |
| 199 arrow_container.style.top = y + "px"; | |
| 200 | |
| 201 // Animate the bubble into view. | |
| 202 bubble.classList.add("notification-bubble-opened"); | |
| 203 bubble.classList.remove("notification-bubble-closed"); | |
| 204 arrow_container.classList.add("notification-bubble-opened"); | |
| 205 arrow_container.classList.remove("notification-bubble-closed"); | |
| 206 | |
| 207 bubble.focus(); | |
| 208 } | |
| 209 | |
| 210 // Hide the notification bubble. | |
| 211 function hideNotificationBubble(event) { | |
| 212 // This will fade the bubble out of existence. | |
| 213 $('app-notification-bubble').classList.add("notification-bubble-closed"); | |
| 214 $('app-notification-bubble').classList.remove("notification-bubble-opened"); | |
| 215 $('arrow-container').classList.add("notification-bubble-closed"); | |
| 216 $('arrow-container').classList.remove("notification-bubble-opened"); | |
| 217 } | |
| 218 | |
| 219 var apps = (function() { | |
| 220 | |
| 221 function createElement(app) { | |
| 222 var div = document.createElement('div'); | |
| 223 div.className = 'app'; | |
| 224 | |
| 225 var a = div.appendChild(document.createElement('a')); | |
| 226 a.className = 'app-anchor'; | |
| 227 a.setAttribute('app-id', app['id']); | |
| 228 a.setAttribute('launch-type', app['launch_type']); | |
| 229 if (typeof(app['notification']) != "undefined") { | |
| 230 a.setAttribute('notification-title', app['notification']['title']); | |
| 231 a.setAttribute('notification-message', app['notification']['body']); | |
| 232 if (typeof(app['notification']['linkUrl']) != "undefined" && | |
| 233 typeof(app['notification']['linkText']) != "undefined") { | |
| 234 a.setAttribute('notification-link', app['notification']['linkUrl']); | |
| 235 a.setAttribute('notification-link-message', | |
| 236 app['notification']['linkText']); | |
| 237 } | |
| 238 } | |
| 239 a.draggable = false; | |
| 240 a.href = app['launch_url']; | |
| 241 | |
| 242 var span = a.appendChild(document.createElement('span')); | |
| 243 span.textContent = app['name']; | |
| 244 | |
| 245 span = a.appendChild(document.createElement('span')); | |
| 246 span.className = "app_notification"; | |
| 247 span.textContent = | |
| 248 typeof(app['notification']) != "undefined" && | |
| 249 typeof(app['notification']['title']) != "undefined" ? | |
| 250 app['notification']['title'] : ""; | |
| 251 span.onclick = handleClick; | |
| 252 | |
| 253 $("app-notification-close").onclick = hideNotificationBubble; | |
| 254 $("app-notification-bubble").setAttribute("tabIndex", 0); | |
| 255 $("app-notification-bubble").onblur = hideNotificationBubble; | |
| 256 | |
| 257 return div; | |
| 258 } | |
| 259 | |
| 260 /** | |
| 261 * Launches an application. | |
| 262 * @param {string} appId Application to launch. | |
| 263 * @param {MouseEvent} opt_mouseEvent Mouse event from the click that | |
| 264 * triggered the launch, used to detect modifier keys that change | |
| 265 * the tab's disposition. | |
| 266 */ | |
| 267 function launchApp(appId, opt_mouseEvent) { | |
| 268 var args = [appId, getAppLaunchType()]; | |
| 269 if (opt_mouseEvent) { | |
| 270 // Launch came from a click - add details of the click | |
| 271 // Otherwise it came from a 'command' event from elsewhere in the UI. | |
| 272 args.push(opt_mouseEvent.altKey, opt_mouseEvent.ctrlKey, | |
| 273 opt_mouseEvent.metaKey, opt_mouseEvent.shiftKey, | |
| 274 opt_mouseEvent.button); | |
| 275 } | |
| 276 chrome.send('launchApp', args); | |
| 277 } | |
| 278 | |
| 279 function isAppSectionMaximized() { | |
| 280 return getAppLaunchType() == APP_LAUNCH.NTP_APPS_MAXIMIZED && | |
| 281 !$('apps').classList.contains('disabled'); | |
| 282 } | |
| 283 | |
| 284 function isAppsMenu(node) { | |
| 285 return node.id == 'apps-menu'; | |
| 286 } | |
| 287 | |
| 288 function getAppLaunchType() { | |
| 289 // We determine if the apps section is maximized, collapsed or in menu mode | |
| 290 // based on the class of the apps section. | |
| 291 if ($('apps').classList.contains('menu')) | |
| 292 return APP_LAUNCH.NTP_APPS_MENU; | |
| 293 else if ($('apps').classList.contains('collapsed')) | |
| 294 return APP_LAUNCH.NTP_APPS_COLLAPSED; | |
| 295 else | |
| 296 return APP_LAUNCH.NTP_APPS_MAXIMIZED; | |
| 297 } | |
| 298 | |
| 299 /** | |
| 300 * @this {!HTMLAnchorElement} | |
| 301 */ | |
| 302 function handleClick(e) { | |
| 303 var appId = e.currentTarget.getAttribute('app-id'); | |
| 304 if (appId == null) { | |
| 305 showNotificationBubble(e); | |
| 306 e.stopPropagation(); | |
| 307 return false; | |
| 308 } | |
| 309 | |
| 310 if (!appDragAndDrop.isDragging()) | |
| 311 launchApp(appId, e); | |
| 312 return false; | |
| 313 } | |
| 314 | |
| 315 // Keep in sync with LaunchType in extension_prefs.h | |
| 316 var LaunchType = { | |
| 317 LAUNCH_PINNED: 0, | |
| 318 LAUNCH_REGULAR: 1, | |
| 319 LAUNCH_FULLSCREEN: 2, | |
| 320 LAUNCH_WINDOW: 3 | |
| 321 }; | |
| 322 | |
| 323 // Keep in sync with LaunchContainer in extension_constants.h | |
| 324 var LaunchContainer = { | |
| 325 LAUNCH_WINDOW: 0, | |
| 326 LAUNCH_PANEL: 1, | |
| 327 LAUNCH_TAB: 2 | |
| 328 }; | |
| 329 | |
| 330 var currentApp; | |
| 331 var promoHasBeenSeen = false; | |
| 332 | |
| 333 function addContextMenu(el, app) { | |
| 334 el.addEventListener('contextmenu', cr.ui.contextMenuHandler); | |
| 335 el.addEventListener('keydown', cr.ui.contextMenuHandler); | |
| 336 el.addEventListener('keyup', cr.ui.contextMenuHandler); | |
| 337 | |
| 338 Object.defineProperty(el, 'contextMenu', { | |
| 339 get: function() { | |
| 340 currentApp = app; | |
| 341 | |
| 342 $('apps-launch-command').label = app['name']; | |
| 343 $('apps-options-command').canExecuteChange(); | |
| 344 $('apps-uninstall-command').canExecuteChange(); | |
| 345 | |
| 346 var launchTypeEl; | |
| 347 if (el.getAttribute('app-id') === app['id']) { | |
| 348 launchTypeEl = el; | |
| 349 } else { | |
| 350 appLinkSel = 'a[app-id=' + app['id'] + ']'; | |
| 351 launchTypeEl = el.querySelector(appLinkSel); | |
| 352 } | |
| 353 | |
| 354 var launchType = launchTypeEl.getAttribute('launch-type'); | |
| 355 var launchContainer = app['launch_container']; | |
| 356 var isPanel = launchContainer == LaunchContainer.LAUNCH_PANEL; | |
| 357 | |
| 358 // Update the commands related to the launch type. | |
| 359 var launchTypeIds = ['apps-launch-type-pinned', | |
| 360 'apps-launch-type-regular', | |
| 361 'apps-launch-type-fullscreen', | |
| 362 'apps-launch-type-window']; | |
| 363 launchTypeIds.forEach(function(id) { | |
| 364 var command = $(id); | |
| 365 command.disabled = isPanel; | |
| 366 command.checked = !isPanel && | |
| 367 launchType == command.getAttribute('launch-type'); | |
| 368 }); | |
| 369 | |
| 370 return $('app-context-menu'); | |
| 371 } | |
| 372 }); | |
| 373 } | |
| 374 | |
| 375 document.addEventListener('command', function(e) { | |
| 376 if (!currentApp) | |
| 377 return; | |
| 378 | |
| 379 var commandId = e.command.id; | |
| 380 switch (commandId) { | |
| 381 case 'apps-options-command': | |
| 382 window.location = currentApp['options_url']; | |
| 383 break; | |
| 384 case 'apps-launch-command': | |
| 385 launchApp(currentApp['id']); | |
| 386 break; | |
| 387 case 'apps-uninstall-command': | |
| 388 chrome.send('uninstallApp', [currentApp['id']]); | |
| 389 break; | |
| 390 case 'apps-create-shortcut-command': | |
| 391 chrome.send('createAppShortcut', [currentApp['id']]); | |
| 392 break; | |
| 393 case 'apps-launch-type-pinned': | |
| 394 case 'apps-launch-type-regular': | |
| 395 case 'apps-launch-type-fullscreen': | |
| 396 case 'apps-launch-type-window': | |
| 397 chrome.send('setLaunchType', | |
| 398 [currentApp['id'], | |
| 399 Number(e.command.getAttribute('launch-type'))]); | |
| 400 break; | |
| 401 } | |
| 402 }); | |
| 403 | |
| 404 document.addEventListener('canExecute', function(e) { | |
| 405 switch (e.command.id) { | |
| 406 case 'apps-options-command': | |
| 407 e.canExecute = currentApp && currentApp['options_url']; | |
| 408 break; | |
| 409 case 'apps-launch-command': | |
| 410 e.canExecute = true; | |
| 411 break; | |
| 412 case 'apps-uninstall-command': | |
| 413 e.canExecute = currentApp && currentApp['can_uninstall']; | |
| 414 break; | |
| 415 } | |
| 416 }); | |
| 417 | |
| 418 // Moves the element at position |from| in array |arr| to position |to|. | |
| 419 function arrayMove(arr, from, to) { | |
| 420 var element = arr.splice(from, 1); | |
| 421 arr.splice(to, 0, element[0]); | |
| 422 } | |
| 423 | |
| 424 // The autoscroll rate during drag and drop, in px per second. | |
| 425 var APP_AUTOSCROLL_RATE = 400; | |
| 426 | |
| 427 return { | |
| 428 loaded: false, | |
| 429 | |
| 430 menu: $('apps-menu'), | |
| 431 | |
| 432 showPromo: false, | |
| 433 | |
| 434 detachWebstoreEntry: false, | |
| 435 | |
| 436 scrollMouseXY_: null, | |
| 437 | |
| 438 scrollListener_: null, | |
| 439 | |
| 440 // The list of app ids, in order, of each app in the launcher. | |
| 441 data_: null, | |
| 442 get data() { return this.data_; }, | |
| 443 set data(data) { | |
| 444 this.data_ = data.map(function(app) { | |
| 445 return app.id; | |
| 446 }); | |
| 447 this.invalidate_(); | |
| 448 }, | |
| 449 | |
| 450 dirty_: true, | |
| 451 invalidate_: function() { | |
| 452 this.dirty_ = true; | |
| 453 }, | |
| 454 | |
| 455 visible_: true, | |
| 456 get visible() { | |
| 457 return this.visible_; | |
| 458 }, | |
| 459 set visible(visible) { | |
| 460 this.visible_ = visible; | |
| 461 this.invalidate_(); | |
| 462 }, | |
| 463 | |
| 464 maybePingPromoSeen_: function() { | |
| 465 if (promoHasBeenSeen || !this.showPromo || !isAppSectionMaximized()) | |
| 466 return; | |
| 467 | |
| 468 promoHasBeenSeen = true; | |
| 469 chrome.send('promoSeen', []); | |
| 470 }, | |
| 471 | |
| 472 // DragAndDropDelegate | |
| 473 | |
| 474 dragContainer: $('apps-content'), | |
| 475 transitionsDuration: 200, | |
| 476 | |
| 477 get dragItem() { return this.dragItem_; }, | |
| 478 set dragItem(dragItem) { | |
| 479 if (this.dragItem_ != dragItem) { | |
| 480 this.dragItem_ = dragItem; | |
| 481 this.invalidate_(); | |
| 482 } | |
| 483 }, | |
| 484 | |
| 485 // The dimensions of each item in the app launcher. | |
| 486 dimensions_: null, | |
| 487 get dimensions() { | |
| 488 if (this.dimensions_) | |
| 489 return this.dimensions_; | |
| 490 | |
| 491 var width = 124; | |
| 492 var height = 136; | |
| 493 | |
| 494 var marginWidth = 6; | |
| 495 var marginHeight = 10; | |
| 496 | |
| 497 var borderWidth = 0; | |
| 498 var borderHeight = 0; | |
| 499 | |
| 500 this.dimensions_ = { | |
| 501 width: width + marginWidth + borderWidth, | |
| 502 height: height + marginHeight + borderHeight | |
| 503 }; | |
| 504 | |
| 505 return this.dimensions_; | |
| 506 }, | |
| 507 | |
| 508 // Gets the item under the mouse event |e|. Returns null if there is no | |
| 509 // item or if the item is not draggable. | |
| 510 getItem: function(e) { | |
| 511 var item = findAncestorByClass(e.target, 'app'); | |
| 512 | |
| 513 // You can't drag the web store launcher. | |
| 514 if (item && item.classList.contains('web-store-entry')) | |
| 515 return null; | |
| 516 | |
| 517 return item; | |
| 518 }, | |
| 519 | |
| 520 // Returns true if |coordinates| point to a valid drop location. The | |
| 521 // coordinates are relative to the drag container and the object should | |
| 522 // have the 'x' and 'y' properties set. | |
| 523 canDropOn: function(coordinates) { | |
| 524 var cols = MAX_APPS_PER_ROW[layoutMode]; | |
| 525 var rows = Math.ceil(this.data.length / cols); | |
| 526 | |
| 527 var bottom = rows * this.dimensions.height; | |
| 528 var right = cols * this.dimensions.width; | |
| 529 | |
| 530 if (coordinates.x >= right || coordinates.x < 0 || | |
| 531 coordinates.y >= bottom || coordinates.y < 0) | |
| 532 return false; | |
| 533 | |
| 534 var position = this.getIndexAt_(coordinates); | |
| 535 var appCount = this.data.length; | |
| 536 | |
| 537 if (!this.detachWebstoreEntry) | |
| 538 appCount--; | |
| 539 | |
| 540 return position >= 0 && position < appCount; | |
| 541 }, | |
| 542 | |
| 543 setDragPlaceholder: function(coordinates) { | |
| 544 var position = this.getIndexAt_(coordinates); | |
| 545 var appId = this.dragItem.querySelector('a').getAttribute('app-id'); | |
| 546 var current = this.data.indexOf(appId); | |
| 547 | |
| 548 if (current == position || current < 0) | |
| 549 return; | |
| 550 | |
| 551 arrayMove(this.data, current, position); | |
| 552 this.invalidate_(); | |
| 553 this.layout(); | |
| 554 }, | |
| 555 | |
| 556 getIndexAt_: function(coordinates) { | |
| 557 var w = this.dimensions.width; | |
| 558 var h = this.dimensions.height; | |
| 559 | |
| 560 var appsPerRow = MAX_APPS_PER_ROW[layoutMode]; | |
| 561 | |
| 562 var row = Math.floor(coordinates.y / h); | |
| 563 var col = Math.floor(coordinates.x / w); | |
| 564 var index = appsPerRow * row + col; | |
| 565 | |
| 566 var appCount = this.data.length; | |
| 567 var rows = Math.ceil(appCount / appsPerRow); | |
| 568 | |
| 569 // Rather than making the free space on the last row invalid, we | |
| 570 // map it to the last valid position. | |
| 571 if (index >= appCount && index < appsPerRow * rows) | |
| 572 return appCount-1; | |
| 573 | |
| 574 return index; | |
| 575 }, | |
| 576 | |
| 577 scrollPage: function(xy) { | |
| 578 var rect = this.dragContainer.getBoundingClientRect(); | |
| 579 | |
| 580 // Here, we calculate the visible boundaries of the app launcher, which | |
| 581 // are then used to determine when we should auto-scroll. | |
| 582 var top = $('apps').getBoundingClientRect().bottom; | |
| 583 var bottomFudge = 15; // Fudge factor due to a gradient mask. | |
| 584 var bottom = top + maxiviewVisibleHeight - bottomFudge; | |
| 585 var left = rect.left + window.scrollX; | |
| 586 var right = Math.min(window.innerWidth, rect.left + rect.width); | |
| 587 | |
| 588 var dy = Math.min(0, xy.y - top) + Math.max(0, xy.y - bottom); | |
| 589 var dx = Math.min(0, xy.x - left) + Math.max(0, xy.x - right); | |
| 590 | |
| 591 if (dx == 0 && dy == 0) { | |
| 592 this.stopScroll_(); | |
| 593 return; | |
| 594 } | |
| 595 | |
| 596 // If we scroll the page directly from this method, it may be choppy and | |
| 597 // inconsistent. Instead, we loop using animation frames, and scroll at a | |
| 598 // speed that's independent of how many times this method is called. | |
| 599 this.scrollMouseXY_ = {dx: dx, dy: dy}; | |
| 600 | |
| 601 if (!this.scrollListener_) { | |
| 602 this.scrollListener_ = this.scrollImpl_.bind(this); | |
| 603 this.scrollStep_(); | |
| 604 } | |
| 605 }, | |
| 606 | |
| 607 scrollStep_: function() { | |
| 608 this.scrollStart_ = Date.now(); | |
| 609 window.webkitRequestAnimationFrame(this.scrollListener_); | |
| 610 }, | |
| 611 | |
| 612 scrollImpl_: function(time) { | |
| 613 if (!appDragAndDrop.isDragging()) { | |
| 614 this.stopScroll_(); | |
| 615 return; | |
| 616 } | |
| 617 | |
| 618 if (!this.scrollMouseXY_) | |
| 619 return; | |
| 620 | |
| 621 var step = time - this.scrollStart_; | |
| 622 | |
| 623 window.scrollBy( | |
| 624 this.calcScroll_(this.scrollMouseXY_.dx, step), | |
| 625 this.calcScroll_(this.scrollMouseXY_.dy, step)); | |
| 626 | |
| 627 this.scrollStep_(); | |
| 628 }, | |
| 629 | |
| 630 calcScroll_: function(delta, step) { | |
| 631 if (delta == 0) | |
| 632 return 0; | |
| 633 | |
| 634 // Increase the multiplier for every 50px the mouse is beyond the edge. | |
| 635 var sign = delta > 0 ? 1 : -1; | |
| 636 var scalar = APP_AUTOSCROLL_RATE * step / 1000; | |
| 637 var multiplier = Math.floor(Math.abs(delta) / 50) + 1; | |
| 638 | |
| 639 return sign * scalar * multiplier; | |
| 640 }, | |
| 641 | |
| 642 stopScroll_: function() { | |
| 643 this.scrollListener_ = null; | |
| 644 this.scrollMouseXY_ = null; | |
| 645 }, | |
| 646 | |
| 647 saveDrag: function(draggedItem) { | |
| 648 this.invalidate_(); | |
| 649 this.layout(); | |
| 650 | |
| 651 var draggedAppId = draggedItem.querySelector('a').getAttribute('app-id'); | |
| 652 var appIds = this.data.filter(function(id) { | |
| 653 return id != 'web-store-entry'; | |
| 654 }); | |
| 655 | |
| 656 // Wait until the transitions are complete before notifying the browser. | |
| 657 // Otherwise, the apps will be re-rendered while still transitioning. | |
| 658 setTimeout(function() { | |
| 659 chrome.send('reorderApps', [draggedAppId, appIds]); | |
| 660 }, this.transitionsDuration + 10); | |
| 661 }, | |
| 662 | |
| 663 layout: function(options) { | |
| 664 options = options || {}; | |
| 665 if (!this.dirty_ && options.force != true) | |
| 666 return; | |
| 667 | |
| 668 try { | |
| 669 var container = this.dragContainer; | |
| 670 if (options.disableAnimations) | |
| 671 container.setAttribute('launcher-animations', false); | |
| 672 var d0 = Date.now(); | |
| 673 this.layoutImpl_(); | |
| 674 this.dirty_ = false; | |
| 675 logEvent('apps.layout: ' + (Date.now() - d0)); | |
| 676 | |
| 677 } finally { | |
| 678 if (options.disableAnimations) { | |
| 679 // We need to re-enable animations asynchronously, so that the | |
| 680 // animations are still disabled for this layout update. | |
| 681 setTimeout(function() { | |
| 682 container.setAttribute('launcher-animations', true); | |
| 683 }, 0); | |
| 684 } | |
| 685 } | |
| 686 }, | |
| 687 | |
| 688 layoutImpl_: function() { | |
| 689 var apps = this.data || []; | |
| 690 var rects = this.getLayoutRects_(apps.length); | |
| 691 var appsContent = this.dragContainer; | |
| 692 | |
| 693 // Ping the PROMO_SEEN histogram only when the promo is maximized, and | |
| 694 // maximum once per NTP load. | |
| 695 this.maybePingPromoSeen_(); | |
| 696 | |
| 697 if (!this.visible) | |
| 698 return; | |
| 699 | |
| 700 for (var i = 0; i < apps.length; i++) { | |
| 701 var app = appsContent.querySelector('[app-id='+apps[i]+']').parentNode; | |
| 702 | |
| 703 // If the node is being dragged, don't try to place it in the grid. | |
| 704 if (app == this.dragItem) | |
| 705 continue; | |
| 706 | |
| 707 app.style.left = rects[i].left + 'px'; | |
| 708 app.style.top = rects[i].top + 'px'; | |
| 709 } | |
| 710 | |
| 711 // We need to set the container's height manually because the apps use | |
| 712 // absolute positioning. | |
| 713 var rows = Math.ceil(apps.length / MAX_APPS_PER_ROW[layoutMode]); | |
| 714 appsContent.style.height = (rows * this.dimensions.height) + 'px'; | |
| 715 }, | |
| 716 | |
| 717 getLayoutRects_: function(appCount) { | |
| 718 var availableWidth = this.dragContainer.offsetWidth; | |
| 719 var rtl = isRtl(); | |
| 720 var rects = []; | |
| 721 var w = this.dimensions.width; | |
| 722 var h = this.dimensions.height; | |
| 723 var appsPerRow = MAX_APPS_PER_ROW[layoutMode]; | |
| 724 | |
| 725 for (var i = 0; i < appCount; i++) { | |
| 726 var top = Math.floor(i / appsPerRow) * h; | |
| 727 var left = (i % appsPerRow) * w; | |
| 728 | |
| 729 // Reflect the X axis if an RTL language is active. | |
| 730 if (rtl) | |
| 731 left = availableWidth - left - w; | |
| 732 rects[i] = {left: left, top: top}; | |
| 733 } | |
| 734 return rects; | |
| 735 }, | |
| 736 | |
| 737 get loadedImages() { | |
| 738 return this.loadedImages_; | |
| 739 }, | |
| 740 | |
| 741 set loadedImages(value) { | |
| 742 this.loadedImages_ = value; | |
| 743 if (this.loadedImages_ == 0) | |
| 744 return; | |
| 745 | |
| 746 // Each application icon is loaded asynchronously. Here, we display | |
| 747 // the icons once they've all been loaded to make it look nicer. | |
| 748 if (this.loadedImages_ == this.data.length) { | |
| 749 this.showImages(); | |
| 750 return; | |
| 751 } | |
| 752 | |
| 753 // We won't actually have the visible height until the sections have | |
| 754 // been layed out. | |
| 755 if (!maxiviewVisibleHeight) | |
| 756 return; | |
| 757 | |
| 758 // If we know the visible height of the maxiview, then we can don't need | |
| 759 // to wait for all the icons. Instead, we wait until the visible portion | |
| 760 // have been loaded. | |
| 761 var appsPerRow = MAX_APPS_PER_ROW[layoutMode]; | |
| 762 var rows = Math.ceil(maxiviewVisibleHeight / this.dimensions.height); | |
| 763 var count = Math.min(appsPerRow * rows, this.data.length); | |
| 764 if (this.loadedImages_ == count) { | |
| 765 this.showImages(); | |
| 766 return; | |
| 767 } | |
| 768 }, | |
| 769 | |
| 770 showImages: function() { | |
| 771 $('apps-content').classList.add('visible'); | |
| 772 clearTimeout(this.imageTimer); | |
| 773 }, | |
| 774 | |
| 775 createElement: function(app) { | |
| 776 var container = document.createElement('div'); | |
| 777 var div = createElement(app); | |
| 778 container.appendChild(div); | |
| 779 var a = div.firstChild; | |
| 780 | |
| 781 a.onclick = handleClick; | |
| 782 a.ping = getAppPingUrl( | |
| 783 'PING_BY_ID', this.showPromo, 'NTP_APPS_MAXIMIZED'); | |
| 784 a.style.backgroundImage = url(app['icon_big']); | |
| 785 if (app.isNew) { | |
| 786 div.setAttribute('new', 'new'); | |
| 787 // Delay changing the attribute a bit to let the page settle down a bit. | |
| 788 setTimeout(function() { | |
| 789 // Make sure the new icon is scrolled into view. | |
| 790 document.body.scrollTop = document.body.scrollHeight; | |
| 791 | |
| 792 // This will trigger the 'bounce' animation defined in apps.css. | |
| 793 div.setAttribute('new', 'installed'); | |
| 794 }, 500); | |
| 795 div.addEventListener('webkitAnimationEnd', function(e) { | |
| 796 div.removeAttribute('new'); | |
| 797 }); | |
| 798 } | |
| 799 | |
| 800 // CSS background images don't fire 'load' events, so we use an Image. | |
| 801 var img = new Image(); | |
| 802 img.onload = function() { this.loadedImages++; }.bind(this); | |
| 803 img.src = app['icon_big']; | |
| 804 | |
| 805 // User cannot change launch options or uninstall component extension. | |
| 806 if (!app['is_component']) { | |
| 807 var settingsButton = div.appendChild(new cr.ui.ContextMenuButton); | |
| 808 settingsButton.className = 'app-settings'; | |
| 809 settingsButton.title = localStrings.getString('appsettings'); | |
| 810 addContextMenu(div, app); | |
| 811 } | |
| 812 | |
| 813 if (app.notifications && app.notifications.length > 0) { | |
| 814 // Create the notification div below the app icon that is used to | |
| 815 // trigger the hidden notification bubble to appear. | |
| 816 var notification = document.createElement('div') | |
| 817 container.appendChild(notification); | |
| 818 var title = document.createElement('span'); | |
| 819 title.textContent = app.notifications[0].title; | |
| 820 notification.appendChild(title); | |
| 821 notification.appendChild(document.createElement('br')); | |
| 822 | |
| 823 var body = document.createElement('span'); | |
| 824 container.appendChild(body); | |
| 825 body.textContent = app.notifications[0].body; | |
| 826 notification.appendChild(body); | |
| 827 if (app.notifications[0].linkUrl) { | |
| 828 notification.appendChild(document.createElement('br')); | |
| 829 var link = document.createElement('a'); | |
| 830 link.href = app.notifications[0].linkUrl; | |
| 831 link.textContent = app.notifications[0].linkText ? | |
| 832 app.notifications[0].linkText : "link"; | |
| 833 notification.appendChild(link); | |
| 834 } | |
| 835 } | |
| 836 | |
| 837 return container; | |
| 838 }, | |
| 839 | |
| 840 createMiniviewElement: function(app) { | |
| 841 var span = document.createElement('span'); | |
| 842 var a = span.appendChild(document.createElement('a')); | |
| 843 | |
| 844 a.setAttribute('app-id', app['id']); | |
| 845 a.textContent = app['name']; | |
| 846 a.href = app['launch_url']; | |
| 847 a.onclick = handleClick; | |
| 848 a.ping = getAppPingUrl( | |
| 849 'PING_BY_ID', this.showPromo, 'NTP_APPS_COLLAPSED'); | |
| 850 a.style.backgroundImage = url(app['icon_small']); | |
| 851 a.className = 'item'; | |
| 852 span.appendChild(a); | |
| 853 | |
| 854 // User cannot change launch options or uninstall component extension. | |
| 855 if (!app['is_component']) { | |
| 856 addContextMenu(span, app); | |
| 857 } | |
| 858 | |
| 859 return span; | |
| 860 }, | |
| 861 | |
| 862 createClosedMenuElement: function(app) { | |
| 863 var a = document.createElement('a'); | |
| 864 a.setAttribute('app-id', app['id']); | |
| 865 a.textContent = app['name']; | |
| 866 a.href = app['launch_url']; | |
| 867 a.onclick = handleClick; | |
| 868 a.ping = getAppPingUrl( | |
| 869 'PING_BY_ID', this.showPromo, 'NTP_APPS_MENU'); | |
| 870 a.style.backgroundImage = url(app['icon_small']); | |
| 871 a.className = 'item'; | |
| 872 | |
| 873 // User cannot change launch options or uninstall component extension. | |
| 874 if (!app['is_component']) { | |
| 875 addContextMenu(a, app); | |
| 876 } | |
| 877 | |
| 878 return a; | |
| 879 }, | |
| 880 | |
| 881 createWebStoreElement: function() { | |
| 882 var elm = createElement({ | |
| 883 'id': 'web-store-entry', | |
| 884 'name': localStrings.getString('web_store_title'), | |
| 885 'launch_url': localStrings.getString('web_store_url') | |
| 886 }); | |
| 887 elm.classList.add('web-store-entry'); | |
| 888 return elm; | |
| 889 }, | |
| 890 | |
| 891 createWebStoreMiniElement: function() { | |
| 892 var span = document.createElement('span'); | |
| 893 span.appendChild(this.createWebStoreClosedMenuElement()); | |
| 894 return span; | |
| 895 }, | |
| 896 | |
| 897 createWebStoreClosedMenuElement: function() { | |
| 898 var a = document.createElement('a'); | |
| 899 a.textContent = localStrings.getString('web_store_title'); | |
| 900 a.href = localStrings.getString('web_store_url'); | |
| 901 a.style.backgroundImage = url('chrome://theme/IDR_WEBSTORE_ICON_16'); | |
| 902 a.className = 'item'; | |
| 903 return a; | |
| 904 } | |
| 905 }; | |
| 906 })(); | |
| 907 | |
| 908 // Enable drag and drop reordering of the app launcher. | |
| 909 var appDragAndDrop = new DragAndDropController(apps); | |
| OLD | NEW |