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 // To avoid creating tons of unnecessary nodes. We assume we cannot fit more | |
6 // than this many items in the miniview. | |
7 var MAX_MINIVIEW_ITEMS = 15; | |
8 | |
9 // Extra spacing at the top of the layout. | |
10 var LAYOUT_SPACING_TOP = 25; | |
11 | |
12 // The visible height of the expanded maxiview. | |
13 var maxiviewVisibleHeight = 0; | |
14 | |
15 var APP_LAUNCH = { | |
16 // The histogram buckets (keep in sync with extension_constants.h). | |
17 NTP_APPS_MAXIMIZED: 0, | |
18 NTP_APPS_COLLAPSED: 1, | |
19 NTP_APPS_MENU: 2, | |
20 NTP_MOST_VISITED: 3, | |
21 NTP_RECENTLY_CLOSED: 4, | |
22 NTP_APP_RE_ENABLE: 16 | |
23 }; | |
24 | |
25 var APP_LAUNCH_URL = { | |
26 // The URL prefix for pings that record app launches by URL. | |
27 PING_BY_URL: 'record-app-launch-by-url', | |
28 | |
29 // The URL prefix for pings that record app launches by ID. | |
30 PING_BY_ID: 'record-app-launch-by-id', | |
31 | |
32 // The URL prefix used by the webstore link 'ping' attributes. | |
33 PING_WEBSTORE: 'record-webstore-launch' | |
34 }; | |
35 | |
36 function getAppPingUrl(prefix, data, bucket) { | |
37 return [APP_LAUNCH_URL[prefix], | |
38 encodeURIComponent(data), | |
39 APP_LAUNCH[bucket]].join('+'); | |
40 } | |
41 | |
42 function getSectionCloseButton(sectionId) { | |
43 return document.querySelector('#' + sectionId + ' .section-close-button'); | |
44 } | |
45 | |
46 function getSectionMenuButton(sectionId) { | |
47 return $(sectionId + '-button'); | |
48 } | |
49 | |
50 function getSectionMenuButtonTextId(sectionId) { | |
51 return sectionId.replace(/-/g, ''); | |
52 } | |
53 | |
54 function setSectionMenuMode(sectionId, section, menuModeEnabled, menuModeMask) { | |
55 var el = $(sectionId); | |
56 if (!menuModeEnabled) { | |
57 // Because sections are collapsed when they are in menu mode, it is not | |
58 // necessary to restore the maxiview here. It will happen if the section | |
59 // header is clicked. | |
60 // TODO(aa): Sections should maintain their collapse state when minimized. | |
61 el.classList.remove('menu'); | |
62 shownSections &= ~menuModeMask; | |
63 } else { | |
64 if (section) { | |
65 hideSection(section); // To hide the maxiview. | |
66 } | |
67 el.classList.add('menu'); | |
68 shownSections |= menuModeMask; | |
69 } | |
70 layoutSections(); | |
71 } | |
72 | |
73 function clearClosedMenu(menu) { | |
74 menu.innerHTML = ''; | |
75 } | |
76 | |
77 function addClosedMenuEntryWithLink(menu, a) { | |
78 var span = document.createElement('span'); | |
79 a.className += ' item menuitem'; | |
80 span.appendChild(a); | |
81 menu.appendChild(span); | |
82 } | |
83 | |
84 function addClosedMenuEntry(menu, url, title, imageUrl, opt_pingUrl) { | |
85 var a = document.createElement('a'); | |
86 a.href = url; | |
87 a.textContent = title; | |
88 a.style.backgroundImage = 'url(' + imageUrl + ')'; | |
89 if (opt_pingUrl) | |
90 a.ping = opt_pingUrl; | |
91 addClosedMenuEntryWithLink(menu, a); | |
92 } | |
93 | |
94 function addClosedMenuFooter(menu, sectionId, mask, opt_section) { | |
95 menu.appendChild(document.createElement('hr')); | |
96 | |
97 var span = document.createElement('span'); | |
98 var a = span.appendChild(document.createElement('a')); | |
99 a.href = ''; | |
100 if (cr.isChromeOS) { | |
101 a.textContent = localStrings.getString('expandMenu'); | |
102 } else { | |
103 a.textContent = | |
104 localStrings.getString(getSectionMenuButtonTextId(sectionId)); | |
105 } | |
106 a.className = 'item'; | |
107 a.addEventListener( | |
108 'click', | |
109 function(e) { | |
110 getSectionMenuButton(sectionId).hideMenu(); | |
111 e.preventDefault(); | |
112 setSectionMenuMode(sectionId, opt_section, false, mask); | |
113 shownSections &= ~mask; | |
114 saveShownSections(); | |
115 }); | |
116 menu.appendChild(span); | |
117 } | |
118 | |
119 function initializeSection(sectionId, mask, opt_section) { | |
120 var button = getSectionCloseButton(sectionId); | |
121 button.addEventListener( | |
122 'click', | |
123 function() { | |
124 setSectionMenuMode(sectionId, opt_section, true, mask); | |
125 saveShownSections(); | |
126 }); | |
127 } | |
128 | |
129 function updateSimpleSection(id, section) { | |
130 var elm = $(id); | |
131 var maxiview = getSectionMaxiview(elm); | |
132 var miniview = getSectionMiniview(elm); | |
133 if (shownSections & section) { | |
134 // The section is expanded, so the maxiview should be opaque (visible) and | |
135 // the miniview should be hidden. | |
136 elm.classList.remove('collapsed'); | |
137 if (maxiview) { | |
138 maxiview.classList.remove('collapsed'); | |
139 maxiview.classList.add('opaque'); | |
140 } | |
141 if (miniview) | |
142 miniview.classList.remove('opaque'); | |
143 } else { | |
144 // The section is collapsed, so the maxiview should be hidden and the | |
145 // miniview should be opaque. | |
146 elm.classList.add('collapsed'); | |
147 if (maxiview) { | |
148 maxiview.classList.add('collapsed'); | |
149 maxiview.classList.remove('opaque'); | |
150 } | |
151 if (miniview) | |
152 miniview.classList.add('opaque'); | |
153 } | |
154 } | |
155 | |
156 var sessionItems = []; | |
157 | |
158 function foreignSessions(data) { | |
159 logEvent('received foreign sessions'); | |
160 // We need to store the foreign sessions so we can update the layout on a | |
161 // resize. | |
162 sessionItems = data; | |
163 renderForeignSessions(); | |
164 layoutSections(); | |
165 } | |
166 | |
167 function renderForeignSessions() { | |
168 // Remove all existing items and create new items. | |
169 var sessionElement = $('foreign-sessions'); | |
170 var parentSessionElement = sessionElement.lastElementChild; | |
171 parentSessionElement.textContent = ''; | |
172 | |
173 // For each client, create entries and append the lists together. | |
174 sessionItems.forEach(function(item, i) { | |
175 // TODO(zea): Get real client names. See crbug/59672. | |
176 var name = 'Client ' + i; | |
177 parentSessionElement.appendChild(createForeignSession(item, name)); | |
178 }); | |
179 | |
180 layoutForeignSessions(); | |
181 } | |
182 | |
183 function layoutForeignSessions() { | |
184 var sessionElement = $('foreign-sessions'); | |
185 // We cannot use clientWidth here since the width has a transition. | |
186 var availWidth = useSmallGrid() ? 692 : 920; | |
187 var parentSessEl = sessionElement.lastElementChild; | |
188 | |
189 if (parentSessEl.hasChildNodes()) { | |
190 sessionElement.classList.remove('disabled'); | |
191 sessionElement.classList.remove('opaque'); | |
192 } else { | |
193 sessionElement.classList.add('disabled'); | |
194 sessionElement.classList.add('opaque'); | |
195 } | |
196 } | |
197 | |
198 function createForeignSession(client, name) { | |
199 // Vertically stack the windows in a client. | |
200 var stack = document.createElement('div'); | |
201 stack.className = 'foreign-session-client item link'; | |
202 stack.textContent = name; | |
203 stack.sessionTag = client[0].sessionTag; | |
204 | |
205 client.forEach(function(win, i) { | |
206 // Create a window entry. | |
207 var winSpan = document.createElement('span'); | |
208 var winEl = document.createElement('p'); | |
209 winEl.className = 'item link window'; | |
210 winEl.tabItems = win.tabs; | |
211 winEl.tabIndex = 0; | |
212 winEl.textContent = formatTabsText(win.tabs.length); | |
213 winEl.xtitle = win.title; | |
214 winEl.sessionTag = win.sessionTag; | |
215 winEl.winNum = i; | |
216 winEl.addEventListener('click', maybeOpenForeignWindow); | |
217 winEl.addEventListener('keydown', | |
218 handleIfEnterKey(maybeOpenForeignWindow)); | |
219 winSpan.appendChild(winEl); | |
220 | |
221 // Sort tabs by MRU order | |
222 win.tabs.sort(function(a, b) { | |
223 return a.timestamp < b.timestamp; | |
224 }); | |
225 | |
226 // Create individual tab information. | |
227 win.tabs.forEach(function(data) { | |
228 var tabEl = document.createElement('a'); | |
229 tabEl.className = 'item link tab'; | |
230 tabEl.href = data.timestamp; | |
231 tabEl.style.backgroundImage = url('chrome://favicon/' + data.url); | |
232 tabEl.dir = data.direction; | |
233 tabEl.textContent = data.title; | |
234 tabEl.sessionTag = win.sessionTag; | |
235 tabEl.winNum = i; | |
236 tabEl.sessionId = data.sessionId; | |
237 tabEl.addEventListener('click', maybeOpenForeignTab); | |
238 tabEl.addEventListener('keydown', | |
239 handleIfEnterKey(maybeOpenForeignTab)); | |
240 | |
241 winSpan.appendChild(tabEl); | |
242 }); | |
243 | |
244 // Append the window. | |
245 stack.appendChild(winSpan); | |
246 }); | |
247 return stack; | |
248 } | |
249 | |
250 var recentItems = []; | |
251 | |
252 function recentlyClosedTabs(data) { | |
253 logEvent('received recently closed tabs'); | |
254 // We need to store the recent items so we can update the layout on a resize. | |
255 recentItems = data; | |
256 renderRecentlyClosed(); | |
257 layoutSections(); | |
258 } | |
259 | |
260 function renderRecentlyClosed() { | |
261 // Remove all existing items and create new items. | |
262 var recentElement = $('recently-closed'); | |
263 var parentEl = recentElement.lastElementChild; | |
264 parentEl.textContent = ''; | |
265 var recentMenu = $('recently-closed-menu'); | |
266 clearClosedMenu(recentMenu); | |
267 | |
268 recentItems.forEach(function(item) { | |
269 parentEl.appendChild(createRecentItem(item)); | |
270 addRecentMenuItem(recentMenu, item); | |
271 }); | |
272 addClosedMenuFooter(recentMenu, 'recently-closed', MENU_RECENT); | |
273 | |
274 layoutRecentlyClosed(); | |
275 } | |
276 | |
277 function createRecentItem(data) { | |
278 var isWindow = data.type == 'window'; | |
279 var el; | |
280 if (isWindow) { | |
281 el = document.createElement('span'); | |
282 el.className = 'item link window'; | |
283 el.tabItems = data.tabs; | |
284 el.tabIndex = 0; | |
285 el.textContent = formatTabsText(data.tabs.length); | |
286 } else { | |
287 el = document.createElement('a'); | |
288 el.className = 'item'; | |
289 el.href = data.url; | |
290 el.ping = getAppPingUrl( | |
291 'PING_BY_URL', data.url, 'NTP_RECENTLY_CLOSED'); | |
292 el.style.backgroundImage = url('chrome://favicon/' + data.url); | |
293 el.dir = data.direction; | |
294 el.textContent = data.title; | |
295 } | |
296 el.sessionId = data.sessionId; | |
297 el.xtitle = data.title; | |
298 el.sessionTag = data.sessionTag; | |
299 var wrapperEl = document.createElement('span'); | |
300 wrapperEl.appendChild(el); | |
301 return wrapperEl; | |
302 } | |
303 | |
304 function addRecentMenuItem(menu, data) { | |
305 var isWindow = data.type == 'window'; | |
306 var a = document.createElement('a'); | |
307 if (isWindow) { | |
308 a.textContent = formatTabsText(data.tabs.length); | |
309 a.className = 'window'; // To get the icon from the CSS .window rule. | |
310 a.href = ''; // To make underline show up. | |
311 } else { | |
312 a.href = data.url; | |
313 a.ping = getAppPingUrl( | |
314 'PING_BY_URL', data.url, 'NTP_RECENTLY_CLOSED'); | |
315 a.style.backgroundImage = 'url(chrome://favicon/' + data.url + ')'; | |
316 a.textContent = data.title; | |
317 } | |
318 function clickHandler(e) { | |
319 chrome.send('reopenTab', [String(data.sessionId)]); | |
320 e.preventDefault(); | |
321 } | |
322 a.addEventListener('click', clickHandler); | |
323 addClosedMenuEntryWithLink(menu, a); | |
324 } | |
325 | |
326 function saveShownSections() { | |
327 chrome.send('setShownSections', [shownSections]); | |
328 } | |
329 | |
330 var LayoutMode = { | |
331 SMALL: 1, | |
332 NORMAL: 2 | |
333 }; | |
334 | |
335 var layoutMode = useSmallGrid() ? LayoutMode.SMALL : LayoutMode.NORMAL; | |
336 | |
337 function handleWindowResize() { | |
338 if (window.innerWidth < 10) { | |
339 // We're probably a background tab, so don't do anything. | |
340 return; | |
341 } | |
342 | |
343 // TODO(jstritar): Remove the small-layout class and revert back to the | |
344 // @media (max-width) directive once http://crbug.com/70930 is fixed. | |
345 var oldLayoutMode = layoutMode; | |
346 var b = useSmallGrid(); | |
347 if (b) { | |
348 layoutMode = LayoutMode.SMALL; | |
349 document.body.classList.add('small-layout'); | |
350 } else { | |
351 layoutMode = LayoutMode.NORMAL; | |
352 document.body.classList.remove('small-layout'); | |
353 } | |
354 | |
355 if (layoutMode != oldLayoutMode){ | |
356 mostVisited.useSmallGrid = b; | |
357 mostVisited.layout(); | |
358 apps.layout({force:true}); | |
359 renderRecentlyClosed(); | |
360 renderForeignSessions(); | |
361 updateAllMiniviewClippings(); | |
362 } | |
363 | |
364 layoutSections(); | |
365 } | |
366 | |
367 // Stores some information about each section necessary to layout. A new | |
368 // instance is constructed for each section on each layout. | |
369 function SectionLayoutInfo(section) { | |
370 this.section = section; | |
371 this.header = section.querySelector('h2'); | |
372 this.miniview = section.querySelector('.miniview'); | |
373 this.maxiview = getSectionMaxiview(section); | |
374 this.expanded = this.maxiview && !section.classList.contains('collapsed'); | |
375 this.fixedHeight = this.section.offsetHeight; | |
376 this.scrollingHeight = 0; | |
377 | |
378 if (this.expanded) | |
379 this.scrollingHeight = this.maxiview.offsetHeight; | |
380 } | |
381 | |
382 // Get all sections to be layed out. | |
383 SectionLayoutInfo.getAll = function() { | |
384 var sections = document.querySelectorAll( | |
385 '.section:not(.disabled):not(.menu)'); | |
386 var result = []; | |
387 for (var i = 0, section; section = sections[i]; i++) { | |
388 result.push(new SectionLayoutInfo(section)); | |
389 } | |
390 return result; | |
391 }; | |
392 | |
393 // Ensure the miniview sections don't have any clipped items. | |
394 function updateMiniviewClipping(miniview) { | |
395 var clipped = false; | |
396 for (var j = 0, item; item = miniview.children[j]; j++) { | |
397 item.style.display = ''; | |
398 if (clipped || | |
399 (item.offsetLeft + item.offsetWidth) > miniview.offsetWidth) { | |
400 item.style.display = 'none'; | |
401 clipped = true; | |
402 } else { | |
403 item.style.display = ''; | |
404 } | |
405 } | |
406 } | |
407 | |
408 // Ensure none of the miniviews have any clipped items. | |
409 function updateAllMiniviewClippings() { | |
410 var miniviews = document.querySelectorAll('.section.collapsed .miniview'); | |
411 for (var i = 0, miniview; miniview = miniviews[i]; i++) { | |
412 updateMiniviewClipping(miniview); | |
413 } | |
414 } | |
415 | |
416 // Returns whether or not vertical scrollbars are present. | |
417 function hasScrollBars() { | |
418 return window.innerHeight != document.body.clientHeight; | |
419 } | |
420 | |
421 // Enables scrollbars (they will only show up if needed). | |
422 function showScrollBars() { | |
423 document.body.classList.remove('noscroll'); | |
424 } | |
425 | |
426 // Hides all scrollbars. | |
427 function hideScrollBars() { | |
428 document.body.classList.add('noscroll'); | |
429 } | |
430 | |
431 // Returns whether or not the sections are currently animating due to a | |
432 // section transition. | |
433 function isAnimating() { | |
434 var de = document.documentElement; | |
435 return de.getAttribute('enable-section-animations') == 'true'; | |
436 } | |
437 | |
438 // Layout the sections in a modified accordion. The header and miniview, if | |
439 // visible are fixed within the viewport. If there is an expanded section, its | |
440 // it scrolls. | |
441 // | |
442 // ============================= | |
443 // | collapsed section | <- Any collapsed sections are fixed position. | |
444 // | and miniview | | |
445 // |---------------------------| | |
446 // | expanded section | | |
447 // | | <- There can be one expanded section and it | |
448 // | and maxiview | is absolutely positioned so that it can | |
449 // | | scroll "underneath" the fixed elements. | |
450 // | | | |
451 // |---------------------------| | |
452 // | another collapsed section | | |
453 // |---------------------------| | |
454 // | |
455 // We want the main frame scrollbar to be the one that scrolls the expanded | |
456 // region. To get this effect, we make the fixed elements position:fixed and the | |
457 // scrollable element position:absolute. We also artificially increase the | |
458 // height of the document so that it is possible to scroll down enough to | |
459 // display the end of the document, even with any fixed elements at the bottom | |
460 // of the viewport. | |
461 // | |
462 // There is a final twist: If the intrinsic height of the expanded section is | |
463 // less than the available height (because the window is tall), any collapsed | |
464 // sections sinch up and sit below the expanded section. This is so that we | |
465 // don't have a bunch of dead whitespace in the case of expanded sections that | |
466 // aren't very tall. | |
467 function layoutSections() { | |
468 // While transitioning sections, we only want scrollbars to appear if they're | |
469 // already present or the window is being resized (so there's no animation). | |
470 if (!hasScrollBars() && isAnimating()) | |
471 hideScrollBars(); | |
472 | |
473 var sections = SectionLayoutInfo.getAll(); | |
474 var expandedSection = null; | |
475 var headerHeight = LAYOUT_SPACING_TOP; | |
476 var footerHeight = 0; | |
477 | |
478 // Calculate the height of the fixed elements above the expanded section. Also | |
479 // take note of the expanded section, if there is one. | |
480 var i; | |
481 var section; | |
482 for (i = 0; section = sections[i]; i++) { | |
483 headerHeight += section.fixedHeight; | |
484 if (section.expanded) { | |
485 expandedSection = section; | |
486 i++; | |
487 break; | |
488 } | |
489 } | |
490 | |
491 // Include the height of the sync promo bar. | |
492 var sync_promo_height = $('sync-promo').offsetHeight; | |
493 headerHeight += sync_promo_height; | |
494 | |
495 // Calculate the height of the fixed elements below the expanded section, if | |
496 // any. | |
497 for (; section = sections[i]; i++) { | |
498 footerHeight += section.fixedHeight; | |
499 } | |
500 // Leave room for bottom bar if it's visible. | |
501 footerHeight += $('closed-sections-bar').offsetHeight; | |
502 | |
503 | |
504 // Determine the height to use for the expanded section. If there isn't enough | |
505 // space to show the expanded section completely, this will be the available | |
506 // height. Otherwise, we use the intrinsic height of the expanded section. | |
507 var expandedSectionHeight; | |
508 var expandedSectionIsClipped = false; | |
509 if (expandedSection) { | |
510 var flexHeight = window.innerHeight - headerHeight - footerHeight; | |
511 if (flexHeight < expandedSection.scrollingHeight) { | |
512 expandedSectionHeight = flexHeight; | |
513 | |
514 // Also, artificially expand the height of the document so that we can see | |
515 // the entire expanded section. | |
516 // | |
517 // TODO(aa): Where does this come from? It is the difference between what | |
518 // we set document.body.style.height to and what | |
519 // document.body.scrollHeight measures afterward. I expect them to be the | |
520 // same if document.body has no margins. | |
521 var fudge = 44; | |
522 document.body.style.height = | |
523 headerHeight + | |
524 expandedSection.scrollingHeight + | |
525 footerHeight + | |
526 fudge + | |
527 'px'; | |
528 expandedSectionIsClipped = true; | |
529 } else { | |
530 expandedSectionHeight = expandedSection.scrollingHeight; | |
531 document.body.style.height = ''; | |
532 } | |
533 } else { | |
534 // We only set the document height when a section is expanded. If | |
535 // all sections are collapsed, then get rid of the previous height. | |
536 document.body.style.height = ''; | |
537 } | |
538 | |
539 maxiviewVisibleHeight = expandedSectionHeight; | |
540 | |
541 // Now position all the elements. | |
542 var y = LAYOUT_SPACING_TOP + sync_promo_height; | |
543 for (i = 0, section; section = sections[i]; i++) { | |
544 section.section.style.top = y + 'px'; | |
545 y += section.fixedHeight; | |
546 | |
547 if (section.maxiview) { | |
548 if (section == expandedSection) { | |
549 section.maxiview.style.top = y + 'px'; | |
550 } else { | |
551 // The miniviews fade out gradually, so it may have height at this | |
552 // point. We position the maxiview as if the miniview was not displayed | |
553 // by subtracting off the miniview's total height (height + margin). | |
554 var miniviewFudge = 40; // miniview margin-bottom + margin-top | |
555 var miniviewHeight = section.miniview.offsetHeight + miniviewFudge; | |
556 section.maxiview.style.top = y - miniviewHeight + 'px'; | |
557 } | |
558 } | |
559 | |
560 if (section.maxiview && section == expandedSection) | |
561 updateMask( | |
562 section.maxiview, expandedSectionHeight, expandedSectionIsClipped); | |
563 | |
564 if (section == expandedSection) | |
565 y += expandedSectionHeight; | |
566 } | |
567 if (cr.isChromeOS) | |
568 $('closed-sections-bar').style.top = y + 'px'; | |
569 | |
570 // Position the notification container below the sync promo. | |
571 $('notification-container').style.top = sync_promo_height + 'px'; | |
572 | |
573 updateMenuSections(); | |
574 updateAttributionDisplay(y); | |
575 } | |
576 | |
577 function updateMask(maxiview, visibleHeightPx, isClipped) { | |
578 // If the section isn't actually clipped, then we don't want to use a mask at | |
579 // all, since enabling one turns off subpixel anti-aliasing. | |
580 if (!isClipped) { | |
581 maxiview.style.WebkitMaskImage = 'none'; | |
582 return; | |
583 } | |
584 | |
585 // We want to end up with 10px gradients at the top and bottom of | |
586 // visibleHeight, but webkit-mask only supports expression in terms of | |
587 // percentages. | |
588 | |
589 // We might not have enough room to do 10px gradients on each side. To get the | |
590 // right effect, we don't want to make the gradients smaller, but make them | |
591 // appear to mush into each other. | |
592 var gradientHeightPx = Math.min(10, Math.floor(visibleHeightPx / 2)); | |
593 var gradientDestination = 'rgba(0,0,0,' + (gradientHeightPx / 10) + ')'; | |
594 | |
595 var bottomSpacing = 15; | |
596 var first = parseFloat(maxiview.style.top) / window.innerHeight; | |
597 var second = first + gradientHeightPx / window.innerHeight; | |
598 var fourth = first + (visibleHeightPx - bottomSpacing) / window.innerHeight; | |
599 var third = fourth - gradientHeightPx / window.innerHeight; | |
600 | |
601 var gradientArguments = [ | |
602 'transparent', | |
603 getColorStopString(first, 'transparent'), | |
604 getColorStopString(second, gradientDestination), | |
605 getColorStopString(third, gradientDestination), | |
606 getColorStopString(fourth, 'transparent'), | |
607 'transparent' | |
608 ]; | |
609 | |
610 var gradient = '-webkit-linear-gradient(' + gradientArguments.join(',') + ')'; | |
611 maxiview.style.WebkitMaskImage = gradient; | |
612 } | |
613 | |
614 function getColorStopString(height, color) { | |
615 // TODO(arv): The CSS3 gradient syntax allows px units so we should simplify | |
616 // this to use pixels instead. | |
617 return color + ' ' + height * 100 + '%'; | |
618 } | |
619 | |
620 // Updates the visibility of the menu buttons for each section, based on | |
621 // whether they are currently enabled and in menu mode. | |
622 function updateMenuSections() { | |
623 var elms = document.getElementsByClassName('section'); | |
624 for (var i = 0, elm; elm = elms[i]; i++) { | |
625 var button = getSectionMenuButton(elm.id); | |
626 if (!button) | |
627 continue; | |
628 | |
629 if (!elm.classList.contains('disabled') && | |
630 elm.classList.contains('menu')) { | |
631 button.style.display = 'inline-block'; | |
632 } else { | |
633 button.style.display = 'none'; | |
634 } | |
635 } | |
636 } | |
637 | |
638 window.addEventListener('resize', handleWindowResize); | |
639 | |
640 var sectionToElementMap; | |
641 function getSectionElement(section) { | |
642 if (!sectionToElementMap) { | |
643 sectionToElementMap = {}; | |
644 for (var key in Section) { | |
645 sectionToElementMap[Section[key]] = | |
646 document.querySelector('.section[section=' + key + ']'); | |
647 } | |
648 } | |
649 return sectionToElementMap[section]; | |
650 } | |
651 | |
652 function getSectionMaxiview(section) { | |
653 return $(section.id + '-maxiview'); | |
654 } | |
655 | |
656 function getSectionMiniview(section) { | |
657 return section.querySelector('.miniview'); | |
658 } | |
659 | |
660 // You usually want to call |showOnlySection()| instead of this. | |
661 function showSection(section) { | |
662 if (!(section & shownSections)) { | |
663 shownSections |= section; | |
664 var el = getSectionElement(section); | |
665 if (el) { | |
666 el.classList.remove('collapsed'); | |
667 | |
668 var maxiview = getSectionMaxiview(el); | |
669 if (maxiview) { | |
670 maxiview.classList.remove('collapsing'); | |
671 maxiview.classList.remove('collapsed'); | |
672 // The opacity won't transition if you toggle the display property | |
673 // at the same time. To get a fade effect, we set the opacity | |
674 // asynchronously from another function, after the display is toggled. | |
675 // 1) 'collapsed' (display: none, opacity: 0) | |
676 // 2) none (display: block, opacity: 0) | |
677 // 3) 'opaque' (display: block, opacity: 1) | |
678 setTimeout(function () { | |
679 maxiview.classList.add('opaque'); | |
680 }, 0); | |
681 } | |
682 | |
683 var miniview = getSectionMiniview(el); | |
684 if (miniview) { | |
685 // The miniview is hidden immediately (no need to set this async). | |
686 miniview.classList.remove('opaque'); | |
687 } | |
688 } | |
689 | |
690 switch (section) { | |
691 case Section.THUMB: | |
692 mostVisited.visible = true; | |
693 mostVisited.layout(); | |
694 break; | |
695 case Section.APPS: | |
696 apps.visible = true; | |
697 apps.layout({disableAnimations:true}); | |
698 break; | |
699 } | |
700 } | |
701 } | |
702 | |
703 // Show this section and hide all other sections - at most one section can | |
704 // be open at one time. | |
705 function showOnlySection(section) { | |
706 for (var p in Section) { | |
707 if (p == section) | |
708 showSection(Section[p]); | |
709 else | |
710 hideSection(Section[p]); | |
711 } | |
712 } | |
713 | |
714 function hideSection(section) { | |
715 if (section & shownSections) { | |
716 shownSections &= ~section; | |
717 | |
718 switch (section) { | |
719 case Section.THUMB: | |
720 mostVisited.visible = false; | |
721 mostVisited.layout(); | |
722 break; | |
723 case Section.APPS: | |
724 apps.visible = false; | |
725 apps.layout(); | |
726 break; | |
727 } | |
728 | |
729 var el = getSectionElement(section); | |
730 if (el) { | |
731 el.classList.add('collapsed'); | |
732 | |
733 var maxiview = getSectionMaxiview(el); | |
734 if (maxiview) { | |
735 maxiview.classList.add((isDoneLoading() && isAnimating()) ? | |
736 'collapsing' : 'collapsed'); | |
737 maxiview.classList.remove('opaque'); | |
738 } | |
739 | |
740 var miniview = getSectionMiniview(el); | |
741 if (miniview) { | |
742 // We need to set this asynchronously to properly get the fade effect. | |
743 setTimeout(function() { | |
744 miniview.classList.add('opaque'); | |
745 }, 0); | |
746 updateMiniviewClipping(miniview); | |
747 } | |
748 } | |
749 } | |
750 } | |
751 | |
752 window.addEventListener('webkitTransitionEnd', function(e) { | |
753 if (e.target.classList.contains('collapsing')) { | |
754 e.target.classList.add('collapsed'); | |
755 e.target.classList.remove('collapsing'); | |
756 } | |
757 | |
758 if (e.target.classList.contains('maxiview') || | |
759 e.target.classList.contains('miniview')) { | |
760 document.documentElement.removeAttribute('enable-section-animations'); | |
761 showScrollBars(); | |
762 } | |
763 }); | |
764 | |
765 /** | |
766 * Callback when the shown sections changes in another NTP. | |
767 * @param {number} newShownSections Bitmask of the shown sections. | |
768 */ | |
769 function setShownSections(newShownSections) { | |
770 for (var key in Section) { | |
771 if (newShownSections & Section[key]) | |
772 showSection(Section[key]); | |
773 else | |
774 hideSection(Section[key]); | |
775 } | |
776 setSectionMenuMode('apps', Section.APPS, newShownSections & MENU_APPS, | |
777 MENU_APPS); | |
778 setSectionMenuMode('most-visited', Section.THUMB, | |
779 newShownSections & MENU_THUMB, MENU_THUMB); | |
780 setSectionMenuMode('recently-closed', undefined, | |
781 newShownSections & MENU_RECENT, MENU_RECENT); | |
782 layoutSections(); | |
783 } | |
784 | |
785 // Recently closed | |
786 | |
787 function layoutRecentlyClosed() { | |
788 var recentElement = $('recently-closed'); | |
789 var miniview = getSectionMiniview(recentElement); | |
790 | |
791 updateMiniviewClipping(miniview); | |
792 | |
793 if (miniview.hasChildNodes()) { | |
794 recentElement.classList.remove('disabled'); | |
795 miniview.classList.add('opaque'); | |
796 } else { | |
797 recentElement.classList.add('disabled'); | |
798 miniview.classList.remove('opaque'); | |
799 } | |
800 | |
801 layoutSections(); | |
802 } | |
803 | |
804 /** | |
805 * This function is called by the backend whenever the sync status section | |
806 * needs to be updated to reflect recent sync state changes. The backend passes | |
807 * the new status information in the newMessage parameter. The state includes | |
808 * the following: | |
809 * | |
810 * syncsectionisvisible: true if the sync section needs to show up on the new | |
811 * tab page and false otherwise. | |
812 * title: the header for the sync status section. | |
813 * msg: the actual message (e.g. "Synced to foo@gmail.com"). | |
814 * linkisvisible: true if the link element should be visible within the sync | |
815 * section and false otherwise. | |
816 * linktext: the text to display as the link in the sync status (only used if | |
817 * linkisvisible is true). | |
818 * linkurlisset: true if an URL should be set as the href for the link and false | |
819 * otherwise. If this field is false, then clicking on the link | |
820 * will result in sending a message to the backend (see | |
821 * 'SyncLinkClicked'). | |
822 * linkurl: the URL to use as the element's href (only used if linkurlisset is | |
823 * true). | |
824 */ | |
825 function syncMessageChanged(newMessage) { | |
826 var syncStatusElement = $('sync-status'); | |
827 | |
828 // Hide the section if the message is emtpy. | |
829 if (!newMessage['syncsectionisvisible']) { | |
830 syncStatusElement.classList.add('disabled'); | |
831 return; | |
832 } | |
833 | |
834 syncStatusElement.classList.remove('disabled'); | |
835 | |
836 var content = syncStatusElement.children[0]; | |
837 | |
838 // Set the sync section background color based on the state. | |
839 if (newMessage.msgtype == 'error') { | |
840 content.style.backgroundColor = 'tomato'; | |
841 } else { | |
842 content.style.backgroundColor = ''; | |
843 } | |
844 | |
845 // Set the text for the header and sync message. | |
846 var titleElement = content.firstElementChild; | |
847 titleElement.textContent = newMessage.title; | |
848 var messageElement = titleElement.nextElementSibling; | |
849 messageElement.textContent = newMessage.msg; | |
850 | |
851 // Remove what comes after the message | |
852 while (messageElement.nextSibling) { | |
853 content.removeChild(messageElement.nextSibling); | |
854 } | |
855 | |
856 if (newMessage.linkisvisible) { | |
857 var el; | |
858 if (newMessage.linkurlisset) { | |
859 // Use a link | |
860 el = document.createElement('a'); | |
861 el.href = newMessage.linkurl; | |
862 } else { | |
863 el = document.createElement('button'); | |
864 el.className = 'link'; | |
865 el.addEventListener('click', syncSectionLinkClicked); | |
866 } | |
867 el.textContent = newMessage.linktext; | |
868 content.appendChild(el); | |
869 fixLinkUnderline(el); | |
870 } | |
871 | |
872 layoutSections(); | |
873 } | |
874 | |
875 /** | |
876 * Invoked when the link in the sync promo or sync status section is clicked. | |
877 */ | |
878 function syncSectionLinkClicked(e) { | |
879 chrome.send('SyncLinkClicked'); | |
880 e.preventDefault(); | |
881 } | |
882 | |
883 /** | |
884 * Invoked when link to start sync in the promo message is clicked, and Chrome | |
885 * has already been synced to an account. | |
886 */ | |
887 function syncAlreadyEnabled(message) { | |
888 showNotification(message.syncEnabledMessage); | |
889 } | |
890 | |
891 /** | |
892 * Returns the text used for a recently closed window. | |
893 * @param {number} numTabs Number of tabs in the window. | |
894 * @return {string} The text to use. | |
895 */ | |
896 function formatTabsText(numTabs) { | |
897 if (numTabs == 1) | |
898 return localStrings.getString('closedwindowsingle'); | |
899 return localStrings.getStringF('closedwindowmultiple', numTabs); | |
900 } | |
901 | |
902 // Theme related | |
903 | |
904 function themeChanged(hasAttribution) { | |
905 document.documentElement.setAttribute('hasattribution', hasAttribution); | |
906 $('themecss').href = 'chrome://theme/css/newtab.css?' + Date.now(); | |
907 updateAttribution(); | |
908 } | |
909 | |
910 function updateAttribution() { | |
911 // Default value for standard NTP with no theme attribution or custom logo. | |
912 logEvent('updateAttribution called'); | |
913 var imageId = 'IDR_PRODUCT_LOGO'; | |
914 // Theme attribution always overrides custom logos. | |
915 if (document.documentElement.getAttribute('hasattribution') == 'true') { | |
916 logEvent('updateAttribution called with THEME ATTR'); | |
917 imageId = 'IDR_THEME_NTP_ATTRIBUTION'; | |
918 } else if (document.documentElement.getAttribute('customlogo') == 'true') { | |
919 logEvent('updateAttribution with CUSTOMLOGO'); | |
920 imageId = 'IDR_CUSTOM_PRODUCT_LOGO'; | |
921 } | |
922 | |
923 $('attribution-img').src = 'chrome://theme/' + imageId + '?' + Date.now(); | |
924 } | |
925 | |
926 // If the content overlaps with the attribution, we bump its opacity down. | |
927 function updateAttributionDisplay(contentBottom) { | |
928 var attribution = $('attribution'); | |
929 var main = $('main'); | |
930 var rtl = document.documentElement.dir == 'rtl'; | |
931 var contentRect = main.getBoundingClientRect(); | |
932 var attributionRect = attribution.getBoundingClientRect(); | |
933 | |
934 // Hack. See comments for '.haslayout' in new_tab.css. | |
935 if (attributionRect.width == 0) | |
936 return; | |
937 else | |
938 attribution.classList.remove('nolayout'); | |
939 | |
940 if (contentBottom > attribution.offsetTop) { | |
941 if ((!rtl && contentRect.right > attributionRect.left) || | |
942 (rtl && attributionRect.right > contentRect.left)) { | |
943 attribution.classList.add('obscured'); | |
944 return; | |
945 } | |
946 } | |
947 | |
948 attribution.classList.remove('obscured'); | |
949 } | |
950 | |
951 function bookmarkBarAttached() { | |
952 document.documentElement.setAttribute('bookmarkbarattached', 'true'); | |
953 } | |
954 | |
955 function bookmarkBarDetached() { | |
956 document.documentElement.setAttribute('bookmarkbarattached', 'false'); | |
957 } | |
958 | |
959 function viewLog() { | |
960 var lines = []; | |
961 var start = log[0][1]; | |
962 | |
963 for (var i = 0; i < log.length; i++) { | |
964 lines.push((log[i][1] - start) + ': ' + log[i][0]); | |
965 } | |
966 | |
967 console.log(lines.join('\n')); | |
968 } | |
969 | |
970 // We apply the size class here so that we don't trigger layout animations | |
971 // onload. | |
972 | |
973 handleWindowResize(); | |
974 | |
975 var localStrings = new LocalStrings(); | |
976 | |
977 /////////////////////////////////////////////////////////////////////////////// | |
978 // Things we know are not needed at startup go below here | |
979 | |
980 function afterTransition(f) { | |
981 if (!isDoneLoading()) { | |
982 // Make sure we do not use a timer during load since it slows down the UI. | |
983 f(); | |
984 } else { | |
985 // The duration of all transitions are .15s | |
986 window.setTimeout(f, 150); | |
987 } | |
988 } | |
989 | |
990 // Notification | |
991 | |
992 | |
993 var notificationTimeout; | |
994 | |
995 /* | |
996 * Displays a message (either a string or a document fragment) in the | |
997 * notification slot at the top of the NTP. A close button ("x") will be | |
998 * inserted at the end of the message. | |
999 * @param {string|Node} message String or node to use as message. | |
1000 * @param {string} actionText The text to show as a link next to the message. | |
1001 * @param {function=} opt_f Function to call when the user clicks the action | |
1002 * link. | |
1003 * @param {number=} opt_delay The time in milliseconds before hiding the | |
1004 * notification. | |
1005 */ | |
1006 function showNotification(message, actionText, opt_f, opt_delay) { | |
1007 // TODO(arv): Create a notification component. | |
1008 var notificationElement = $('notification'); | |
1009 var f = opt_f || function() {}; | |
1010 var delay = opt_delay || 10000; | |
1011 | |
1012 function show() { | |
1013 window.clearTimeout(notificationTimeout); | |
1014 notificationElement.classList.add('show'); | |
1015 document.body.classList.add('notification-shown'); | |
1016 } | |
1017 | |
1018 function delayedHide() { | |
1019 notificationTimeout = window.setTimeout(hideNotification, delay); | |
1020 } | |
1021 | |
1022 function doAction() { | |
1023 f(); | |
1024 closeNotification(); | |
1025 } | |
1026 | |
1027 function closeNotification() { | |
1028 if (notification.classList.contains('promo')) | |
1029 chrome.send('closePromo'); | |
1030 hideNotification(); | |
1031 } | |
1032 | |
1033 // Remove classList entries from previous notifications. | |
1034 notification.classList.remove('first-run'); | |
1035 notification.classList.remove('promo'); | |
1036 | |
1037 var messageContainer = notificationElement.firstElementChild; | |
1038 var actionLink = notificationElement.querySelector('#action-link'); | |
1039 var closeButton = notificationElement.querySelector('#notification-close'); | |
1040 | |
1041 // Remove any previous actionLink entry. | |
1042 actionLink.textContent = ''; | |
1043 | |
1044 $('notification-close').onclick = closeNotification; | |
1045 | |
1046 if (typeof message == 'string') { | |
1047 messageContainer.textContent = message; | |
1048 } else { | |
1049 messageContainer.textContent = ''; // Remove all children. | |
1050 messageContainer.appendChild(message); | |
1051 } | |
1052 | |
1053 if (actionText) { | |
1054 actionLink.style.display = ''; | |
1055 actionLink.textContent = actionText; | |
1056 } else { | |
1057 actionLink.style.display = 'none'; | |
1058 } | |
1059 | |
1060 actionLink.onclick = doAction; | |
1061 actionLink.onkeydown = handleIfEnterKey(doAction); | |
1062 notificationElement.onmouseover = show; | |
1063 notificationElement.onmouseout = delayedHide; | |
1064 actionLink.onfocus = show; | |
1065 actionLink.onblur = delayedHide; | |
1066 // Enable tabbing to the link now that it is shown. | |
1067 actionLink.tabIndex = 0; | |
1068 | |
1069 show(); | |
1070 delayedHide(); | |
1071 } | |
1072 | |
1073 /** | |
1074 * Hides the notifier. | |
1075 */ | |
1076 function hideNotification() { | |
1077 var notificationElement = $('notification'); | |
1078 notificationElement.classList.remove('show'); | |
1079 document.body.classList.remove('notification-shown'); | |
1080 var actionLink = notificationElement.querySelector('#actionlink'); | |
1081 var closeButton = notificationElement.querySelector('#notification-close'); | |
1082 // Prevent tabbing to the hidden link. | |
1083 // Setting tabIndex to -1 only prevents future tabbing to it. If, however, the | |
1084 // user switches window or a tab and then moves back to this tab the element | |
1085 // may gain focus. We therefore make sure that we blur the element so that the | |
1086 // element focus is not restored when coming back to this window. | |
1087 if (actionLink) { | |
1088 actionLink.tabIndex = -1; | |
1089 actionLink.blur(); | |
1090 } | |
1091 if (closeButton) { | |
1092 closeButton.tabIndex = -1; | |
1093 closeButton.blur(); | |
1094 } | |
1095 } | |
1096 | |
1097 function showPromoNotification() { | |
1098 showNotification(parseHtmlSubset(localStrings.getString('serverpromo')), | |
1099 localStrings.getString('syncpromotext'), | |
1100 function () { chrome.send('SyncLinkClicked'); }, | |
1101 60000); | |
1102 var notificationElement = $('notification'); | |
1103 notification.classList.add('promo'); | |
1104 } | |
1105 | |
1106 $('main').addEventListener('click', function(e) { | |
1107 var p = e.target; | |
1108 while (p && p.tagName != 'H2') { | |
1109 // In case the user clicks on a button we do not want to expand/collapse a | |
1110 // section. | |
1111 if (p.tagName == 'BUTTON') | |
1112 return; | |
1113 p = p.parentNode; | |
1114 } | |
1115 | |
1116 if (!p) | |
1117 return; | |
1118 | |
1119 p = p.parentNode; | |
1120 if (!getSectionMaxiview(p)) | |
1121 return; | |
1122 | |
1123 toggleSectionVisibilityAndAnimate(p.getAttribute('section')); | |
1124 }); | |
1125 | |
1126 $('most-visited-settings').addEventListener('click', function() { | |
1127 $('clear-all-blacklisted').execute(); | |
1128 }); | |
1129 | |
1130 function toggleSectionVisibilityAndAnimate(section) { | |
1131 if (!section) | |
1132 return; | |
1133 | |
1134 // It looks better to return the scroll to the top when toggling sections. | |
1135 document.body.scrollTop = 0; | |
1136 | |
1137 // We set it back in webkitTransitionEnd. | |
1138 document.documentElement.setAttribute('enable-section-animations', 'true'); | |
1139 if (shownSections & Section[section]) { | |
1140 hideSection(Section[section]); | |
1141 } else { | |
1142 showOnlySection(section); | |
1143 } | |
1144 layoutSections(); | |
1145 saveShownSections(); | |
1146 } | |
1147 | |
1148 function handleIfEnterKey(f) { | |
1149 return function(e) { | |
1150 if (e.keyIdentifier == 'Enter') | |
1151 f(e); | |
1152 }; | |
1153 } | |
1154 | |
1155 function maybeReopenTab(e) { | |
1156 var el = findAncestor(e.target, function(el) { | |
1157 return el.sessionId !== undefined; | |
1158 }); | |
1159 if (el) { | |
1160 chrome.send('reopenTab', [String(el.sessionId)]); | |
1161 e.preventDefault(); | |
1162 | |
1163 setWindowTooltipTimeout(); | |
1164 } | |
1165 } | |
1166 | |
1167 // Note that the openForeignSession calls can fail, resulting this method to | |
1168 // not have any action (hence the maybe). | |
1169 function maybeOpenForeignSession(e) { | |
1170 var el = findAncestor(e.target, function(el) { | |
1171 return el.sessionTag !== undefined; | |
1172 }); | |
1173 if (el) { | |
1174 chrome.send('openForeignSession', [String(el.sessionTag)]); | |
1175 e.stopPropagation(); | |
1176 e.preventDefault(); | |
1177 setWindowTooltipTimeout(); | |
1178 } | |
1179 } | |
1180 | |
1181 function maybeOpenForeignWindow(e) { | |
1182 var el = findAncestor(e.target, function(el) { | |
1183 return el.winNum !== undefined; | |
1184 }); | |
1185 if (el) { | |
1186 chrome.send('openForeignSession', [String(el.sessionTag), | |
1187 String(el.winNum)]); | |
1188 e.stopPropagation(); | |
1189 e.preventDefault(); | |
1190 setWindowTooltipTimeout(); | |
1191 } | |
1192 } | |
1193 | |
1194 function maybeOpenForeignTab(e) { | |
1195 var el = findAncestor(e.target, function(el) { | |
1196 return el.sessionId !== undefined; | |
1197 }); | |
1198 if (el) { | |
1199 chrome.send('openForeignSession', [String(el.sessionTag), String(el.winNum), | |
1200 String(el.sessionId)]); | |
1201 e.stopPropagation(); | |
1202 e.preventDefault(); | |
1203 setWindowTooltipTimeout(); | |
1204 } | |
1205 } | |
1206 | |
1207 // HACK(arv): After the window onblur event happens we get a mouseover event | |
1208 // on the next item and we want to make sure that we do not show a tooltip | |
1209 // for that. | |
1210 function setWindowTooltipTimeout(e) { | |
1211 window.setTimeout(function() { | |
1212 windowTooltip.hide(); | |
1213 }, 2 * WindowTooltip.DELAY); | |
1214 } | |
1215 | |
1216 function maybeShowWindowTooltip(e) { | |
1217 var f = function(el) { | |
1218 return el.tabItems !== undefined; | |
1219 }; | |
1220 var el = findAncestor(e.target, f); | |
1221 var relatedEl = findAncestor(e.relatedTarget, f); | |
1222 if (el && el != relatedEl) { | |
1223 windowTooltip.handleMouseOver(e, el, el.tabItems); | |
1224 } | |
1225 } | |
1226 | |
1227 | |
1228 var recentlyClosedElement = $('recently-closed'); | |
1229 | |
1230 recentlyClosedElement.addEventListener('click', maybeReopenTab); | |
1231 recentlyClosedElement.addEventListener('keydown', | |
1232 handleIfEnterKey(maybeReopenTab)); | |
1233 | |
1234 recentlyClosedElement.addEventListener('mouseover', maybeShowWindowTooltip); | |
1235 recentlyClosedElement.addEventListener('focus', maybeShowWindowTooltip, true); | |
1236 | |
1237 var foreignSessionElement = $('foreign-sessions'); | |
1238 | |
1239 foreignSessionElement.addEventListener('click', maybeOpenForeignSession); | |
1240 foreignSessionElement.addEventListener('keydown', | |
1241 handleIfEnterKey( | |
1242 maybeOpenForeignSession)); | |
1243 | |
1244 foreignSessionElement.addEventListener('mouseover', maybeShowWindowTooltip); | |
1245 foreignSessionElement.addEventListener('focus', maybeShowWindowTooltip, true); | |
1246 | |
1247 /** | |
1248 * This object represents a tooltip representing a closed window. It is | |
1249 * shown when hovering over a closed window item or when the item is focused. It | |
1250 * gets hidden when blurred or when mousing out of the menu or the item. | |
1251 * @param {Element} tooltipEl The element to use as the tooltip. | |
1252 * @constructor | |
1253 */ | |
1254 function WindowTooltip(tooltipEl) { | |
1255 this.tooltipEl = tooltipEl; | |
1256 this.boundHide_ = this.hide.bind(this); | |
1257 this.boundHandleMouseOut_ = this.handleMouseOut.bind(this); | |
1258 } | |
1259 | |
1260 WindowTooltip.trackMouseMove_ = function(e) { | |
1261 WindowTooltip.clientX = e.clientX; | |
1262 WindowTooltip.clientY = e.clientY; | |
1263 }; | |
1264 | |
1265 /** | |
1266 * Time in ms to delay before the tooltip is shown. | |
1267 * @type {number} | |
1268 */ | |
1269 WindowTooltip.DELAY = 300; | |
1270 | |
1271 WindowTooltip.prototype = { | |
1272 timer: 0, | |
1273 handleMouseOver: function(e, linkEl, tabs) { | |
1274 this.linkEl_ = linkEl; | |
1275 if (e.type == 'mouseover') { | |
1276 this.linkEl_.addEventListener('mousemove', WindowTooltip.trackMouseMove_); | |
1277 this.linkEl_.addEventListener('mouseout', this.boundHandleMouseOut_); | |
1278 } else { // focus | |
1279 this.linkEl_.addEventListener('blur', this.boundHide_); | |
1280 } | |
1281 this.timer = window.setTimeout(this.show.bind(this, e.type, linkEl, tabs), | |
1282 WindowTooltip.DELAY); | |
1283 }, | |
1284 show: function(type, linkEl, tabs) { | |
1285 window.addEventListener('blur', this.boundHide_); | |
1286 this.linkEl_.removeEventListener('mousemove', | |
1287 WindowTooltip.trackMouseMove_); | |
1288 window.clearTimeout(this.timer); | |
1289 | |
1290 this.renderItems(tabs); | |
1291 var rect = linkEl.getBoundingClientRect(); | |
1292 var bodyRect = document.body.getBoundingClientRect(); | |
1293 var rtl = document.documentElement.dir == 'rtl'; | |
1294 | |
1295 this.tooltipEl.style.display = 'block'; | |
1296 var tooltipRect = this.tooltipEl.getBoundingClientRect(); | |
1297 var x, y; | |
1298 | |
1299 // When focused show below, like a drop down menu. | |
1300 if (type == 'focus') { | |
1301 x = rtl ? | |
1302 rect.left + bodyRect.left + rect.width - this.tooltipEl.offsetWidth : | |
1303 rect.left + bodyRect.left; | |
1304 y = rect.top + bodyRect.top + rect.height; | |
1305 } else { | |
1306 x = bodyRect.left + (rtl ? | |
1307 WindowTooltip.clientX - this.tooltipEl.offsetWidth : | |
1308 WindowTooltip.clientX); | |
1309 // Offset like a tooltip | |
1310 y = 20 + WindowTooltip.clientY + bodyRect.top; | |
1311 } | |
1312 | |
1313 // We need to ensure that the tooltip is inside the window viewport. | |
1314 x = Math.min(x, bodyRect.width - tooltipRect.width); | |
1315 x = Math.max(x, 0); | |
1316 y = Math.min(y, bodyRect.height - tooltipRect.height); | |
1317 y = Math.max(y, 0); | |
1318 | |
1319 this.tooltipEl.style.left = x + 'px'; | |
1320 this.tooltipEl.style.top = y + 'px'; | |
1321 }, | |
1322 handleMouseOut: function(e) { | |
1323 // Don't hide when move to another item in the link. | |
1324 var f = function(el) { | |
1325 return el.tabItems !== undefined; | |
1326 }; | |
1327 var el = findAncestor(e.target, f); | |
1328 var relatedEl = findAncestor(e.relatedTarget, f); | |
1329 if (el && el != relatedEl) { | |
1330 this.hide(); | |
1331 } | |
1332 }, | |
1333 hide: function() { | |
1334 window.clearTimeout(this.timer); | |
1335 window.removeEventListener('blur', this.boundHide_); | |
1336 this.linkEl_.removeEventListener('mousemove', | |
1337 WindowTooltip.trackMouseMove_); | |
1338 this.linkEl_.removeEventListener('mouseout', this.boundHandleMouseOut_); | |
1339 this.linkEl_.removeEventListener('blur', this.boundHide_); | |
1340 this.linkEl_ = null; | |
1341 | |
1342 this.tooltipEl.style.display = 'none'; | |
1343 }, | |
1344 renderItems: function(tabs) { | |
1345 var tooltip = this.tooltipEl; | |
1346 tooltip.textContent = ''; | |
1347 | |
1348 tabs.forEach(function(tab) { | |
1349 var span = document.createElement('span'); | |
1350 span.className = 'item'; | |
1351 span.style.backgroundImage = url('chrome://favicon/' + tab.url); | |
1352 span.dir = tab.direction; | |
1353 span.textContent = tab.title; | |
1354 tooltip.appendChild(span); | |
1355 }); | |
1356 } | |
1357 }; | |
1358 | |
1359 var windowTooltip = new WindowTooltip($('window-tooltip')); | |
1360 | |
1361 window.addEventListener('load', | |
1362 logEvent.bind(global, 'Tab.NewTabOnload', true)); | |
1363 | |
1364 window.addEventListener('resize', handleWindowResize); | |
1365 document.addEventListener('DOMContentLoaded', | |
1366 logEvent.bind(global, 'Tab.NewTabDOMContentLoaded', true)); | |
1367 | |
1368 // Whether or not we should send the initial 'GetSyncMessage' to the backend | |
1369 // depends on the value of the attribue 'syncispresent' which the backend sets | |
1370 // to indicate if there is code in the backend which is capable of processing | |
1371 // this message. This attribute is loaded by the JSTemplate and therefore we | |
1372 // must make sure we check the attribute after the DOM is loaded. | |
1373 document.addEventListener('DOMContentLoaded', | |
1374 callGetSyncMessageIfSyncIsPresent); | |
1375 | |
1376 /** | |
1377 * The sync code is not yet built by default on all platforms so we have to | |
1378 * make sure we don't send the initial sync message to the backend unless the | |
1379 * backend told us that the sync code is present. | |
1380 */ | |
1381 function callGetSyncMessageIfSyncIsPresent() { | |
1382 if (document.documentElement.getAttribute('syncispresent') == 'true') { | |
1383 chrome.send('GetSyncMessage'); | |
1384 } | |
1385 } | |
1386 | |
1387 // Tooltip for elements that have text that overflows. | |
1388 document.addEventListener('mouseover', function(e) { | |
1389 // We don't want to do this while we are dragging because it makes things very | |
1390 // janky | |
1391 if (mostVisited.isDragging()) { | |
1392 return; | |
1393 } | |
1394 | |
1395 var el = findAncestor(e.target, function(el) { | |
1396 return el.xtitle; | |
1397 }); | |
1398 if (el && el.xtitle != el.title) { | |
1399 if (el.scrollWidth > el.clientWidth) { | |
1400 el.title = el.xtitle; | |
1401 } else { | |
1402 el.title = ''; | |
1403 } | |
1404 } | |
1405 }); | |
1406 | |
1407 /** | |
1408 * Makes links and buttons support a different underline color. | |
1409 * @param {Node} node The node to search for links and buttons in. | |
1410 */ | |
1411 function fixLinkUnderlines(node) { | |
1412 var elements = node.querySelectorAll('a,button'); | |
1413 Array.prototype.forEach.call(elements, fixLinkUnderline); | |
1414 } | |
1415 | |
1416 /** | |
1417 * Wraps the content of an element in a a link-color span. | |
1418 * @param {Element} el The element to wrap. | |
1419 */ | |
1420 function fixLinkUnderline(el) { | |
1421 var span = document.createElement('span'); | |
1422 span.className = 'link-color'; | |
1423 while (el.hasChildNodes()) { | |
1424 span.appendChild(el.firstChild); | |
1425 } | |
1426 el.appendChild(span); | |
1427 } | |
1428 | |
1429 updateAttribution(); | |
1430 | |
1431 function initializeLogin() { | |
1432 chrome.send('initializeLogin', []); | |
1433 } | |
1434 | |
1435 function updateLogin(login) { | |
1436 $('login-container').style.display = login ? 'block' : ''; | |
1437 if (login) | |
1438 $('login-username').textContent = login; | |
1439 } | |
1440 | |
1441 var mostVisited = new MostVisited( | |
1442 $('most-visited-maxiview'), | |
1443 document.querySelector('#most-visited .miniview'), | |
1444 $('most-visited-menu'), | |
1445 useSmallGrid(), | |
1446 shownSections & Section.THUMB); | |
1447 | |
1448 function setMostVisitedPages(data, hasBlacklistedUrls) { | |
1449 logEvent('received most visited pages'); | |
1450 | |
1451 mostVisited.updateSettingsLink(hasBlacklistedUrls); | |
1452 mostVisited.data = data; | |
1453 mostVisited.layout(); | |
1454 layoutSections(); | |
1455 | |
1456 // Remove class name in a timeout so that changes done in this JS thread are | |
1457 // not animated. | |
1458 window.setTimeout(function() { | |
1459 mostVisited.ensureSmallGridCorrect(); | |
1460 maybeDoneLoading(); | |
1461 }, 1); | |
1462 | |
1463 if (localStrings.getString('serverpromo')) { | |
1464 showPromoNotification(); | |
1465 } | |
1466 | |
1467 } | |
1468 | |
1469 function maybeDoneLoading() { | |
1470 if (mostVisited.data && apps.loaded) | |
1471 document.body.classList.remove('loading'); | |
1472 } | |
1473 | |
1474 function isDoneLoading() { | |
1475 return !document.body.classList.contains('loading'); | |
1476 } | |
1477 | |
1478 document.addEventListener('DOMContentLoaded', function() { | |
1479 cr.enablePlatformSpecificCSSRules(); | |
1480 | |
1481 // Initialize the listener for the "hide this" link on the apps promo. We do | |
1482 // this outside of getAppsCallback because it only needs to be done once per | |
1483 // NTP load. | |
1484 $('apps-promo-hide').addEventListener('click', function() { | |
1485 chrome.send('hideAppsPromo', []); | |
1486 document.documentElement.classList.remove('apps-promo-visible'); | |
1487 layoutSections(); | |
1488 }); | |
1489 }); | |
OLD | NEW |