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 |