OLD | NEW |
| 1 // Copyright (c) 2010 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. |
1 | 4 |
2 // Helpers | |
3 | |
4 function findAncestorByClass(el, className) { | |
5 return findAncestor(el, function(el) { | |
6 return hasClass(el, className); | |
7 }); | |
8 } | |
9 | |
10 /** | |
11 * Return the first ancestor for which the {@code predicate} returns true. | |
12 * @param {Node} node The node to check. | |
13 * @param {function(Node) : boolean} predicate The function that tests the | |
14 * nodes. | |
15 * @return {Node} The found ancestor or null if not found. | |
16 */ | |
17 function findAncestor(node, predicate) { | |
18 var last = false; | |
19 while (node != null && !(last = predicate(node))) { | |
20 node = node.parentNode; | |
21 } | |
22 return last ? node : null; | |
23 } | |
24 | |
25 // WebKit does not have Node.prototype.swapNode | |
26 // https://bugs.webkit.org/show_bug.cgi?id=26525 | |
27 function swapDomNodes(a, b) { | |
28 var afterA = a.nextSibling; | |
29 if (afterA == b) { | |
30 swapDomNodes(b, a); | |
31 return; | |
32 } | |
33 var aParent = a.parentNode; | |
34 b.parentNode.replaceChild(a, b); | |
35 aParent.insertBefore(b, afterA); | |
36 } | |
37 | |
38 function bind(fn, selfObj, var_args) { | |
39 var boundArgs = Array.prototype.slice.call(arguments, 2); | |
40 return function() { | |
41 var args = Array.prototype.slice.call(arguments); | |
42 args.unshift.apply(args, boundArgs); | |
43 return fn.apply(selfObj, args); | |
44 } | |
45 } | |
46 | |
47 const IS_MAC = /$Mac/.test(navigator.platform); | |
48 | |
49 var loading = true; | |
50 var mostVisitedData = []; | 5 var mostVisitedData = []; |
51 var gotMostVisited = false; | 6 var gotMostVisited = false; |
52 | 7 |
53 function mostVisitedPages(data, firstRun) { | 8 function mostVisitedPages(data, firstRun) { |
54 logEvent('received most visited pages'); | 9 logEvent('received most visited pages'); |
55 | 10 |
56 // We append the class name with the "filler" so that we can style fillers | 11 // We append the class name with the "filler" so that we can style fillers |
57 // differently. | 12 // differently. |
58 var maxItems = 8; | 13 var maxItems = 8; |
59 data.length = Math.min(maxItems, data.length); | 14 data.length = Math.min(maxItems, data.length); |
60 var len = data.length; | 15 var len = data.length; |
61 for (var i = len; i < maxItems; i++) { | 16 for (var i = len; i < maxItems; i++) { |
62 data[i] = {filler: true}; | 17 data[i] = {filler: true}; |
63 } | 18 } |
64 | 19 |
65 mostVisitedData = data; | 20 mostVisitedData = data; |
66 renderMostVisited(data); | 21 renderMostVisited(data); |
67 | 22 |
68 gotMostVisited = true; | 23 gotMostVisited = true; |
69 onDataLoaded(); | 24 onDataLoaded(); |
70 | 25 |
71 // Only show the first run notification if first run. | 26 // Only show the first run notification if first run. |
72 if (firstRun) { | 27 if (firstRun) { |
73 showFirstRunNotification(); | 28 showFirstRunNotification(); |
74 } | 29 } |
75 } | 30 } |
76 | |
77 function getAppsCallback(data) { | |
78 var appsSection = $('apps-section'); | |
79 appsSection.innerHTML = ''; | |
80 appsSection.style.display = data.length ? 'block' : ''; | |
81 | |
82 data.forEach(function(app) { | |
83 appsSection.appendChild(apps.createElement(app)); | |
84 }); | |
85 } | |
86 | |
87 var apps = { | |
88 /** | |
89 * @this {!HTMLAnchorElement} | |
90 */ | |
91 handleClick_: function() { | |
92 chrome.send('launchApp', [this.id]); | |
93 return false; | |
94 }, | |
95 | |
96 createElement: function(app) { | |
97 var a = document.createElement('a'); | |
98 a.xtitle = a.textContent = app['name']; | |
99 a.href = app['launch_url']; | |
100 a.id = app['id']; | |
101 a.onclick = apps.handleClick_; | |
102 a.style.backgroundImage = url(app['icon']); | |
103 return a; | |
104 } | |
105 }; | |
106 | |
107 var tipCache = {}; | |
108 | |
109 function tips(data) { | |
110 logEvent('received tips'); | |
111 tipCache = data; | |
112 renderTip(); | |
113 } | |
114 | |
115 function createTip(data) { | |
116 if (data.length) { | |
117 if (data[0].set_homepage_tip) { | |
118 var homepageButton = document.createElement('button'); | |
119 homepageButton.className = 'link'; | |
120 homepageButton.textContent = data[0].set_homepage_tip; | |
121 homepageButton.addEventListener('click', setAsHomePageLinkClicked); | |
122 return homepageButton; | |
123 } else { | |
124 try { | |
125 return parseHtmlSubset(data[0].tip_html_text); | |
126 } catch (parseErr) { | |
127 console.error('Error parsing tips: ' + parseErr.message); | |
128 } | |
129 } | |
130 } | |
131 // Return an empty DF in case of failure. | |
132 return document.createDocumentFragment(); | |
133 } | |
134 | |
135 function clearTipLine() { | |
136 var tipElement = $('tip-line'); | |
137 // There should always be only one tip. | |
138 tipElement.textContent = ''; | |
139 tipElement.removeEventListener('click', setAsHomePageLinkClicked); | |
140 } | |
141 | |
142 function renderTip() { | |
143 clearTipLine(); | |
144 var tipElement = $('tip-line'); | |
145 tipElement.appendChild(createTip(tipCache)); | |
146 fixLinkUnderlines(tipElement); | |
147 } | |
148 | |
149 function recentlyClosedTabs(data) { | |
150 logEvent('received recently closed tabs'); | |
151 // We need to store the recent items so we can update the layout on a resize. | |
152 recentItems = data; | |
153 renderRecentlyClosed(); | |
154 } | |
155 | |
156 var recentItems = []; | |
157 | |
158 function renderRecentlyClosed() { | |
159 // We remove all items but the header and the nav | |
160 var recentlyClosedElement = $('recently-closed'); | |
161 var headerEl = recentlyClosedElement.firstElementChild; | |
162 var navEl = recentlyClosedElement.lastElementChild.lastElementChild; | |
163 var parentEl = navEl.parentNode; | |
164 | |
165 for (var el = navEl.previousElementSibling; el; | |
166 el = navEl.previousElementSibling) { | |
167 parentEl.removeChild(el); | |
168 } | |
169 | |
170 // Create new items | |
171 recentItems.forEach(function(item) { | |
172 var el = createRecentItem(item); | |
173 parentEl.insertBefore(el, navEl); | |
174 }); | |
175 | |
176 layoutRecentlyClosed(); | |
177 } | |
178 | |
179 function createRecentItem(data) { | |
180 var isWindow = data.type == 'window'; | |
181 var el; | |
182 if (isWindow) { | |
183 el = document.createElement('span'); | |
184 el.className = 'item link window'; | |
185 el.tabItems = data.tabs; | |
186 el.tabIndex = 0; | |
187 el.textContent = formatTabsText(data.tabs.length); | |
188 } else { | |
189 el = document.createElement('a'); | |
190 el.className = 'item'; | |
191 el.href = data.url; | |
192 el.style.backgroundImage = url('chrome://favicon/' + data.url); | |
193 el.dir = data.direction; | |
194 el.textContent = data.title; | |
195 } | |
196 el.sessionId = data.sessionId; | |
197 el.xtitle = data.title; | |
198 var wrapperEl = document.createElement('span'); | |
199 wrapperEl.appendChild(el); | |
200 return wrapperEl; | |
201 } | |
202 | |
203 function onShownSections(mask) { | |
204 logEvent('received shown sections'); | |
205 if (mask != shownSections) { | |
206 var oldShownSections = shownSections; | |
207 shownSections = mask; | |
208 | |
209 // Only invalidate most visited if needed. | |
210 if ((mask & Section.THUMB) != (oldShownSections & Section.THUMB)) { | |
211 mostVisited.invalidate(); | |
212 } | |
213 | |
214 mostVisited.updateDisplayMode(); | |
215 renderRecentlyClosed(); | |
216 } | |
217 } | |
218 | |
219 function saveShownSections() { | |
220 chrome.send('setShownSections', [String(shownSections)]); | |
221 } | |
222 | |
223 function getThumbnailClassName(data) { | 31 function getThumbnailClassName(data) { |
224 return 'thumbnail-container' + | 32 return 'thumbnail-container' + |
225 (data.pinned ? ' pinned' : '') + | 33 (data.pinned ? ' pinned' : '') + |
226 (data.filler ? ' filler' : ''); | 34 (data.filler ? ' filler' : ''); |
227 } | 35 } |
228 | 36 |
229 function url(s) { | |
230 // http://www.w3.org/TR/css3-values/#uris | |
231 // Parentheses, commas, whitespace characters, single quotes (') and double | |
232 // quotes (") appearing in a URI must be escaped with a backslash | |
233 var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1'); | |
234 // WebKit has a bug when it comes to URLs that end with \ | |
235 // https://bugs.webkit.org/show_bug.cgi?id=28885 | |
236 if (/\\\\$/.test(s2)) { | |
237 // Add a space to work around the WebKit bug. | |
238 s2 += ' '; | |
239 } | |
240 return 'url("' + s2 + '")'; | |
241 } | |
242 | |
243 function renderMostVisited(data) { | 37 function renderMostVisited(data) { |
244 var parent = $('most-visited'); | 38 var parent = $('most-visited'); |
245 var children = parent.children; | 39 var children = parent.children; |
246 for (var i = 0; i < data.length; i++) { | 40 for (var i = 0; i < data.length; i++) { |
247 var d = data[i]; | 41 var d = data[i]; |
248 var t = children[i]; | 42 var t = children[i]; |
249 | 43 |
250 // If we have a filler continue | 44 // If we have a filler continue |
251 var oldClassName = t.className; | 45 var oldClassName = t.className; |
252 var newClassName = getThumbnailClassName(d); | 46 var newClassName = getThumbnailClassName(d); |
(...skipping 24 matching lines...) Expand all Loading... |
277 t.querySelector('.thumbnail-wrapper').style.backgroundImage = | 71 t.querySelector('.thumbnail-wrapper').style.backgroundImage = |
278 url(thumbnailUrl); | 72 url(thumbnailUrl); |
279 var titleDiv = t.querySelector('.title > div'); | 73 var titleDiv = t.querySelector('.title > div'); |
280 titleDiv.xtitle = titleDiv.textContent = d.title; | 74 titleDiv.xtitle = titleDiv.textContent = d.title; |
281 var faviconUrl = d.faviconUrl || 'chrome://favicon/' + d.url; | 75 var faviconUrl = d.faviconUrl || 'chrome://favicon/' + d.url; |
282 titleDiv.style.backgroundImage = url(faviconUrl); | 76 titleDiv.style.backgroundImage = url(faviconUrl); |
283 titleDiv.dir = d.direction; | 77 titleDiv.dir = d.direction; |
284 } | 78 } |
285 } | 79 } |
286 | 80 |
287 /** | |
288 * Calls chrome.send with a callback and restores the original afterwards. | |
289 */ | |
290 function chromeSend(name, params, callbackName, callback) { | |
291 var old = global[callbackName]; | |
292 global[callbackName] = function() { | |
293 // restore | |
294 global[callbackName] = old; | |
295 | |
296 var args = Array.prototype.slice.call(arguments); | |
297 return callback.apply(global, args); | |
298 }; | |
299 chrome.send(name, params); | |
300 } | |
301 | |
302 var LayoutMode = { | |
303 SMALL: 1, | |
304 NORMAL: 2 | |
305 }; | |
306 | |
307 var layoutMode = useSmallGrid() ? LayoutMode.SMALL : LayoutMode.NORMAL; | |
308 | |
309 function handleWindowResize() { | |
310 if (window.innerWidth < 10) { | |
311 // We're probably a background tab, so don't do anything. | |
312 return; | |
313 } | |
314 | |
315 var oldLayoutMode = layoutMode; | |
316 layoutMode = useSmallGrid() ? LayoutMode.SMALL : LayoutMode.NORMAL | |
317 | |
318 if (layoutMode != oldLayoutMode){ | |
319 mostVisited.invalidate(); | |
320 mostVisited.layout(); | |
321 renderRecentlyClosed(); | |
322 } | |
323 } | |
324 | |
325 function showSection(section) { | |
326 if (!(section & shownSections)) { | |
327 shownSections |= section; | |
328 | |
329 switch (section) { | |
330 case Section.THUMB: | |
331 mostVisited.invalidate(); | |
332 mostVisited.updateDisplayMode(); | |
333 mostVisited.layout(); | |
334 break; | |
335 case Section.RECENT: | |
336 renderRecentlyClosed(); | |
337 break; | |
338 case Section.TIPS: | |
339 removeClass($('tip-line'), 'hidden'); | |
340 break; | |
341 } | |
342 } | |
343 } | |
344 | |
345 function hideSection(section) { | |
346 if (section & shownSections) { | |
347 shownSections &= ~section; | |
348 | |
349 switch (section) { | |
350 case Section.THUMB: | |
351 mostVisited.invalidate(); | |
352 mostVisited.updateDisplayMode(); | |
353 mostVisited.layout(); | |
354 break; | |
355 case Section.RECENT: | |
356 renderRecentlyClosed(); | |
357 break; | |
358 case Section.TIPS: | |
359 addClass($('tip-line'), 'hidden'); | |
360 break; | |
361 } | |
362 } | |
363 } | |
364 | |
365 var mostVisited = { | 81 var mostVisited = { |
366 addPinnedUrl_: function(data, index) { | 82 addPinnedUrl_: function(data, index) { |
367 chrome.send('addPinnedURL', [data.url, data.title, data.faviconUrl || '', | 83 chrome.send('addPinnedURL', [data.url, data.title, data.faviconUrl || '', |
368 data.thumbnailUrl || '', String(index)]); | 84 data.thumbnailUrl || '', String(index)]); |
369 }, | 85 }, |
370 getItem: function(el) { | 86 getItem: function(el) { |
371 return findAncestorByClass(el, 'thumbnail-container'); | 87 return findAncestorByClass(el, 'thumbnail-container'); |
372 }, | 88 }, |
373 | 89 |
374 getHref: function(el) { | 90 getHref: function(el) { |
(...skipping 168 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
543 this.dirty_ = false; | 259 this.dirty_ = false; |
544 | 260 |
545 logEvent('mostVisited.layout: ' + (Date.now() - d0)); | 261 logEvent('mostVisited.layout: ' + (Date.now() - d0)); |
546 }, | 262 }, |
547 | 263 |
548 getRectByIndex: function(index) { | 264 getRectByIndex: function(index) { |
549 return getMostVisitedLayoutRects()[index]; | 265 return getMostVisitedLayoutRects()[index]; |
550 } | 266 } |
551 }; | 267 }; |
552 | 268 |
553 // Recently closed | |
554 | |
555 function layoutRecentlyClosed() { | |
556 var recentShown = shownSections & Section.RECENT; | |
557 updateSimpleSection('recently-closed', Section.RECENT); | |
558 | |
559 if (recentShown) { | |
560 var recentElement = $('recently-closed'); | |
561 var style = recentElement.style; | |
562 // We cannot use clientWidth here since the width has a transition. | |
563 var spacing = 20; | |
564 var headerEl = recentElement.firstElementChild; | |
565 var navEl = recentElement.lastElementChild.lastElementChild; | |
566 var navWidth = navEl.offsetWidth; | |
567 // Subtract 10 for the padding | |
568 var availWidth = (useSmallGrid() ? 690 : 918) - navWidth - 10; | |
569 | |
570 // Now go backwards and hide as many elements as needed. | |
571 var elementsToHide = []; | |
572 for (var el = navEl.previousElementSibling; el; | |
573 el = el.previousElementSibling) { | |
574 if (el.offsetLeft + el.offsetWidth + spacing > availWidth) { | |
575 elementsToHide.push(el); | |
576 } | |
577 } | |
578 | |
579 elementsToHide.forEach(function(el) { | |
580 el.parentNode.removeChild(el); | |
581 }); | |
582 } | |
583 } | |
584 | |
585 /** | |
586 * This function is called by the backend whenever the sync status section | |
587 * needs to be updated to reflect recent sync state changes. The backend passes | |
588 * the new status information in the newMessage parameter. The state includes | |
589 * the following: | |
590 * | |
591 * syncsectionisvisible: true if the sync section needs to show up on the new | |
592 * tab page and false otherwise. | |
593 * title: the header for the sync status section. | |
594 * msg: the actual message (e.g. "Synced to foo@gmail.com"). | |
595 * linkisvisible: true if the link element should be visible within the sync | |
596 * section and false otherwise. | |
597 * linktext: the text to display as the link in the sync status (only used if | |
598 * linkisvisible is true). | |
599 * linkurlisset: true if an URL should be set as the href for the link and false | |
600 * otherwise. If this field is false, then clicking on the link | |
601 * will result in sending a message to the backend (see | |
602 * 'SyncLinkClicked'). | |
603 * linkurl: the URL to use as the element's href (only used if linkurlisset is | |
604 * true). | |
605 */ | |
606 function syncMessageChanged(newMessage) { | |
607 var syncStatusElement = $('sync-status'); | |
608 var style = syncStatusElement.style; | |
609 | |
610 // Hide the section if the message is emtpy. | |
611 if (!newMessage['syncsectionisvisible']) { | |
612 style.display = 'none'; | |
613 return; | |
614 } | |
615 style.display = 'block'; | |
616 | |
617 // Set the sync section background color based on the state. | |
618 if (newMessage.msgtype == 'error') { | |
619 style.backgroundColor = 'tomato'; | |
620 } else { | |
621 style.backgroundColor = ''; | |
622 } | |
623 | |
624 // Set the text for the header and sync message. | |
625 var titleElement = syncStatusElement.firstElementChild; | |
626 titleElement.textContent = newMessage.title; | |
627 var messageElement = titleElement.nextElementSibling; | |
628 messageElement.textContent = newMessage.msg; | |
629 | |
630 // Remove what comes after the message | |
631 while (messageElement.nextSibling) { | |
632 syncStatusElement.removeChild(messageElement.nextSibling); | |
633 } | |
634 | |
635 if (newMessage.linkisvisible) { | |
636 var el; | |
637 if (newMessage.linkurlisset) { | |
638 // Use a link | |
639 el = document.createElement('a'); | |
640 el.href = newMessage.linkurl; | |
641 } else { | |
642 el = document.createElement('button'); | |
643 el.className = 'link'; | |
644 el.addEventListener('click', syncSectionLinkClicked); | |
645 } | |
646 el.textContent = newMessage.linktext; | |
647 syncStatusElement.appendChild(el); | |
648 fixLinkUnderline(el); | |
649 } | |
650 } | |
651 | |
652 /** | |
653 * Invoked when the link in the sync status section is clicked. | |
654 */ | |
655 function syncSectionLinkClicked(e) { | |
656 chrome.send('SyncLinkClicked'); | |
657 e.preventDefault(); | |
658 } | |
659 | |
660 /** | |
661 * Invoked when link to start sync in the promo message is clicked, and Chrome | |
662 * has already been synced to an account. | |
663 */ | |
664 function syncAlreadyEnabled(message) { | |
665 showNotification(message.syncEnabledMessage, | |
666 localStrings.getString('close')); | |
667 } | |
668 | |
669 /** | |
670 * Returns the text used for a recently closed window. | |
671 * @param {number} numTabs Number of tabs in the window. | |
672 * @return {string} The text to use. | |
673 */ | |
674 function formatTabsText(numTabs) { | |
675 if (numTabs == 1) | |
676 return localStrings.getString('closedwindowsingle'); | |
677 return localStrings.getStringF('closedwindowmultiple', numTabs); | |
678 } | |
679 | |
680 /** | |
681 * We need both most visited and the shown sections to be considered loaded. | |
682 * @return {boolean} | |
683 */ | |
684 function onDataLoaded() { | |
685 if (gotMostVisited) { | |
686 mostVisited.layout(); | |
687 loading = false; | |
688 // Remove class name in a timeout so that changes done in this JS thread are | |
689 // not animated. | |
690 window.setTimeout(function() { | |
691 ensureSmallGridCorrect(); | |
692 removeClass(document.body, 'loading'); | |
693 }, 1); | |
694 } | |
695 } | |
696 | |
697 // Theme related | |
698 | |
699 function themeChanged() { | |
700 $('themecss').href = 'chrome://theme/css/newtab.css?' + Date.now(); | |
701 updateAttribution(); | |
702 } | |
703 | |
704 function updateAttribution() { | |
705 $('attribution-img').src = 'chrome://theme/theme_ntp_attribution?' + | |
706 Date.now(); | |
707 } | |
708 | |
709 function bookmarkBarAttached() { | |
710 document.documentElement.setAttribute('bookmarkbarattached', 'true'); | |
711 } | |
712 | |
713 function bookmarkBarDetached() { | |
714 document.documentElement.setAttribute('bookmarkbarattached', 'false'); | |
715 } | |
716 | |
717 function viewLog() { | |
718 var lines = []; | |
719 var start = log[0][1]; | |
720 | |
721 for (var i = 0; i < log.length; i++) { | |
722 lines.push((log[i][1] - start) + ': ' + log[i][0]); | |
723 } | |
724 | |
725 console.log(lines.join('\n')); | |
726 } | |
727 | |
728 // Updates the visibility of the menu items. | |
729 function updateOptionMenu() { | |
730 var menuItems = $('option-menu').children; | |
731 for (var i = 0; i < menuItems.length; i++) { | |
732 var item = menuItems[i]; | |
733 var command = item.getAttribute('command'); | |
734 if (command == 'show' || command == 'hide') { | |
735 var section = Section[item.getAttribute('section')]; | |
736 var visible = shownSections & section; | |
737 item.setAttribute('command', visible ? 'hide' : 'show'); | |
738 } | |
739 } | |
740 } | |
741 | |
742 // We apply the size class here so that we don't trigger layout animations | |
743 // onload. | |
744 | |
745 handleWindowResize(); | |
746 | |
747 var localStrings = new LocalStrings(); | |
748 | |
749 /////////////////////////////////////////////////////////////////////////////// | |
750 // Things we know are not needed at startup go below here | |
751 | |
752 function afterTransition(f) { | |
753 if (loading) { | |
754 // Make sure we do not use a timer during load since it slows down the UI. | |
755 f(); | |
756 } else { | |
757 // The duration of all transitions are .15s | |
758 window.setTimeout(f, 150); | |
759 } | |
760 } | |
761 | |
762 // Notification | |
763 | |
764 | |
765 var notificationTimeout; | |
766 | |
767 function showNotification(text, actionText, opt_f, opt_delay) { | |
768 var notificationElement = $('notification'); | |
769 var f = opt_f || function() {}; | |
770 var delay = opt_delay || 10000; | |
771 | |
772 function show() { | |
773 window.clearTimeout(notificationTimeout); | |
774 addClass(notificationElement, 'show'); | |
775 addClass(document.body, 'notification-shown'); | |
776 } | |
777 | |
778 function delayedHide() { | |
779 notificationTimeout = window.setTimeout(hideNotification, delay); | |
780 } | |
781 | |
782 function doAction() { | |
783 f(); | |
784 hideNotification(); | |
785 } | |
786 | |
787 // Remove any possible first-run trails. | |
788 removeClass(notification, 'first-run'); | |
789 | |
790 var actionLink = notificationElement.querySelector('.link-color'); | |
791 notificationElement.firstElementChild.textContent = text; | |
792 actionLink.textContent = actionText; | |
793 | |
794 actionLink.onclick = doAction; | |
795 actionLink.onkeydown = handleIfEnterKey(doAction); | |
796 notificationElement.onmouseover = show; | |
797 notificationElement.onmouseout = delayedHide; | |
798 actionLink.onfocus = show; | |
799 actionLink.onblur = delayedHide; | |
800 // Enable tabbing to the link now that it is shown. | |
801 actionLink.tabIndex = 0; | |
802 | |
803 show(); | |
804 delayedHide(); | |
805 } | |
806 | |
807 /** | |
808 * Hides the notifier. | |
809 */ | |
810 function hideNotification() { | |
811 var notificationElement = $('notification'); | |
812 removeClass(notificationElement, 'show'); | |
813 removeClass(document.body, 'notification-shown'); | |
814 var actionLink = notificationElement.querySelector('.link-color'); | |
815 // Prevent tabbing to the hidden link. | |
816 actionLink.tabIndex = -1; | |
817 // Setting tabIndex to -1 only prevents future tabbing to it. If, however, the | |
818 // user switches window or a tab and then moves back to this tab the element | |
819 // may gain focus. We therefore make sure that we blur the element so that the | |
820 // element focus is not restored when coming back to this window. | |
821 actionLink.blur(); | |
822 } | |
823 | |
824 function showFirstRunNotification() { | |
825 showNotification(localStrings.getString('firstrunnotification'), | |
826 localStrings.getString('closefirstrunnotification'), | |
827 null, 30000); | |
828 var notificationElement = $('notification'); | |
829 addClass(notification, 'first-run'); | |
830 } | |
831 | |
832 | |
833 /** | |
834 * This handles the option menu. | |
835 * @param {Element} button The button element. | |
836 * @param {Element} menu The menu element. | |
837 * @constructor | |
838 */ | |
839 function OptionMenu(button, menu) { | |
840 this.button = button; | |
841 this.menu = menu; | |
842 this.button.onmousedown = bind(this.handleMouseDown, this); | |
843 this.button.onkeydown = bind(this.handleKeyDown, this); | |
844 this.boundHideMenu_ = bind(this.hide, this); | |
845 this.boundMaybeHide_ = bind(this.maybeHide_, this); | |
846 this.menu.onmouseover = bind(this.handleMouseOver, this); | |
847 this.menu.onmouseout = bind(this.handleMouseOut, this); | |
848 this.menu.onmouseup = bind(this.handleMouseUp, this); | |
849 } | |
850 | |
851 OptionMenu.prototype = { | |
852 show: function() { | |
853 updateOptionMenu(); | |
854 this.positionMenu_(); | |
855 this.menu.style.display = 'block'; | |
856 addClass(this.button, 'open'); | |
857 this.button.focus(); | |
858 | |
859 // Listen to document and window events so that we hide the menu when the | |
860 // user clicks outside the menu or tabs away or the whole window is blurred. | |
861 document.addEventListener('focus', this.boundMaybeHide_, true); | |
862 document.addEventListener('mousedown', this.boundMaybeHide_, true); | |
863 }, | |
864 | |
865 positionMenu_: function() { | |
866 this.menu.style.top = this.button.getBoundingClientRect().bottom + 'px'; | |
867 }, | |
868 | |
869 hide: function() { | |
870 this.menu.style.display = 'none'; | |
871 removeClass(this.button, 'open'); | |
872 this.setSelectedIndex(-1); | |
873 | |
874 document.removeEventListener('focus', this.boundMaybeHide_, true); | |
875 document.removeEventListener('mousedown', this.boundMaybeHide_, true); | |
876 }, | |
877 | |
878 isShown: function() { | |
879 return this.menu.style.display == 'block'; | |
880 }, | |
881 | |
882 /** | |
883 * Callback for document mousedown and focus. It checks if the user tried to | |
884 * navigate to a different element on the page and if so hides the menu. | |
885 * @param {Event} e The mouse or focus event. | |
886 * @private | |
887 */ | |
888 maybeHide_: function(e) { | |
889 if (!this.menu.contains(e.target) && !this.button.contains(e.target)) { | |
890 this.hide(); | |
891 } | |
892 }, | |
893 | |
894 handleMouseDown: function(e) { | |
895 if (this.isShown()) { | |
896 this.hide(); | |
897 } else { | |
898 this.show(); | |
899 } | |
900 }, | |
901 | |
902 handleMouseOver: function(e) { | |
903 var el = e.target; | |
904 if (!el.hasAttribute('command')) { | |
905 this.setSelectedIndex(-1); | |
906 } else { | |
907 var index = Array.prototype.indexOf.call(this.menu.children, el); | |
908 this.setSelectedIndex(index); | |
909 } | |
910 }, | |
911 | |
912 handleMouseOut: function(e) { | |
913 this.setSelectedIndex(-1); | |
914 }, | |
915 | |
916 handleMouseUp: function(e) { | |
917 var item = this.getSelectedItem(); | |
918 if (item) { | |
919 this.executeItem(item); | |
920 } | |
921 }, | |
922 | |
923 handleKeyDown: function(e) { | |
924 var item = this.getSelectedItem(); | |
925 | |
926 var self = this; | |
927 function selectNextVisible(m) { | |
928 var children = self.menu.children; | |
929 var len = children.length; | |
930 var i = self.selectedIndex_; | |
931 if (i == -1 && m == -1) { | |
932 // Edge case when we need to go the last item fisrt. | |
933 i = 0; | |
934 } | |
935 while (true) { | |
936 i = (i + m + len) % len; | |
937 item = children[i]; | |
938 if (item && item.hasAttribute('command') && | |
939 item.style.display != 'none') { | |
940 break; | |
941 } | |
942 } | |
943 if (item) { | |
944 self.setSelectedIndex(i); | |
945 } | |
946 } | |
947 | |
948 switch (e.keyIdentifier) { | |
949 case 'Down': | |
950 if (!this.isShown()) { | |
951 this.show(); | |
952 } | |
953 selectNextVisible(1); | |
954 e.preventDefault(); | |
955 break; | |
956 case 'Up': | |
957 if (!this.isShown()) { | |
958 this.show(); | |
959 } | |
960 selectNextVisible(-1); | |
961 e.preventDefault(); | |
962 break; | |
963 case 'Esc': | |
964 case 'U+001B': // Maybe this is remote desktop playing a prank? | |
965 this.hide(); | |
966 break; | |
967 case 'Enter': | |
968 case 'U+0020': // Space | |
969 if (this.isShown()) { | |
970 if (item) { | |
971 this.executeItem(item); | |
972 } else { | |
973 this.hide(); | |
974 } | |
975 } else { | |
976 this.show(); | |
977 } | |
978 e.preventDefault(); | |
979 break; | |
980 } | |
981 }, | |
982 | |
983 selectedIndex_: -1, | |
984 setSelectedIndex: function(i) { | |
985 if (i != this.selectedIndex_) { | |
986 var items = this.menu.children; | |
987 var oldItem = items[this.selectedIndex_]; | |
988 if (oldItem) { | |
989 oldItem.removeAttribute('selected'); | |
990 } | |
991 var newItem = items[i]; | |
992 if (newItem) { | |
993 newItem.setAttribute('selected', 'selected'); | |
994 } | |
995 this.selectedIndex_ = i; | |
996 } | |
997 }, | |
998 | |
999 getSelectedItem: function() { | |
1000 return this.menu.children[this.selectedIndex_] || null; | |
1001 }, | |
1002 | |
1003 executeItem: function(item) { | |
1004 var command = item.getAttribute('command'); | |
1005 if (command in this.commands) { | |
1006 this.commands[command].call(this, item); | |
1007 } | |
1008 | |
1009 this.hide(); | |
1010 } | |
1011 }; | |
1012 | |
1013 var optionMenu = new OptionMenu($('option-button'), $('option-menu')); | |
1014 optionMenu.commands = { | |
1015 'clear-all-blacklisted' : function() { | |
1016 mostVisited.clearAllBlacklisted(); | |
1017 chrome.send('getMostVisited'); | |
1018 }, | |
1019 'show': function(item) { | |
1020 var section = Section[item.getAttribute('section')]; | |
1021 showSection(section); | |
1022 saveShownSections(); | |
1023 }, | |
1024 'hide': function(item) { | |
1025 var section = Section[item.getAttribute('section')]; | |
1026 hideSection(section); | |
1027 saveShownSections(); | |
1028 } | |
1029 }; | |
1030 | |
1031 $('most-visited').addEventListener('click', function(e) { | 269 $('most-visited').addEventListener('click', function(e) { |
1032 var target = e.target; | 270 var target = e.target; |
1033 if (hasClass(target, 'pin')) { | 271 if (hasClass(target, 'pin')) { |
1034 mostVisited.togglePinned(mostVisited.getItem(target)); | 272 mostVisited.togglePinned(mostVisited.getItem(target)); |
1035 e.preventDefault(); | 273 e.preventDefault(); |
1036 } else if (hasClass(target, 'remove')) { | 274 } else if (hasClass(target, 'remove')) { |
1037 mostVisited.blacklist(mostVisited.getItem(target)); | 275 mostVisited.blacklist(mostVisited.getItem(target)); |
1038 e.preventDefault(); | 276 e.preventDefault(); |
1039 } | 277 } |
1040 }); | 278 }); |
1041 | 279 |
1042 // Allow blacklisting most visited site using the keyboard. | 280 // Allow blacklisting most visited site using the keyboard. |
1043 $('most-visited').addEventListener('keydown', function(e) { | 281 $('most-visited').addEventListener('keydown', function(e) { |
1044 if (!IS_MAC && e.keyCode == 46 || // Del | 282 if (!IS_MAC && e.keyCode == 46 || // Del |
1045 IS_MAC && e.metaKey && e.keyCode == 8) { // Cmd + Backspace | 283 IS_MAC && e.metaKey && e.keyCode == 8) { // Cmd + Backspace |
1046 mostVisited.blacklist(e.target); | 284 mostVisited.blacklist(e.target); |
1047 } | 285 } |
1048 }); | 286 }); |
1049 | 287 |
1050 $('main').addEventListener('click', function(e) { | |
1051 if (e.target.tagName == 'H2') { | |
1052 var p = e.target.parentNode; | |
1053 var section = p.getAttribute('section'); | |
1054 if (section) { | |
1055 if (shownSections & Section[section]) | |
1056 hideSection(Section[section]); | |
1057 else | |
1058 showSection(Section[section]); | |
1059 saveShownSections(); | |
1060 } | |
1061 } | |
1062 }); | |
1063 | |
1064 function handleIfEnterKey(f) { | |
1065 return function(e) { | |
1066 if (e.keyIdentifier == 'Enter') { | |
1067 f(e); | |
1068 } | |
1069 }; | |
1070 } | |
1071 | |
1072 function maybeReopenTab(e) { | |
1073 var el = findAncestor(e.target, function(el) { | |
1074 return el.sessionId !== undefined; | |
1075 }); | |
1076 if (el) { | |
1077 chrome.send('reopenTab', [String(el.sessionId)]); | |
1078 e.preventDefault(); | |
1079 | |
1080 // HACK(arv): After the window onblur event happens we get a mouseover event | |
1081 // on the next item and we want to make sure that we do not show a tooltip | |
1082 // for that. | |
1083 window.setTimeout(function() { | |
1084 windowTooltip.hide(); | |
1085 }, 2 * WindowTooltip.DELAY); | |
1086 } | |
1087 } | |
1088 | |
1089 function maybeShowWindowTooltip(e) { | |
1090 var f = function(el) { | |
1091 return el.tabItems !== undefined; | |
1092 }; | |
1093 var el = findAncestor(e.target, f); | |
1094 var relatedEl = findAncestor(e.relatedTarget, f); | |
1095 if (el && el != relatedEl) { | |
1096 windowTooltip.handleMouseOver(e, el, el.tabItems); | |
1097 } | |
1098 } | |
1099 | |
1100 | |
1101 var recentlyClosedElement = $('recently-closed'); | |
1102 | |
1103 recentlyClosedElement.addEventListener('click', maybeReopenTab); | |
1104 recentlyClosedElement.addEventListener('keydown', | |
1105 handleIfEnterKey(maybeReopenTab)); | |
1106 | |
1107 recentlyClosedElement.addEventListener('mouseover', maybeShowWindowTooltip); | |
1108 recentlyClosedElement.addEventListener('focus', maybeShowWindowTooltip, true); | |
1109 | |
1110 /** | |
1111 * This object represents a tooltip representing a closed window. It is | |
1112 * shown when hovering over a closed window item or when the item is focused. It | |
1113 * gets hidden when blurred or when mousing out of the menu or the item. | |
1114 * @param {Element} tooltipEl The element to use as the tooltip. | |
1115 * @constructor | |
1116 */ | |
1117 function WindowTooltip(tooltipEl) { | |
1118 this.tooltipEl = tooltipEl; | |
1119 this.boundHide_ = bind(this.hide, this); | |
1120 this.boundHandleMouseOut_ = bind(this.handleMouseOut, this); | |
1121 } | |
1122 | |
1123 WindowTooltip.trackMouseMove_ = function(e) { | |
1124 WindowTooltip.clientX = e.clientX; | |
1125 WindowTooltip.clientY = e.clientY; | |
1126 }; | |
1127 | |
1128 /** | |
1129 * Time in ms to delay before the tooltip is shown. | |
1130 * @type {number} | |
1131 */ | |
1132 WindowTooltip.DELAY = 300; | |
1133 | |
1134 WindowTooltip.prototype = { | |
1135 timer: 0, | |
1136 handleMouseOver: function(e, linkEl, tabs) { | |
1137 this.linkEl_ = linkEl; | |
1138 if (e.type == 'mouseover') { | |
1139 this.linkEl_.addEventListener('mousemove', WindowTooltip.trackMouseMove_); | |
1140 this.linkEl_.addEventListener('mouseout', this.boundHandleMouseOut_); | |
1141 } else { // focus | |
1142 this.linkEl_.addEventListener('blur', this.boundHide_); | |
1143 } | |
1144 this.timer = window.setTimeout(bind(this.show, this, e.type, linkEl, tabs), | |
1145 WindowTooltip.DELAY); | |
1146 }, | |
1147 show: function(type, linkEl, tabs) { | |
1148 window.addEventListener('blur', this.boundHide_); | |
1149 this.linkEl_.removeEventListener('mousemove', | |
1150 WindowTooltip.trackMouseMove_); | |
1151 window.clearTimeout(this.timer); | |
1152 | |
1153 this.renderItems(tabs); | |
1154 var rect = linkEl.getBoundingClientRect(); | |
1155 var bodyRect = document.body.getBoundingClientRect(); | |
1156 var rtl = document.documentElement.dir == 'rtl'; | |
1157 | |
1158 this.tooltipEl.style.display = 'block'; | |
1159 var tooltipRect = this.tooltipEl.getBoundingClientRect(); | |
1160 var x, y; | |
1161 | |
1162 // When focused show below, like a drop down menu. | |
1163 if (type == 'focus') { | |
1164 x = rtl ? | |
1165 rect.left + bodyRect.left + rect.width - this.tooltipEl.offsetWidth : | |
1166 rect.left + bodyRect.left; | |
1167 y = rect.top + bodyRect.top + rect.height; | |
1168 } else { | |
1169 x = bodyRect.left + (rtl ? | |
1170 WindowTooltip.clientX - this.tooltipEl.offsetWidth : | |
1171 WindowTooltip.clientX); | |
1172 // Offset like a tooltip | |
1173 y = 20 + WindowTooltip.clientY + bodyRect.top; | |
1174 } | |
1175 | |
1176 // We need to ensure that the tooltip is inside the window viewport. | |
1177 x = Math.min(x, bodyRect.width - tooltipRect.width); | |
1178 x = Math.max(x, 0); | |
1179 y = Math.min(y, bodyRect.height - tooltipRect.height); | |
1180 y = Math.max(y, 0); | |
1181 | |
1182 this.tooltipEl.style.left = x + 'px'; | |
1183 this.tooltipEl.style.top = y + 'px'; | |
1184 }, | |
1185 handleMouseOut: function(e) { | |
1186 // Don't hide when move to another item in the link. | |
1187 var f = function(el) { | |
1188 return el.tabItems !== undefined; | |
1189 }; | |
1190 var el = findAncestor(e.target, f); | |
1191 var relatedEl = findAncestor(e.relatedTarget, f); | |
1192 if (el && el != relatedEl) { | |
1193 this.hide(); | |
1194 } | |
1195 }, | |
1196 hide: function() { | |
1197 window.clearTimeout(this.timer); | |
1198 window.removeEventListener('blur', this.boundHide_); | |
1199 this.linkEl_.removeEventListener('mousemove', | |
1200 WindowTooltip.trackMouseMove_); | |
1201 this.linkEl_.removeEventListener('mouseout', this.boundHandleMouseOut_); | |
1202 this.linkEl_.removeEventListener('blur', this.boundHide_); | |
1203 this.linkEl_ = null; | |
1204 | |
1205 this.tooltipEl.style.display = 'none'; | |
1206 }, | |
1207 renderItems: function(tabs) { | |
1208 var tooltip = this.tooltipEl; | |
1209 tooltip.textContent = ''; | |
1210 | |
1211 tabs.forEach(function(tab) { | |
1212 var span = document.createElement('span'); | |
1213 span.className = 'item'; | |
1214 span.style.backgroundImage = url('chrome://favicon/' + tab.url); | |
1215 span.dir = tab.direction; | |
1216 span.textContent = tab.title; | |
1217 tooltip.appendChild(span); | |
1218 }); | |
1219 } | |
1220 }; | |
1221 | |
1222 var windowTooltip = new WindowTooltip($('window-tooltip')); | |
1223 | |
1224 window.addEventListener('load', bind(logEvent, global, 'Tab.NewTabOnload', | |
1225 true)); | |
1226 window.addEventListener('load', onDataLoaded); | 288 window.addEventListener('load', onDataLoaded); |
1227 | 289 |
1228 window.addEventListener('resize', handleWindowResize); | 290 window.addEventListener('resize', handleWindowResize); |
1229 document.addEventListener('DOMContentLoaded', | |
1230 bind(logEvent, global, 'Tab.NewTabDOMContentLoaded', true)); | |
1231 | |
1232 // Whether or not we should send the initial 'GetSyncMessage' to the backend | |
1233 // depends on the value of the attribue 'syncispresent' which the backend sets | |
1234 // to indicate if there is code in the backend which is capable of processing | |
1235 // this message. This attribute is loaded by the JSTemplate and therefore we | |
1236 // must make sure we check the attribute after the DOM is loaded. | |
1237 document.addEventListener('DOMContentLoaded', | |
1238 callGetSyncMessageIfSyncIsPresent); | |
1239 | |
1240 // Set up links and text-decoration for promotional message. | |
1241 document.addEventListener('DOMContentLoaded', setUpPromoMessage); | |
1242 | 291 |
1243 // Work around for http://crbug.com/25329 | 292 // Work around for http://crbug.com/25329 |
1244 function ensureSmallGridCorrect() { | 293 function ensureSmallGridCorrect() { |
1245 if (wasSmallGrid != useSmallGrid()) { | 294 if (wasSmallGrid != useSmallGrid()) { |
1246 applyMostVisitedRects(); | 295 applyMostVisitedRects(); |
1247 } | 296 } |
1248 } | 297 } |
1249 document.addEventListener('DOMContentLoaded', ensureSmallGridCorrect); | 298 document.addEventListener('DOMContentLoaded', ensureSmallGridCorrect); |
1250 | 299 |
1251 /** | |
1252 * The sync code is not yet built by default on all platforms so we have to | |
1253 * make sure we don't send the initial sync message to the backend unless the | |
1254 * backend told us that the sync code is present. | |
1255 */ | |
1256 function callGetSyncMessageIfSyncIsPresent() { | |
1257 if (document.documentElement.getAttribute('syncispresent') == 'true') { | |
1258 chrome.send('GetSyncMessage'); | |
1259 } | |
1260 } | |
1261 | |
1262 function setAsHomePageLinkClicked(e) { | |
1263 chrome.send('setHomePage'); | |
1264 e.preventDefault(); | |
1265 } | |
1266 | |
1267 function onHomePageSet(data) { | |
1268 showNotification(data[0], data[1]); | |
1269 // Removes the "make this my home page" tip. | |
1270 clearTipLine(); | |
1271 } | |
1272 | |
1273 function hideAllMenus() { | |
1274 optionMenu.hide(); | |
1275 } | |
1276 | |
1277 window.addEventListener('blur', hideAllMenus); | |
1278 window.addEventListener('keydown', function(e) { | |
1279 if (e.keyIdentifier == 'Alt' || e.keyIdentifier == 'Meta') { | |
1280 hideAllMenus(); | |
1281 } | |
1282 }, true); | |
1283 | |
1284 // Tooltip for elements that have text that overflows. | |
1285 document.addEventListener('mouseover', function(e) { | |
1286 // We don't want to do this while we are dragging because it makes things very | |
1287 // janky | |
1288 if (dnd.dragItem) { | |
1289 return; | |
1290 } | |
1291 | |
1292 var el = findAncestor(e.target, function(el) { | |
1293 return el.xtitle; | |
1294 }); | |
1295 if (el && el.xtitle != el.title) { | |
1296 if (el.scrollWidth > el.clientWidth) { | |
1297 el.title = el.xtitle; | |
1298 } else { | |
1299 el.title = ''; | |
1300 } | |
1301 } | |
1302 }); | |
1303 | |
1304 // DnD | 300 // DnD |
1305 | 301 |
1306 var dnd = { | 302 var dnd = { |
1307 currentOverItem_: null, | 303 currentOverItem_: null, |
1308 get currentOverItem() { | 304 get currentOverItem() { |
1309 return this.currentOverItem_; | 305 return this.currentOverItem_; |
1310 }, | 306 }, |
1311 set currentOverItem(item) { | 307 set currentOverItem(item) { |
1312 var style; | 308 var style; |
1313 if (item != this.currentOverItem_) { | 309 if (item != this.currentOverItem_) { |
(...skipping 173 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1487 el.addEventListener('dragleave', bind(this.handleDragLeave, this)); | 483 el.addEventListener('dragleave', bind(this.handleDragLeave, this)); |
1488 el.addEventListener('drop', bind(this.handleDrop, this)); | 484 el.addEventListener('drop', bind(this.handleDrop, this)); |
1489 el.addEventListener('dragend', bind(this.handleDragEnd, this)); | 485 el.addEventListener('dragend', bind(this.handleDragEnd, this)); |
1490 el.addEventListener('drag', bind(this.handleDrag, this)); | 486 el.addEventListener('drag', bind(this.handleDrag, this)); |
1491 el.addEventListener('mousedown', bind(this.handleMouseDown, this)); | 487 el.addEventListener('mousedown', bind(this.handleMouseDown, this)); |
1492 } | 488 } |
1493 }; | 489 }; |
1494 | 490 |
1495 dnd.init(); | 491 dnd.init(); |
1496 | 492 |
1497 /** | |
1498 * Whitelist of tag names allowed in parseHtmlSubset. | |
1499 * @type {[string]} | |
1500 */ | |
1501 var allowedTags = ['A', 'B', 'STRONG']; | |
1502 | |
1503 /** | |
1504 * Parse a very small subset of HTML. | |
1505 * @param {string} s The string to parse. | |
1506 * @throws {Error} In case of non supported markup. | |
1507 * @return {DocumentFragment} A document fragment containing the DOM tree. | |
1508 */ | |
1509 var allowedAttributes = { | |
1510 'href': function(node, value) { | |
1511 // Only allow a[href] starting with http:// and https:// | |
1512 return node.tagName == 'A' && (value.indexOf('http://') == 0 || | |
1513 value.indexOf('https://') == 0); | |
1514 }, | |
1515 'target': function(node, value) { | |
1516 // Allow a[target] but reset the value to "". | |
1517 if (node.tagName != 'A') | |
1518 return false; | |
1519 node.setAttribute('target', ''); | |
1520 return true; | |
1521 } | |
1522 } | |
1523 | |
1524 /** | |
1525 * Parse a very small subset of HTML. This ensures that insecure HTML / | |
1526 * javascript cannot be injected into the new tab page. | |
1527 * @param {string} s The string to parse. | |
1528 * @throws {Error} In case of non supported markup. | |
1529 * @return {DocumentFragment} A document fragment containing the DOM tree. | |
1530 */ | |
1531 function parseHtmlSubset(s) { | |
1532 function walk(n, f) { | |
1533 f(n); | |
1534 for (var i = 0; i < n.childNodes.length; i++) { | |
1535 walk(n.childNodes[i], f); | |
1536 } | |
1537 } | |
1538 | |
1539 function assertElement(node) { | |
1540 if (allowedTags.indexOf(node.tagName) == -1) | |
1541 throw Error(node.tagName + ' is not supported'); | |
1542 } | |
1543 | |
1544 function assertAttribute(attrNode, node) { | |
1545 var n = attrNode.nodeName; | |
1546 var v = attrNode.nodeValue; | |
1547 if (!allowedAttributes.hasOwnProperty(n) || !allowedAttributes[n](node, v)) | |
1548 throw Error(node.tagName + '[' + n + '="' + v + '"] is not supported'); | |
1549 } | |
1550 | |
1551 var r = document.createRange(); | |
1552 r.selectNode(document.body); | |
1553 // This does not execute any scripts. | |
1554 var df = r.createContextualFragment(s); | |
1555 walk(df, function(node) { | |
1556 switch (node.nodeType) { | |
1557 case Node.ELEMENT_NODE: | |
1558 assertElement(node); | |
1559 var attrs = node.attributes; | |
1560 for (var i = 0; i < attrs.length; i++) { | |
1561 assertAttribute(attrs[i], node); | |
1562 } | |
1563 break; | |
1564 | |
1565 case Node.COMMENT_NODE: | |
1566 case Node.DOCUMENT_FRAGMENT_NODE: | |
1567 case Node.TEXT_NODE: | |
1568 break; | |
1569 | |
1570 default: | |
1571 throw Error('Node type ' + node.nodeType + ' is not supported'); | |
1572 } | |
1573 }); | |
1574 return df; | |
1575 } | |
1576 | |
1577 /** | |
1578 * Makes links and buttons support a different underline color. | |
1579 * @param {Node} node The node to search for links and buttons in. | |
1580 */ | |
1581 function fixLinkUnderlines(node) { | |
1582 var elements = node.querySelectorAll('a,button'); | |
1583 Array.prototype.forEach.call(elements, fixLinkUnderline); | |
1584 } | |
1585 | |
1586 /** | |
1587 * Wraps the content of an element in a a link-color span. | |
1588 * @param {Element} el The element to wrap. | |
1589 */ | |
1590 function fixLinkUnderline(el) { | |
1591 var span = document.createElement('span'); | |
1592 span.className = 'link-color'; | |
1593 while (el.hasChildNodes()) { | |
1594 span.appendChild(el.firstChild); | |
1595 } | |
1596 el.appendChild(span); | |
1597 } | |
1598 | |
1599 updateAttribution(); | |
1600 | |
1601 // Closes the promo line when close button is clicked. | |
1602 $('promo-close').onclick = function (e) { | |
1603 addClass($('promo-line'), 'hidden'); | |
1604 chrome.send('stopPromoLineMessage'); | |
1605 e.preventDefault(); | |
1606 }; | |
1607 | |
1608 // Set bookmark sync button to start bookmark sync process on click; also set | |
1609 // link underline colors correctly. | |
1610 function setUpPromoMessage() { | |
1611 var syncButton = document.querySelector('#promo-message button'); | |
1612 syncButton.className = 'sync-button link'; | |
1613 syncButton.onclick = syncSectionLinkClicked; | |
1614 fixLinkUnderlines($('promo-message')); | |
1615 } | |
OLD | NEW |