| OLD | NEW |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 | 5 |
| 6 /** | 6 /** |
| 7 * @fileoverview The local InstantExtended NTP. | 7 * @fileoverview The local InstantExtended NTP. |
| 8 */ | 8 */ |
| 9 | 9 |
| 10 | 10 |
| 11 /** | 11 /** |
| 12 * Controls rendering the new tab page for InstantExtended. | 12 * Controls rendering the new tab page for InstantExtended. |
| 13 * @return {Object} A limited interface for testing the local NTP. | 13 * @return {Object} A limited interface for testing the local NTP. |
| 14 */ | 14 */ |
| 15 function LocalNTP() { | 15 function LocalNTP() { |
| 16 'use strict'; | 16 'use strict'; |
| 17 | 17 |
| 18 | 18 |
| 19 /** | 19 /** |
| 20 * Alias for document.getElementById. | 20 * Alias for document.getElementById. |
| 21 * @param {string} id The ID of the element to find. | 21 * @param {string} id The ID of the element to find. |
| 22 * @return {HTMLElement} The found element or null if not found. | 22 * @return {HTMLElement} The found element or null if not found. |
| 23 */ | 23 */ |
| 24 function $(id) { | 24 function $(id) { |
| 25 return document.getElementById(id); | 25 return document.getElementById(id); |
| 26 } | 26 } |
| 27 | 27 |
| 28 | 28 |
| 29 /** | 29 /** |
| 30 * Specifications for an NTP design (not comprehensive). | 30 * Specifications for an NTP design (not comprehensive). |
| 31 * | 31 * |
| 32 * fakeboxWingSize: Extra distance for fakebox to extend beyond beyond the list | 32 * fakeboxWingSize: Extra distance for fakebox to extend beyond beyond the |
| 33 * of tiles. | 33 * list |
| 34 * fontFamily: Font family to use for title and thumbnail iframes. | 34 * of tiles. |
| 35 * fontSize: Font size to use for the iframes, in px. | 35 * fontFamily: Font family to use for title and thumbnail iframes. |
| 36 * mainClass: Class applied to #ntp-contents to control CSS. | 36 * fontSize: Font size to use for the iframes, in px. |
| 37 * numTitleLines: Number of lines to display in titles. | 37 * mainClass: Class applied to #ntp-contents to control CSS. |
| 38 * showFavicon: Whether to show favicon. | 38 * numTitleLines: Number of lines to display in titles. |
| 39 * thumbnailTextColor: The 4-component color that thumbnail iframe may use to | 39 * showFavicon: Whether to show favicon. |
| 40 * display text message in place of missing thumbnail. | 40 * thumbnailTextColor: The 4-component color that thumbnail iframe may use to |
| 41 * thumbnailFallback: (Optional) A value in THUMBNAIL_FALLBACK to specify the | 41 * display text message in place of missing thumbnail. |
| 42 * thumbnail fallback strategy. If unassigned, then the thumbnail.html | 42 * thumbnailFallback: (Optional) A value in THUMBNAIL_FALLBACK to specify the |
| 43 * iframe would handle the fallback. | 43 * thumbnail fallback strategy. If unassigned, then the thumbnail.html |
| 44 * tileWidth: The width of each suggestion tile, in px. | 44 * iframe would handle the fallback. |
| 45 * tileMargin: Spacing between successive tiles, in px. | 45 * tileWidth: The width of each suggestion tile, in px. |
| 46 * titleColor: The 4-component color of title text. | 46 * tileMargin: Spacing between successive tiles, in px. |
| 47 * titleColorAgainstDark: The 4-component color of title text against a dark | 47 * titleColor: The 4-component color of title text. |
| 48 * theme. | 48 * titleColorAgainstDark: The 4-component color of title text against a dark |
| 49 * titleTextAlign: (Optional) The alignment of title text. If unspecified, the | 49 * theme. |
| 50 * default value is 'center'. | 50 * titleTextAlign: (Optional) The alignment of title text. If unspecified, the |
| 51 * titleTextFade: (Optional) The number of pixels beyond which title | 51 * default value is 'center'. |
| 52 * text begins to fade. This overrides the default ellipsis style. | 52 * titleTextFade: (Optional) The number of pixels beyond which title |
| 53 * | 53 * text begins to fade. This overrides the default ellipsis style. |
| 54 * @type {{ | 54 * |
| 55 * fakeboxWingSize: number, | 55 * @type {{ |
| 56 * fontFamily: string, | 56 * fakeboxWingSize: number, |
| 57 * fontSize: number, | 57 * fontFamily: string, |
| 58 * mainClass: string, | 58 * fontSize: number, |
| 59 * numTitleLines: number, | 59 * mainClass: string, |
| 60 * showFavicon: boolean, | 60 * numTitleLines: number, |
| 61 * thumbnailTextColor: string, | 61 * showFavicon: boolean, |
| 62 * thumbnailFallback: string|null|undefined, | 62 * thumbnailTextColor: string, |
| 63 * tileWidth: number, | 63 * thumbnailFallback: string|null|undefined, |
| 64 * tileMargin: number, | 64 * tileWidth: number, |
| 65 * titleColor: string, | 65 * tileMargin: number, |
| 66 * titleColorAgainstDark: string, | 66 * titleColor: string, |
| 67 * titleTextAlign: string|null|undefined, | 67 * titleColorAgainstDark: string, |
| 68 * titleTextFade: number|null|undefined | 68 * titleTextAlign: string|null|undefined, |
| 69 * }} | 69 * titleTextFade: number|null|undefined |
| 70 */ | 70 * }} |
| 71 var NTP_DESIGN = { | 71 */ |
| 72 fakeboxWingSize: 0, | 72 var NTP_DESIGN = { |
| 73 fontFamily: 'arial, sans-serif', | 73 fakeboxWingSize: 0, |
| 74 fontSize: 12, | 74 fontFamily: 'arial, sans-serif', |
| 75 mainClass: 'thumb-ntp', | 75 fontSize: 12, |
| 76 numTitleLines: 1, | 76 mainClass: 'thumb-ntp', |
| 77 showFavicon: true, | 77 numTitleLines: 1, |
| 78 thumbnailTextColor: [50, 50, 50, 255], | 78 showFavicon: true, |
| 79 thumbnailFallback: 'dot', // Draw single dot. | 79 thumbnailTextColor: [50, 50, 50, 255], |
| 80 tileWidth: 154, | 80 thumbnailFallback: 'dot', // Draw single dot. |
| 81 tileMargin: 16, | 81 tileWidth: 154, |
| 82 titleColor: [50, 50, 50, 255], | 82 tileMargin: 16, |
| 83 titleColorAgainstDark: [210, 210, 210, 255], | 83 titleColor: [50, 50, 50, 255], |
| 84 titleTextAlign: 'inherit', | 84 titleColorAgainstDark: [210, 210, 210, 255], |
| 85 titleTextFade: 122 - 36 // 112px wide title with 32 pixel fade at end. | 85 titleTextAlign: 'inherit', |
| 86 }; | 86 titleTextFade: 122 - 36 // 112px wide title with 32 pixel fade at end. |
| 87 | 87 }; |
| 88 | 88 |
| 89 /** | 89 |
| 90 * Modifies NTP_DESIGN parameters for icon NTP. | 90 /** |
| 91 */ | 91 * Modifies NTP_DESIGN parameters for icon NTP. |
| 92 function modifyNtpDesignForIcons() { | 92 */ |
| 93 NTP_DESIGN.fakeboxWingSize = 132; | 93 function modifyNtpDesignForIcons() { |
| 94 NTP_DESIGN.mainClass = 'icon-ntp'; | 94 NTP_DESIGN.fakeboxWingSize = 132; |
| 95 NTP_DESIGN.numTitleLines = 2; | 95 NTP_DESIGN.mainClass = 'icon-ntp'; |
| 96 NTP_DESIGN.showFavicon = false; | 96 NTP_DESIGN.numTitleLines = 2; |
| 97 NTP_DESIGN.thumbnailFallback = null; | 97 NTP_DESIGN.showFavicon = false; |
| 98 NTP_DESIGN.tileWidth = 48 + 2 * 18; | 98 NTP_DESIGN.thumbnailFallback = null; |
| 99 NTP_DESIGN.tileMargin = 60 - 18 * 2; | 99 NTP_DESIGN.tileWidth = 48 + 2 * 18; |
| 100 NTP_DESIGN.titleColor = [120, 120, 120, 255]; | 100 NTP_DESIGN.tileMargin = 60 - 18 * 2; |
| 101 NTP_DESIGN.titleColorAgainstDark = [210, 210, 210, 255]; | 101 NTP_DESIGN.titleColor = [120, 120, 120, 255]; |
| 102 NTP_DESIGN.titleTextAlign = 'center'; | 102 NTP_DESIGN.titleColorAgainstDark = [210, 210, 210, 255]; |
| 103 delete NTP_DESIGN.titleTextFade; | 103 NTP_DESIGN.titleTextAlign = 'center'; |
| 104 } | 104 delete NTP_DESIGN.titleTextFade; |
| 105 | 105 } |
| 106 | 106 |
| 107 /** | 107 |
| 108 * Enum for classnames. | 108 /** |
| 109 * @enum {string} | 109 * Enum for classnames. |
| 110 * @const | 110 * @enum {string} |
| 111 */ | 111 * @const |
| 112 var CLASSES = { | 112 */ |
| 113 ALTERNATE_LOGO: 'alternate-logo', // Shows white logo if required by theme | 113 var CLASSES = { |
| 114 DARK: 'dark', | 114 ALTERNATE_LOGO: 'alternate-logo', // Shows white logo if required by theme |
| 115 DEFAULT_THEME: 'default-theme', | 115 DARK: 'dark', |
| 116 DELAYED_HIDE_NOTIFICATION: 'mv-notice-delayed-hide', | 116 DEFAULT_THEME: 'default-theme', |
| 117 FAKEBOX_DISABLE: 'fakebox-disable', // Makes fakebox non-interactive | 117 DELAYED_HIDE_NOTIFICATION: 'mv-notice-delayed-hide', |
| 118 FAKEBOX_FOCUS: 'fakebox-focused', // Applies focus styles to the fakebox | 118 FAKEBOX_DISABLE: 'fakebox-disable', // Makes fakebox non-interactive |
| 119 // Applies drag focus style to the fakebox | 119 FAKEBOX_FOCUS: 'fakebox-focused', // Applies focus styles to the fakebox |
| 120 FAKEBOX_DRAG_FOCUS: 'fakebox-drag-focused', | 120 // Applies drag focus style to the fakebox |
| 121 HIDE_FAKEBOX_AND_LOGO: 'hide-fakebox-logo', | 121 FAKEBOX_DRAG_FOCUS: 'fakebox-drag-focused', |
| 122 HIDE_NOTIFICATION: 'mv-notice-hide', | 122 HIDE_FAKEBOX_AND_LOGO: 'hide-fakebox-logo', |
| 123 LEFT_ALIGN_ATTRIBUTION: 'left-align-attribution', | 123 HIDE_NOTIFICATION: 'mv-notice-hide', |
| 124 // Vertically centers the most visited section for a non-Google provided page. | 124 LEFT_ALIGN_ATTRIBUTION: 'left-align-attribution', |
| 125 NON_GOOGLE_PAGE: 'non-google-page', | 125 // Vertically centers the most visited section for a non-Google provided |
| 126 RTL: 'rtl' // Right-to-left language text. | 126 // page. |
| 127 }; | 127 NON_GOOGLE_PAGE: 'non-google-page', |
| 128 | 128 RTL: 'rtl' // Right-to-left language text. |
| 129 | 129 }; |
| 130 /** | 130 |
| 131 * Enum for HTML element ids. | 131 |
| 132 * @enum {string} | 132 /** |
| 133 * @const | 133 * Enum for HTML element ids. |
| 134 */ | 134 * @enum {string} |
| 135 var IDS = { | 135 * @const |
| 136 ATTRIBUTION: 'attribution', | 136 */ |
| 137 ATTRIBUTION_TEXT: 'attribution-text', | 137 var IDS = { |
| 138 CUSTOM_THEME_STYLE: 'ct-style', | 138 ATTRIBUTION: 'attribution', |
| 139 FAKEBOX: 'fakebox', | 139 ATTRIBUTION_TEXT: 'attribution-text', |
| 140 FAKEBOX_INPUT: 'fakebox-input', | 140 CUSTOM_THEME_STYLE: 'ct-style', |
| 141 FAKEBOX_TEXT: 'fakebox-text', | 141 FAKEBOX: 'fakebox', |
| 142 LOGO: 'logo', | 142 FAKEBOX_INPUT: 'fakebox-input', |
| 143 NOTIFICATION: 'mv-notice', | 143 FAKEBOX_TEXT: 'fakebox-text', |
| 144 NOTIFICATION_CLOSE_BUTTON: 'mv-notice-x', | 144 LOGO: 'logo', |
| 145 NOTIFICATION_MESSAGE: 'mv-msg', | 145 NOTIFICATION: 'mv-notice', |
| 146 NTP_CONTENTS: 'ntp-contents', | 146 NOTIFICATION_CLOSE_BUTTON: 'mv-notice-x', |
| 147 RESTORE_ALL_LINK: 'mv-restore', | 147 NOTIFICATION_MESSAGE: 'mv-msg', |
| 148 TILES: 'mv-tiles', | 148 NTP_CONTENTS: 'ntp-contents', |
| 149 UNDO_LINK: 'mv-undo' | 149 RESTORE_ALL_LINK: 'mv-restore', |
| 150 }; | 150 TILES: 'mv-tiles', |
| 151 | 151 UNDO_LINK: 'mv-undo' |
| 152 | 152 }; |
| 153 /** | 153 |
| 154 * Enum for keycodes. | 154 |
| 155 * @enum {number} | 155 /** |
| 156 * @const | 156 * Enum for keycodes. |
| 157 */ | 157 * @enum {number} |
| 158 var KEYCODE = { | 158 * @const |
| 159 ENTER: 13 | 159 */ |
| 160 }; | 160 var KEYCODE = {ENTER: 13}; |
| 161 | 161 |
| 162 | 162 |
| 163 /** | 163 /** |
| 164 * Enum for the state of the NTP when it is disposed. | 164 * Enum for the state of the NTP when it is disposed. |
| 165 * @enum {number} | 165 * @enum {number} |
| 166 * @const | 166 * @const |
| 167 */ | 167 */ |
| 168 var NTP_DISPOSE_STATE = { | 168 var NTP_DISPOSE_STATE = { |
| 169 NONE: 0, // Preserve the NTP appearance and functionality | 169 NONE: 0, // Preserve the NTP appearance and functionality |
| 170 DISABLE_FAKEBOX: 1, | 170 DISABLE_FAKEBOX: 1, |
| 171 HIDE_FAKEBOX_AND_LOGO: 2 | 171 HIDE_FAKEBOX_AND_LOGO: 2 |
| 172 }; | 172 }; |
| 173 | 173 |
| 174 | 174 |
| 175 /** | 175 /** |
| 176 * The notification displayed when a page is blacklisted. | 176 * The notification displayed when a page is blacklisted. |
| 177 * @type {Element} | 177 * @type {Element} |
| 178 */ | 178 */ |
| 179 var notification; | 179 var notification; |
| 180 | 180 |
| 181 | 181 |
| 182 /** | 182 /** |
| 183 * The container for the theme attribution. | 183 * The container for the theme attribution. |
| 184 * @type {Element} | 184 * @type {Element} |
| 185 */ | 185 */ |
| 186 var attribution; | 186 var attribution; |
| 187 | 187 |
| 188 | 188 |
| 189 /** | 189 /** |
| 190 * The "fakebox" - an input field that looks like a regular searchbox. When it | 190 * The "fakebox" - an input field that looks like a regular searchbox. When |
| 191 * is focused, any text the user types goes directly into the omnibox. | 191 * it |
| 192 * @type {Element} | 192 * is focused, any text the user types goes directly into the omnibox. |
| 193 */ | 193 * @type {Element} |
| 194 var fakebox; | 194 */ |
| 195 | 195 var fakebox; |
| 196 | 196 |
| 197 /** | 197 |
| 198 * The container for NTP elements. | 198 /** |
| 199 * @type {Element} | 199 * The container for NTP elements. |
| 200 */ | 200 * @type {Element} |
| 201 var ntpContents; | 201 */ |
| 202 | 202 var ntpContents; |
| 203 | 203 |
| 204 /** | 204 |
| 205 * The last blacklisted tile rid if any, which by definition should not be | 205 /** |
| 206 * filler. | 206 * The last blacklisted tile rid if any, which by definition should not be |
| 207 * @type {?number} | 207 * filler. |
| 208 */ | 208 * @type {?number} |
| 209 var lastBlacklistedTile = null; | 209 */ |
| 210 | 210 var lastBlacklistedTile = null; |
| 211 | 211 |
| 212 /** | 212 |
| 213 * Current number of tiles columns shown based on the window width, including | 213 /** |
| 214 * those that just contain filler. | 214 * Current number of tiles columns shown based on the window width, including |
| 215 * @type {number} | 215 * those that just contain filler. |
| 216 */ | 216 * @type {number} |
| 217 var numColumnsShown = 0; | 217 */ |
| 218 | 218 var numColumnsShown = 0; |
| 219 | 219 |
| 220 /** | 220 |
| 221 * The browser embeddedSearch.newTabPage object. | 221 /** |
| 222 * @type {Object} | 222 * The browser embeddedSearch.newTabPage object. |
| 223 */ | 223 * @type {Object} |
| 224 var ntpApiHandle; | 224 */ |
| 225 | 225 var ntpApiHandle; |
| 226 | 226 |
| 227 /** | 227 |
| 228 * The browser embeddedSearch.searchBox object. | 228 /** |
| 229 * @type {Object} | 229 * The browser embeddedSearch.searchBox object. |
| 230 */ | 230 * @type {Object} |
| 231 var searchboxApiHandle; | 231 */ |
| 232 | 232 var searchboxApiHandle; |
| 233 | 233 |
| 234 /** | 234 |
| 235 * The state of the NTP when a query is entered into the Omnibox. | 235 /** |
| 236 * @type {NTP_DISPOSE_STATE} | 236 * The state of the NTP when a query is entered into the Omnibox. |
| 237 */ | 237 * @type {NTP_DISPOSE_STATE} |
| 238 var omniboxInputBehavior = NTP_DISPOSE_STATE.NONE; | 238 */ |
| 239 | 239 var omniboxInputBehavior = NTP_DISPOSE_STATE.NONE; |
| 240 | 240 |
| 241 /** | 241 |
| 242 * The state of the NTP when a query is entered into the Fakebox. | 242 /** |
| 243 * @type {NTP_DISPOSE_STATE} | 243 * The state of the NTP when a query is entered into the Fakebox. |
| 244 */ | 244 * @type {NTP_DISPOSE_STATE} |
| 245 var fakeboxInputBehavior = NTP_DISPOSE_STATE.HIDE_FAKEBOX_AND_LOGO; | 245 */ |
| 246 | 246 var fakeboxInputBehavior = NTP_DISPOSE_STATE.HIDE_FAKEBOX_AND_LOGO; |
| 247 | 247 |
| 248 /** @type {number} @const */ | 248 |
| 249 var MAX_NUM_TILES_TO_SHOW = 8; | 249 /** @type {number} @const */ |
| 250 | 250 var MAX_NUM_TILES_TO_SHOW = 8; |
| 251 | 251 |
| 252 /** @type {number} @const */ | 252 |
| 253 var MIN_NUM_COLUMNS = 2; | 253 /** @type {number} @const */ |
| 254 | 254 var MIN_NUM_COLUMNS = 2; |
| 255 | 255 |
| 256 /** @type {number} @const */ | 256 |
| 257 var MAX_NUM_COLUMNS = 4; | 257 /** @type {number} @const */ |
| 258 | 258 var MAX_NUM_COLUMNS = 4; |
| 259 | 259 |
| 260 /** @type {number} @const */ | 260 |
| 261 var NUM_ROWS = 2; | 261 /** @type {number} @const */ |
| 262 | 262 var NUM_ROWS = 2; |
| 263 | 263 |
| 264 /** | 264 |
| 265 * Minimum total padding to give to the left and right of the most visited | 265 /** |
| 266 * section. Used to determine how many tiles to show. | 266 * Minimum total padding to give to the left and right of the most visited |
| 267 * @type {number} | 267 * section. Used to determine how many tiles to show. |
| 268 * @const | 268 * @type {number} |
| 269 */ | 269 * @const |
| 270 var MIN_TOTAL_HORIZONTAL_PADDING = 200; | 270 */ |
| 271 | 271 var MIN_TOTAL_HORIZONTAL_PADDING = 200; |
| 272 | 272 |
| 273 /** | 273 |
| 274 /** |
| 274 * Heuristic to determine whether a theme should be considered to be dark, so | 275 * Heuristic to determine whether a theme should be considered to be dark, so |
| 275 * the colors of various UI elements can be adjusted. | 276 * the colors of various UI elements can be adjusted. |
| 276 * @param {ThemeBackgroundInfo|undefined} info Theme background information. | 277 * @param {ThemeBackgroundInfo|undefined} info Theme background information. |
| 277 * @return {boolean} Whether the theme is dark. | 278 * @return {boolean} Whether the theme is dark. |
| 278 * @private | 279 * @private |
| 279 */ | 280 */ |
| 280 function getIsThemeDark(info) { | 281 function getIsThemeDark(info) { |
| 281 if (!info) | 282 if (!info) |
| 282 return false; | 283 return false; |
| 283 // Heuristic: light text implies dark theme. | 284 // Heuristic: light text implies dark theme. |
| 284 var rgba = info.textColorRgba; | 285 var rgba = info.textColorRgba; |
| 285 var luminance = 0.3 * rgba[0] + 0.59 * rgba[1] + 0.11 * rgba[2]; | 286 var luminance = 0.3 * rgba[0] + 0.59 * rgba[1] + 0.11 * rgba[2]; |
| 286 return luminance >= 128; | 287 return luminance >= 128; |
| 287 } | 288 } |
| 288 | 289 |
| 289 | 290 |
| 290 /** | 291 /** |
| 291 * Updates the NTP based on the current theme. | 292 * Updates the NTP based on the current theme. |
| 292 * @private | 293 * @private |
| 293 */ | 294 */ |
| 294 function renderTheme() { | 295 function renderTheme() { |
| 295 var fakeboxText = $(IDS.FAKEBOX_TEXT); | 296 var fakeboxText = $(IDS.FAKEBOX_TEXT); |
| 296 if (fakeboxText) { | 297 if (fakeboxText) { |
| 297 fakeboxText.innerHTML = ''; | 298 fakeboxText.innerHTML = ''; |
| 298 if (configData.translatedStrings.searchboxPlaceholder) { | 299 if (configData.translatedStrings.searchboxPlaceholder) { |
| 299 fakeboxText.textContent = | 300 fakeboxText.textContent = |
| 300 configData.translatedStrings.searchboxPlaceholder; | 301 configData.translatedStrings.searchboxPlaceholder; |
| 301 } | 302 } |
| 302 } | 303 } |
| 303 | 304 |
| 304 var info = ntpApiHandle.themeBackgroundInfo; | 305 var info = ntpApiHandle.themeBackgroundInfo; |
| 305 var isThemeDark = getIsThemeDark(info); | 306 var isThemeDark = getIsThemeDark(info); |
| 306 ntpContents.classList.toggle(CLASSES.DARK, isThemeDark); | 307 ntpContents.classList.toggle(CLASSES.DARK, isThemeDark); |
| 307 if (!info) { | 308 if (!info) { |
| 308 return; | 309 return; |
| 309 } | 310 } |
| 310 | 311 |
| 311 var background = [convertToRGBAColor(info.backgroundColorRgba), | 312 var background = [ |
| 312 info.imageUrl, | 313 convertToRGBAColor(info.backgroundColorRgba), info.imageUrl, |
| 313 info.imageTiling, | 314 info.imageTiling, info.imageHorizontalAlignment, |
| 314 info.imageHorizontalAlignment, | 315 info.imageVerticalAlignment |
| 315 info.imageVerticalAlignment].join(' ').trim(); | 316 ].join(' ').trim(); |
| 316 | 317 |
| 317 document.body.style.background = background; | 318 document.body.style.background = background; |
| 318 document.body.classList.toggle(CLASSES.ALTERNATE_LOGO, info.alternateLogo); | 319 document.body.classList.toggle(CLASSES.ALTERNATE_LOGO, info.alternateLogo); |
| 319 updateThemeAttribution(info.attributionUrl, info.imageHorizontalAlignment); | 320 updateThemeAttribution(info.attributionUrl, info.imageHorizontalAlignment); |
| 320 setCustomThemeStyle(info); | 321 setCustomThemeStyle(info); |
| 321 | 322 |
| 322 var themeinfo = {cmd: 'updateTheme'}; | 323 var themeinfo = {cmd: 'updateTheme'}; |
| 323 if (!info.usingDefaultTheme) { | 324 if (!info.usingDefaultTheme) { |
| 324 themeinfo.tileBorderColor = convertToRGBAColor(info.sectionBorderColorRgba); | 325 themeinfo.tileBorderColor = |
| 325 themeinfo.tileHoverBorderColor = convertToRGBAColor(info.headerColorRgba); | 326 convertToRGBAColor(info.sectionBorderColorRgba); |
| 326 } | 327 themeinfo.tileHoverBorderColor = convertToRGBAColor(info.headerColorRgba); |
| 327 themeinfo.isThemeDark = isThemeDark; | 328 } |
| 328 | 329 themeinfo.isThemeDark = isThemeDark; |
| 329 var titleColor = NTP_DESIGN.titleColor; | 330 |
| 330 if (!info.usingDefaultTheme && info.textColorRgba) { | 331 var titleColor = NTP_DESIGN.titleColor; |
| 331 titleColor = info.textColorRgba; | 332 if (!info.usingDefaultTheme && info.textColorRgba) { |
| 332 } else if (isThemeDark) { | 333 titleColor = info.textColorRgba; |
| 333 titleColor = NTP_DESIGN.titleColorAgainstDark; | 334 } else if (isThemeDark) { |
| 334 } | 335 titleColor = NTP_DESIGN.titleColorAgainstDark; |
| 335 themeinfo.tileTitleColor = convertToRGBAColor(titleColor); | 336 } |
| 336 | 337 themeinfo.tileTitleColor = convertToRGBAColor(titleColor); |
| 337 $('mv-single').contentWindow.postMessage(themeinfo, '*'); | 338 |
| 338 } | 339 $('mv-single').contentWindow.postMessage(themeinfo, '*'); |
| 339 | 340 } |
| 340 | 341 |
| 341 /** | 342 |
| 342 * Updates the NTP based on the current theme, then rerenders all tiles. | 343 /** |
| 343 * @private | 344 * Updates the NTP based on the current theme, then rerenders all tiles. |
| 344 */ | 345 * @private |
| 345 function onThemeChange() { | 346 */ |
| 346 renderTheme(); | 347 function onThemeChange() { |
| 347 } | 348 renderTheme(); |
| 348 | 349 } |
| 349 | 350 |
| 350 /** | 351 |
| 351 * Updates the NTP style according to theme. | 352 /** |
| 352 * @param {Object=} opt_themeInfo The information about the theme. If it is | 353 * Updates the NTP style according to theme. |
| 353 * omitted the style will be reverted to the default. | 354 * @param {Object=} opt_themeInfo The information about the theme. If it is |
| 354 * @private | 355 * omitted the style will be reverted to the default. |
| 355 */ | 356 * @private |
| 356 function setCustomThemeStyle(opt_themeInfo) { | 357 */ |
| 357 var customStyleElement = $(IDS.CUSTOM_THEME_STYLE); | 358 function setCustomThemeStyle(opt_themeInfo) { |
| 358 var head = document.head; | 359 var customStyleElement = $(IDS.CUSTOM_THEME_STYLE); |
| 359 if (opt_themeInfo && !opt_themeInfo.usingDefaultTheme) { | 360 var head = document.head; |
| 360 ntpContents.classList.remove(CLASSES.DEFAULT_THEME); | 361 if (opt_themeInfo && !opt_themeInfo.usingDefaultTheme) { |
| 361 var themeStyle = | 362 ntpContents.classList.remove(CLASSES.DEFAULT_THEME); |
| 362 '#attribution {' + | 363 var themeStyle = '#attribution {' + |
| 363 ' color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + ';' + | 364 ' color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + |
| 364 '}' + | 365 ';' + |
| 365 '#mv-msg {' + | 366 '}' + |
| 366 ' color: ' + convertToRGBAColor(opt_themeInfo.textColorRgba) + ';' + | 367 '#mv-msg {' + |
| 367 '}' + | 368 ' color: ' + convertToRGBAColor(opt_themeInfo.textColorRgba) + ';' + |
| 368 '#mv-notice-links span {' + | 369 '}' + |
| 369 ' color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + ';' + | 370 '#mv-notice-links span {' + |
| 370 '}' + | 371 ' color: ' + convertToRGBAColor(opt_themeInfo.textColorLightRgba) + |
| 371 '#mv-notice-x {' + | 372 ';' + |
| 372 ' -webkit-filter: drop-shadow(0 0 0 ' + | 373 '}' + |
| 374 '#mv-notice-x {' + |
| 375 ' -webkit-filter: drop-shadow(0 0 0 ' + |
| 373 convertToRGBAColor(opt_themeInfo.textColorRgba) + ');' + | 376 convertToRGBAColor(opt_themeInfo.textColorRgba) + ');' + |
| 374 '}' + | 377 '}' + |
| 375 '.mv-page-ready .mv-mask {' + | 378 '.mv-page-ready .mv-mask {' + |
| 376 ' border: 1px solid ' + | 379 ' border: 1px solid ' + |
| 377 convertToRGBAColor(opt_themeInfo.sectionBorderColorRgba) + ';' + | 380 convertToRGBAColor(opt_themeInfo.sectionBorderColorRgba) + ';' + |
| 378 '}' + | 381 '}' + |
| 379 '.mv-page-ready:hover .mv-mask, .mv-page-ready .mv-focused ~ .mv-mask {' + | 382 '.mv-page-ready:hover .mv-mask, .mv-page-ready .mv-focused ~ .mv-mask
{' + |
| 380 ' border-color: ' + | 383 ' border-color: ' + |
| 381 convertToRGBAColor(opt_themeInfo.headerColorRgba) + ';' + | 384 convertToRGBAColor(opt_themeInfo.headerColorRgba) + ';' + |
| 382 '}'; | 385 '}'; |
| 383 | 386 |
| 384 if (customStyleElement) { | 387 if (customStyleElement) { |
| 385 customStyleElement.textContent = themeStyle; | 388 customStyleElement.textContent = themeStyle; |
| 389 } else { |
| 390 customStyleElement = document.createElement('style'); |
| 391 customStyleElement.type = 'text/css'; |
| 392 customStyleElement.id = IDS.CUSTOM_THEME_STYLE; |
| 393 customStyleElement.textContent = themeStyle; |
| 394 head.appendChild(customStyleElement); |
| 395 } |
| 396 |
| 386 } else { | 397 } else { |
| 387 customStyleElement = document.createElement('style'); | 398 ntpContents.classList.add(CLASSES.DEFAULT_THEME); |
| 388 customStyleElement.type = 'text/css'; | 399 if (customStyleElement) |
| 389 customStyleElement.id = IDS.CUSTOM_THEME_STYLE; | 400 head.removeChild(customStyleElement); |
| 390 customStyleElement.textContent = themeStyle; | 401 } |
| 391 head.appendChild(customStyleElement); | 402 } |
| 392 } | 403 |
| 393 | 404 |
| 394 } else { | 405 /** |
| 395 ntpContents.classList.add(CLASSES.DEFAULT_THEME); | 406 * Renders the attribution if the URL is present, otherwise hides it. |
| 396 if (customStyleElement) | 407 * @param {string} url The URL of the attribution image, if any. |
| 397 head.removeChild(customStyleElement); | 408 * @param {string} themeBackgroundAlignment The alignment of the theme |
| 398 } | 409 * background image. This is used to compute the attribution's alignment. |
| 399 } | 410 * @private |
| 400 | 411 */ |
| 401 | 412 function updateThemeAttribution(url, themeBackgroundAlignment) { |
| 402 /** | 413 if (!url) { |
| 403 * Renders the attribution if the URL is present, otherwise hides it. | 414 setAttributionVisibility_(false); |
| 404 * @param {string} url The URL of the attribution image, if any. | 415 return; |
| 405 * @param {string} themeBackgroundAlignment The alignment of the theme | 416 } |
| 406 * background image. This is used to compute the attribution's alignment. | 417 |
| 407 * @private | 418 var attributionImage = attribution.querySelector('img'); |
| 408 */ | 419 if (!attributionImage) { |
| 409 function updateThemeAttribution(url, themeBackgroundAlignment) { | 420 attributionImage = new Image(); |
| 410 if (!url) { | 421 attribution.appendChild(attributionImage); |
| 411 setAttributionVisibility_(false); | 422 } |
| 412 return; | 423 attributionImage.style.content = url; |
| 413 } | 424 |
| 414 | 425 // To avoid conflicts, place the attribution on the left for themes that |
| 415 var attributionImage = attribution.querySelector('img'); | 426 // right align their background images. |
| 416 if (!attributionImage) { | 427 attribution.classList.toggle( |
| 417 attributionImage = new Image(); | 428 CLASSES.LEFT_ALIGN_ATTRIBUTION, themeBackgroundAlignment == 'right'); |
| 418 attribution.appendChild(attributionImage); | 429 setAttributionVisibility_(true); |
| 419 } | 430 } |
| 420 attributionImage.style.content = url; | 431 |
| 421 | 432 |
| 422 // To avoid conflicts, place the attribution on the left for themes that | 433 /** |
| 423 // right align their background images. | 434 * Sets the visibility of the theme attribution. |
| 424 attribution.classList.toggle(CLASSES.LEFT_ALIGN_ATTRIBUTION, | 435 * @param {boolean} show True to show the attribution. |
| 425 themeBackgroundAlignment == 'right'); | 436 * @private |
| 426 setAttributionVisibility_(true); | 437 */ |
| 427 } | 438 function setAttributionVisibility_(show) { |
| 428 | 439 if (attribution) { |
| 429 | 440 attribution.style.display = show ? '' : 'none'; |
| 430 /** | 441 } |
| 431 * Sets the visibility of the theme attribution. | 442 } |
| 432 * @param {boolean} show True to show the attribution. | 443 |
| 433 * @private | 444 |
| 434 */ | 445 /** |
| 435 function setAttributionVisibility_(show) { | |
| 436 if (attribution) { | |
| 437 attribution.style.display = show ? '' : 'none'; | |
| 438 } | |
| 439 } | |
| 440 | |
| 441 | |
| 442 /** | |
| 443 * Converts an Array of color components into RRGGBBAA format. | 446 * Converts an Array of color components into RRGGBBAA format. |
| 444 * @param {Array<number>} color Array of rgba color components. | 447 * @param {Array<number>} color Array of rgba color components. |
| 445 * @return {string} Color string in RRGGBBAA format. | 448 * @return {string} Color string in RRGGBBAA format. |
| 446 * @private | 449 * @private |
| 447 */ | 450 */ |
| 448 function convertToRRGGBBAAColor(color) { | 451 function convertToRRGGBBAAColor(color) { |
| 449 return color.map(function(t) { | 452 return color |
| 450 return ('0' + t.toString(16)).slice(-2); // To 2-digit, 0-padded hex. | 453 .map(function(t) { |
| 451 }).join(''); | 454 return ('0' + t.toString(16)).slice(-2); // To 2-digit, 0-padded hex. |
| 452 } | 455 }) |
| 453 | 456 .join(''); |
| 454 | 457 } |
| 455 /** | 458 |
| 459 |
| 460 /** |
| 456 * Converts an Array of color components into RGBA format "rgba(R,G,B,A)". | 461 * Converts an Array of color components into RGBA format "rgba(R,G,B,A)". |
| 457 * @param {Array<number>} color Array of rgba color components. | 462 * @param {Array<number>} color Array of rgba color components. |
| 458 * @return {string} CSS color in RGBA format. | 463 * @return {string} CSS color in RGBA format. |
| 459 * @private | 464 * @private |
| 460 */ | 465 */ |
| 461 function convertToRGBAColor(color) { | 466 function convertToRGBAColor(color) { |
| 462 return 'rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',' + | 467 return 'rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',' + |
| 463 color[3] / 255 + ')'; | 468 color[3] / 255 + ')'; |
| 464 } | 469 } |
| 465 | 470 |
| 466 | 471 |
| 467 /** | 472 /** |
| 468 * Called when page data change. | 473 * Called when page data change. |
| 469 */ | 474 */ |
| 470 function onMostVisitedChange() { | 475 function onMostVisitedChange() { |
| 471 reloadTiles(); | 476 reloadTiles(); |
| 472 } | 477 } |
| 473 | 478 |
| 474 | 479 |
| 475 /** | 480 /** |
| 476 * Fetches new data, creates, and renders tiles. | 481 * Fetches new data, creates, and renders tiles. |
| 477 */ | 482 */ |
| 478 function reloadTiles() { | 483 function reloadTiles() { |
| 479 var pages = ntpApiHandle.mostVisited; | 484 var pages = ntpApiHandle.mostVisited; |
| 480 var cmds = []; | 485 var cmds = []; |
| 481 for (var i = 0; i < Math.min(MAX_NUM_TILES_TO_SHOW, pages.length); ++i) { | 486 for (var i = 0; i < Math.min(MAX_NUM_TILES_TO_SHOW, pages.length); ++i) { |
| 482 cmds.push({cmd: 'tile', rid: pages[i].rid}); | 487 cmds.push({cmd: 'tile', rid: pages[i].rid}); |
| 483 } | 488 } |
| 484 cmds.push({cmd: 'show', maxVisible: numColumnsShown * NUM_ROWS}); | 489 cmds.push({cmd: 'show', maxVisible: numColumnsShown * NUM_ROWS}); |
| 485 | 490 |
| 486 $('mv-single').contentWindow.postMessage(cmds, '*'); | 491 $('mv-single').contentWindow.postMessage(cmds, '*'); |
| 487 } | 492 } |
| 488 | 493 |
| 489 | 494 |
| 490 /** | 495 /** |
| 491 * Shows the blacklist notification and triggers a delay to hide it. | 496 * Shows the blacklist notification and triggers a delay to hide it. |
| 492 */ | 497 */ |
| 493 function showNotification() { | 498 function showNotification() { |
| 494 notification.classList.remove(CLASSES.HIDE_NOTIFICATION); | 499 notification.classList.remove(CLASSES.HIDE_NOTIFICATION); |
| 495 notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION); | 500 notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION); |
| 496 notification.scrollTop; | 501 notification.scrollTop; |
| 497 notification.classList.add(CLASSES.DELAYED_HIDE_NOTIFICATION); | 502 notification.classList.add(CLASSES.DELAYED_HIDE_NOTIFICATION); |
| 498 } | 503 } |
| 499 | 504 |
| 500 | 505 |
| 501 /** | 506 /** |
| 502 * Hides the blacklist notification. | 507 * Hides the blacklist notification. |
| 503 */ | 508 */ |
| 504 function hideNotification() { | 509 function hideNotification() { |
| 505 notification.classList.add(CLASSES.HIDE_NOTIFICATION); | 510 notification.classList.add(CLASSES.HIDE_NOTIFICATION); |
| 506 notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION); | 511 notification.classList.remove(CLASSES.DELAYED_HIDE_NOTIFICATION); |
| 507 } | 512 } |
| 508 | 513 |
| 509 | 514 |
| 510 /** | 515 /** |
| 511 * Handles a click on the notification undo link by hiding the notification and | 516 * Handles a click on the notification undo link by hiding the notification |
| 512 * informing Chrome. | 517 * and |
| 513 */ | 518 * informing Chrome. |
| 514 function onUndo() { | 519 */ |
| 515 hideNotification(); | 520 function onUndo() { |
| 516 if (lastBlacklistedTile != null) { | 521 hideNotification(); |
| 517 ntpApiHandle.undoMostVisitedDeletion(lastBlacklistedTile); | 522 if (lastBlacklistedTile != null) { |
| 518 } | 523 ntpApiHandle.undoMostVisitedDeletion(lastBlacklistedTile); |
| 519 } | 524 } |
| 520 | 525 } |
| 521 | 526 |
| 522 /** | 527 |
| 523 * Handles a click on the restore all notification link by hiding the | 528 /** |
| 524 * notification and informing Chrome. | 529 * Handles a click on the restore all notification link by hiding the |
| 525 */ | 530 * notification and informing Chrome. |
| 526 function onRestoreAll() { | 531 */ |
| 527 hideNotification(); | 532 function onRestoreAll() { |
| 528 ntpApiHandle.undoAllMostVisitedDeletions(); | 533 hideNotification(); |
| 529 } | 534 ntpApiHandle.undoAllMostVisitedDeletions(); |
| 530 | 535 } |
| 531 | 536 |
| 532 /** | 537 |
| 538 /** |
| 533 * Recomputes the number of tile columns, and width of various contents based | 539 * Recomputes the number of tile columns, and width of various contents based |
| 534 * on the width of the window. | 540 * on the width of the window. |
| 535 * @return {boolean} Whether the number of tile columns has changed. | 541 * @return {boolean} Whether the number of tile columns has changed. |
| 536 */ | 542 */ |
| 537 function updateContentWidth() { | 543 function updateContentWidth() { |
| 538 var tileRequiredWidth = NTP_DESIGN.tileWidth + NTP_DESIGN.tileMargin; | 544 var tileRequiredWidth = NTP_DESIGN.tileWidth + NTP_DESIGN.tileMargin; |
| 539 // If innerWidth is zero, then use the maximum snap size. | 545 // If innerWidth is zero, then use the maximum snap size. |
| 540 var maxSnapSize = MAX_NUM_COLUMNS * tileRequiredWidth - | 546 var maxSnapSize = MAX_NUM_COLUMNS * tileRequiredWidth - |
| 541 NTP_DESIGN.tileMargin + MIN_TOTAL_HORIZONTAL_PADDING; | 547 NTP_DESIGN.tileMargin + MIN_TOTAL_HORIZONTAL_PADDING; |
| 542 var innerWidth = window.innerWidth || maxSnapSize; | 548 var innerWidth = window.innerWidth || maxSnapSize; |
| 543 // Each tile has left and right margins that sum to NTP_DESIGN.tileMargin. | 549 // Each tile has left and right margins that sum to NTP_DESIGN.tileMargin. |
| 544 var availableWidth = innerWidth + NTP_DESIGN.tileMargin - | 550 var availableWidth = innerWidth + NTP_DESIGN.tileMargin - |
| 545 NTP_DESIGN.fakeboxWingSize * 2 - MIN_TOTAL_HORIZONTAL_PADDING; | 551 NTP_DESIGN.fakeboxWingSize * 2 - MIN_TOTAL_HORIZONTAL_PADDING; |
| 546 var newNumColumns = Math.floor(availableWidth / tileRequiredWidth); | 552 var newNumColumns = Math.floor(availableWidth / tileRequiredWidth); |
| 547 if (newNumColumns < MIN_NUM_COLUMNS) | 553 if (newNumColumns < MIN_NUM_COLUMNS) |
| 548 newNumColumns = MIN_NUM_COLUMNS; | 554 newNumColumns = MIN_NUM_COLUMNS; |
| 549 else if (newNumColumns > MAX_NUM_COLUMNS) | 555 else if (newNumColumns > MAX_NUM_COLUMNS) |
| 550 newNumColumns = MAX_NUM_COLUMNS; | 556 newNumColumns = MAX_NUM_COLUMNS; |
| 551 | 557 |
| 552 if (numColumnsShown === newNumColumns) | 558 if (numColumnsShown === newNumColumns) |
| 553 return false; | 559 return false; |
| 554 | 560 |
| 555 numColumnsShown = newNumColumns; | 561 numColumnsShown = newNumColumns; |
| 556 // We add an extra pixel because rounding errors on different zooms can | 562 // We add an extra pixel because rounding errors on different zooms can |
| 557 // make the width shorter than it should be. | 563 // make the width shorter than it should be. |
| 558 var tilesContainerWidth = Math.ceil(numColumnsShown * tileRequiredWidth) + 1; | 564 var tilesContainerWidth = |
| 559 $(IDS.TILES).style.width = tilesContainerWidth + 'px'; | 565 Math.ceil(numColumnsShown * tileRequiredWidth) + 1; |
| 560 if (fakebox) { | 566 $(IDS.TILES).style.width = tilesContainerWidth + 'px'; |
| 561 // -2 to account for border. | 567 if (fakebox) { |
| 562 var fakeboxWidth = (tilesContainerWidth - NTP_DESIGN.tileMargin - 2); | 568 // -2 to account for border. |
| 563 fakeboxWidth += NTP_DESIGN.fakeboxWingSize * 2; | 569 var fakeboxWidth = (tilesContainerWidth - NTP_DESIGN.tileMargin - 2); |
| 564 fakebox.style.width = fakeboxWidth + 'px'; | 570 fakeboxWidth += NTP_DESIGN.fakeboxWingSize * 2; |
| 565 } | 571 fakebox.style.width = fakeboxWidth + 'px'; |
| 566 return true; | 572 } |
| 567 } | 573 return true; |
| 568 | 574 } |
| 569 | 575 |
| 570 /** | 576 |
| 571 * Resizes elements because the number of tile columns may need to change in | 577 /** |
| 572 * response to resizing. Also shows or hides extra tiles tiles according to the | 578 * Resizes elements because the number of tile columns may need to change in |
| 573 * new width of the page. | 579 * response to resizing. Also shows or hides extra tiles tiles according to |
| 574 */ | 580 * the |
| 575 function onResize() { | 581 * new width of the page. |
| 576 updateContentWidth(); | 582 */ |
| 577 $('mv-single').contentWindow.postMessage( | 583 function onResize() { |
| 578 {cmd: 'tilesVisible', maxVisible: numColumnsShown * NUM_ROWS}, '*'); | 584 updateContentWidth(); |
| 579 } | 585 $('mv-single') |
| 580 | 586 .contentWindow.postMessage( |
| 581 | 587 {cmd: 'tilesVisible', maxVisible: numColumnsShown * NUM_ROWS}, '*'); |
| 582 /** | 588 } |
| 583 * Handles new input by disposing the NTP, according to where the input was | 589 |
| 584 * entered. | 590 |
| 585 */ | 591 /** |
| 586 function onInputStart() { | 592 * Handles new input by disposing the NTP, according to where the input was |
| 587 if (fakebox && isFakeboxFocused()) { | 593 * entered. |
| 588 setFakeboxFocus(false); | 594 */ |
| 589 setFakeboxDragFocus(false); | 595 function onInputStart() { |
| 590 disposeNtp(true); | 596 if (fakebox && isFakeboxFocused()) { |
| 591 } else if (!isFakeboxFocused()) { | 597 setFakeboxFocus(false); |
| 592 disposeNtp(false); | 598 setFakeboxDragFocus(false); |
| 593 } | 599 disposeNtp(true); |
| 594 } | 600 } else if (!isFakeboxFocused()) { |
| 595 | 601 disposeNtp(false); |
| 596 | 602 } |
| 597 /** | 603 } |
| 598 * Disposes the NTP, according to where the input was entered. | 604 |
| 599 * @param {boolean} wasFakeboxInput True if the input was in the fakebox. | 605 |
| 600 */ | 606 /** |
| 601 function disposeNtp(wasFakeboxInput) { | 607 * Disposes the NTP, according to where the input was entered. |
| 602 var behavior = wasFakeboxInput ? fakeboxInputBehavior : omniboxInputBehavior; | 608 * @param {boolean} wasFakeboxInput True if the input was in the fakebox. |
| 603 if (behavior == NTP_DISPOSE_STATE.DISABLE_FAKEBOX) | 609 */ |
| 604 setFakeboxActive(false); | 610 function disposeNtp(wasFakeboxInput) { |
| 605 else if (behavior == NTP_DISPOSE_STATE.HIDE_FAKEBOX_AND_LOGO) | 611 var behavior = |
| 606 setFakeboxAndLogoVisibility(false); | 612 wasFakeboxInput ? fakeboxInputBehavior : omniboxInputBehavior; |
| 607 } | 613 if (behavior == NTP_DISPOSE_STATE.DISABLE_FAKEBOX) |
| 608 | 614 setFakeboxActive(false); |
| 609 | 615 else if (behavior == NTP_DISPOSE_STATE.HIDE_FAKEBOX_AND_LOGO) |
| 610 /** | 616 setFakeboxAndLogoVisibility(false); |
| 611 * Restores the NTP (re-enables the fakebox and unhides the logo.) | 617 } |
| 612 */ | 618 |
| 613 function restoreNtp() { | 619 |
| 614 setFakeboxActive(true); | 620 /** |
| 615 setFakeboxAndLogoVisibility(true); | 621 * Restores the NTP (re-enables the fakebox and unhides the logo.) |
| 616 } | 622 */ |
| 617 | 623 function restoreNtp() { |
| 618 | 624 setFakeboxActive(true); |
| 619 /** | 625 setFakeboxAndLogoVisibility(true); |
| 620 * @param {boolean} focus True to focus the fakebox. | 626 } |
| 621 */ | 627 |
| 622 function setFakeboxFocus(focus) { | 628 |
| 623 document.body.classList.toggle(CLASSES.FAKEBOX_FOCUS, focus); | 629 /** |
| 624 } | 630 * @param {boolean} focus True to focus the fakebox. |
| 625 | 631 */ |
| 626 /** | 632 function setFakeboxFocus(focus) { |
| 627 * @param {boolean} focus True to show a dragging focus to the fakebox. | 633 document.body.classList.toggle(CLASSES.FAKEBOX_FOCUS, focus); |
| 628 */ | 634 } |
| 629 function setFakeboxDragFocus(focus) { | 635 |
| 630 document.body.classList.toggle(CLASSES.FAKEBOX_DRAG_FOCUS, focus); | 636 /** |
| 631 } | 637 * @param {boolean} focus True to show a dragging focus to the fakebox. |
| 632 | 638 */ |
| 633 /** | 639 function setFakeboxDragFocus(focus) { |
| 640 document.body.classList.toggle(CLASSES.FAKEBOX_DRAG_FOCUS, focus); |
| 641 } |
| 642 |
| 643 /** |
| 634 * @return {boolean} True if the fakebox has focus. | 644 * @return {boolean} True if the fakebox has focus. |
| 635 */ | 645 */ |
| 636 function isFakeboxFocused() { | 646 function isFakeboxFocused() { |
| 637 return document.body.classList.contains(CLASSES.FAKEBOX_FOCUS) || | 647 return document.body.classList.contains(CLASSES.FAKEBOX_FOCUS) || |
| 638 document.body.classList.contains(CLASSES.FAKEBOX_DRAG_FOCUS); | 648 document.body.classList.contains(CLASSES.FAKEBOX_DRAG_FOCUS); |
| 639 } | 649 } |
| 640 | 650 |
| 641 | 651 |
| 642 /** | 652 /** |
| 643 * @param {boolean} enable True to enable the fakebox. | 653 * @param {boolean} enable True to enable the fakebox. |
| 644 */ | 654 */ |
| 645 function setFakeboxActive(enable) { | 655 function setFakeboxActive(enable) { |
| 646 document.body.classList.toggle(CLASSES.FAKEBOX_DISABLE, !enable); | 656 document.body.classList.toggle(CLASSES.FAKEBOX_DISABLE, !enable); |
| 647 } | 657 } |
| 648 | 658 |
| 649 | 659 |
| 650 /** | 660 /** |
| 651 * @param {!Event} event The click event. | 661 * @param {!Event} event The click event. |
| 652 * @return {boolean} True if the click occurred in an enabled fakebox. | 662 * @return {boolean} True if the click occurred in an enabled fakebox. |
| 653 */ | 663 */ |
| 654 function isFakeboxClick(event) { | 664 function isFakeboxClick(event) { |
| 655 return fakebox.contains(event.target) && | 665 return fakebox.contains(event.target) && |
| 656 !document.body.classList.contains(CLASSES.FAKEBOX_DISABLE); | 666 !document.body.classList.contains(CLASSES.FAKEBOX_DISABLE); |
| 657 } | 667 } |
| 658 | 668 |
| 659 | 669 |
| 660 /** | 670 /** |
| 661 * @param {boolean} show True to show the fakebox and logo. | 671 * @param {boolean} show True to show the fakebox and logo. |
| 662 */ | 672 */ |
| 663 function setFakeboxAndLogoVisibility(show) { | 673 function setFakeboxAndLogoVisibility(show) { |
| 664 document.body.classList.toggle(CLASSES.HIDE_FAKEBOX_AND_LOGO, !show); | 674 document.body.classList.toggle(CLASSES.HIDE_FAKEBOX_AND_LOGO, !show); |
| 665 } | 675 } |
| 666 | 676 |
| 667 | 677 |
| 668 /** | 678 /** |
| 669 * Shortcut for document.getElementById. | 679 * Shortcut for document.getElementById. |
| 670 * @param {string} id of the element. | 680 * @param {string} id of the element. |
| 671 * @return {HTMLElement} with the id. | 681 * @return {HTMLElement} with the id. |
| 672 */ | 682 */ |
| 673 function $(id) { | 683 function $(id) { |
| 674 return document.getElementById(id); | 684 return document.getElementById(id); |
| 675 } | 685 } |
| 676 | 686 |
| 677 | 687 |
| 678 /** | 688 /** |
| 679 * Utility function which creates an element with an optional classname and | 689 * Utility function which creates an element with an optional classname and |
| 680 * appends it to the specified parent. | 690 * appends it to the specified parent. |
| 681 * @param {Element} parent The parent to append the new element. | 691 * @param {Element} parent The parent to append the new element. |
| 682 * @param {string} name The name of the new element. | 692 * @param {string} name The name of the new element. |
| 683 * @param {string=} opt_class The optional classname of the new element. | 693 * @param {string=} opt_class The optional classname of the new element. |
| 684 * @return {Element} The new element. | 694 * @return {Element} The new element. |
| 685 */ | 695 */ |
| 686 function createAndAppendElement(parent, name, opt_class) { | 696 function createAndAppendElement(parent, name, opt_class) { |
| 687 var child = document.createElement(name); | 697 var child = document.createElement(name); |
| 688 if (opt_class) | 698 if (opt_class) |
| 689 child.classList.add(opt_class); | 699 child.classList.add(opt_class); |
| 690 parent.appendChild(child); | 700 parent.appendChild(child); |
| 691 return child; | 701 return child; |
| 692 } | 702 } |
| 693 | 703 |
| 694 | 704 |
| 695 /** | 705 /** |
| 696 * @param {!Element} element The element to register the handler for. | 706 * @param {!Element} element The element to register the handler for. |
| 697 * @param {number} keycode The keycode of the key to register. | 707 * @param {number} keycode The keycode of the key to register. |
| 698 * @param {!Function} handler The key handler to register. | 708 * @param {!Function} handler The key handler to register. |
| 699 */ | 709 */ |
| 700 function registerKeyHandler(element, keycode, handler) { | 710 function registerKeyHandler(element, keycode, handler) { |
| 701 element.addEventListener('keydown', function(event) { | 711 element.addEventListener('keydown', function(event) { |
| 702 if (event.keyCode == keycode) | 712 if (event.keyCode == keycode) |
| 703 handler(event); | 713 handler(event); |
| 704 }); | 714 }); |
| 705 } | 715 } |
| 706 | 716 |
| 707 | 717 |
| 708 /** | 718 /** |
| 709 * @return {Object} the handle to the embeddedSearch API. | 719 * @return {Object} the handle to the embeddedSearch API. |
| 710 */ | 720 */ |
| 711 function getEmbeddedSearchApiHandle() { | 721 function getEmbeddedSearchApiHandle() { |
| 712 if (window.cideb) | 722 if (window.cideb) |
| 713 return window.cideb; | 723 return window.cideb; |
| 714 if (window.chrome && window.chrome.embeddedSearch) | 724 if (window.chrome && window.chrome.embeddedSearch) |
| 715 return window.chrome.embeddedSearch; | 725 return window.chrome.embeddedSearch; |
| 716 return null; | 726 return null; |
| 727 } |
| 728 |
| 729 |
| 730 /** |
| 731 * Event handler for the focus changed and blacklist messages on link |
| 732 * elements. |
| 733 * Used to toggle visual treatment on the tiles (depending on the message). |
| 734 * @param {Event} event Event received. |
| 735 */ |
| 736 function handlePostMessage(event) { |
| 737 var cmd = event.data.cmd; |
| 738 var args = event.data; |
| 739 if (cmd == 'tileBlacklisted') { |
| 740 showNotification(); |
| 741 lastBlacklistedTile = args.tid; |
| 742 |
| 743 ntpApiHandle.deleteMostVisitedItem(args.tid); |
| 744 } |
| 745 } |
| 746 |
| 747 |
| 748 /** |
| 749 * Prepares the New Tab Page by adding listeners, rendering the current |
| 750 * theme, the most visited pages section, and Google-specific elements for a |
| 751 * Google-provided page. |
| 752 */ |
| 753 function init() { |
| 754 notification = $(IDS.NOTIFICATION); |
| 755 attribution = $(IDS.ATTRIBUTION); |
| 756 ntpContents = $(IDS.NTP_CONTENTS); |
| 757 |
| 758 if (configData.isGooglePage) { |
| 759 var logo = document.createElement('div'); |
| 760 logo.id = IDS.LOGO; |
| 761 logo.title = 'Google'; |
| 762 |
| 763 fakebox = document.createElement('div'); |
| 764 fakebox.id = IDS.FAKEBOX; |
| 765 var fakeboxHtml = []; |
| 766 fakeboxHtml.push('<div id="' + IDS.FAKEBOX_TEXT + '"></div>'); |
| 767 fakeboxHtml.push( |
| 768 '<input id="' + IDS.FAKEBOX_INPUT + |
| 769 '" autocomplete="off" tabindex="-1" type="url" aria-hidden="true">'); |
| 770 fakeboxHtml.push('<div id="cursor"></div>'); |
| 771 fakebox.innerHTML = fakeboxHtml.join(''); |
| 772 |
| 773 ntpContents.insertBefore(fakebox, ntpContents.firstChild); |
| 774 ntpContents.insertBefore(logo, ntpContents.firstChild); |
| 775 } else { |
| 776 document.body.classList.add(CLASSES.NON_GOOGLE_PAGE); |
| 777 } |
| 778 |
| 779 // Modify design for experimental icon NTP, if specified. |
| 780 if (configData.useIcons) |
| 781 modifyNtpDesignForIcons(); |
| 782 document.querySelector('#ntp-contents').classList.add(NTP_DESIGN.mainClass); |
| 783 |
| 784 // Hide notifications after fade out, so we can't focus on links via |
| 785 // keyboard. |
| 786 notification.addEventListener('webkitTransitionEnd', hideNotification); |
| 787 |
| 788 var notificationMessage = $(IDS.NOTIFICATION_MESSAGE); |
| 789 notificationMessage.textContent = |
| 790 configData.translatedStrings.thumbnailRemovedNotification; |
| 791 |
| 792 var undoLink = $(IDS.UNDO_LINK); |
| 793 undoLink.addEventListener('click', onUndo); |
| 794 registerKeyHandler(undoLink, KEYCODE.ENTER, onUndo); |
| 795 undoLink.textContent = configData.translatedStrings.undoThumbnailRemove; |
| 796 |
| 797 var restoreAllLink = $(IDS.RESTORE_ALL_LINK); |
| 798 restoreAllLink.addEventListener('click', onRestoreAll); |
| 799 registerKeyHandler(restoreAllLink, KEYCODE.ENTER, onUndo); |
| 800 restoreAllLink.textContent = |
| 801 configData.translatedStrings.restoreThumbnailsShort; |
| 802 |
| 803 $(IDS.ATTRIBUTION_TEXT).textContent = |
| 804 configData.translatedStrings.attributionIntro; |
| 805 |
| 806 var notificationCloseButton = $(IDS.NOTIFICATION_CLOSE_BUTTON); |
| 807 createAndAppendElement( |
| 808 notificationCloseButton, 'div', CLASSES.BLACKLIST_BUTTON_INNER); |
| 809 notificationCloseButton.addEventListener('click', hideNotification); |
| 810 |
| 811 window.addEventListener('resize', onResize); |
| 812 updateContentWidth(); |
| 813 |
| 814 var topLevelHandle = getEmbeddedSearchApiHandle(); |
| 815 |
| 816 ntpApiHandle = topLevelHandle.newTabPage; |
| 817 ntpApiHandle.onthemechange = onThemeChange; |
| 818 ntpApiHandle.onmostvisitedchange = onMostVisitedChange; |
| 819 |
| 820 ntpApiHandle.oninputstart = onInputStart; |
| 821 ntpApiHandle.oninputcancel = restoreNtp; |
| 822 |
| 823 if (ntpApiHandle.isInputInProgress) |
| 824 onInputStart(); |
| 825 |
| 826 searchboxApiHandle = topLevelHandle.searchBox; |
| 827 |
| 828 if (fakebox) { |
| 829 // Listener for updating the key capture state. |
| 830 document.body.onmousedown = function(event) { |
| 831 if (isFakeboxClick(event)) |
| 832 searchboxApiHandle.startCapturingKeyStrokes(); |
| 833 else if (isFakeboxFocused()) |
| 834 searchboxApiHandle.stopCapturingKeyStrokes(); |
| 835 }; |
| 836 searchboxApiHandle.onkeycapturechange = function() { |
| 837 setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled); |
| 838 }; |
| 839 var inputbox = $(IDS.FAKEBOX_INPUT); |
| 840 if (inputbox) { |
| 841 inputbox.onpaste = function(event) { |
| 842 event.preventDefault(); |
| 843 // Send pasted text to Omnibox. |
| 844 var text = event.clipboardData.getData('text/plain'); |
| 845 if (text) |
| 846 searchboxApiHandle.paste(text); |
| 847 }; |
| 848 inputbox.ondrop = function(event) { |
| 849 event.preventDefault(); |
| 850 var text = event.dataTransfer.getData('text/plain'); |
| 851 if (text) { |
| 852 searchboxApiHandle.paste(text); |
| 853 } |
| 854 setFakeboxDragFocus(false); |
| 855 }; |
| 856 inputbox.ondragenter = function() { |
| 857 setFakeboxDragFocus(true); |
| 858 }; |
| 859 inputbox.ondragleave = function() { |
| 860 setFakeboxDragFocus(false); |
| 861 }; |
| 862 } |
| 863 |
| 864 // Update the fakebox style to match the current key capturing state. |
| 865 setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled); |
| 866 } |
| 867 |
| 868 if (searchboxApiHandle.rtl) { |
| 869 $(IDS.NOTIFICATION).dir = 'rtl'; |
| 870 // Grabbing the root HTML element. |
| 871 document.documentElement.setAttribute('dir', 'rtl'); |
| 872 // Add class for setting alignments based on language directionality. |
| 873 document.documentElement.classList.add(CLASSES.RTL); |
| 874 } |
| 875 |
| 876 var iframe = document.createElement('iframe'); |
| 877 // Change the order of tabbing the page to start with NTP tiles. |
| 878 iframe.setAttribute('tabindex', '1'); |
| 879 iframe.id = 'mv-single'; |
| 880 |
| 881 var args = []; |
| 882 |
| 883 if (searchboxApiHandle.rtl) |
| 884 args.push('rtl=1'); |
| 885 if (window.configData.useIcons) |
| 886 args.push('icons=1'); |
| 887 if (NTP_DESIGN.numTitleLines > 1) |
| 888 args.push('ntl=' + NTP_DESIGN.numTitleLines); |
| 889 |
| 890 args.push( |
| 891 'removeTooltip=' + |
| 892 encodeURIComponent( |
| 893 configData.translatedStrings.removeThumbnailTooltip)); |
| 894 |
| 895 iframe.src = '//most-visited/single.html?' + args.join('&'); |
| 896 $(IDS.TILES).appendChild(iframe); |
| 897 |
| 898 iframe.onload = function() { |
| 899 reloadTiles(); |
| 900 renderTheme(); |
| 901 }; |
| 902 |
| 903 window.addEventListener('message', handlePostMessage); |
| 904 } |
| 905 |
| 906 |
| 907 /** |
| 908 * Binds event listeners. |
| 909 */ |
| 910 function listen() { |
| 911 document.addEventListener('DOMContentLoaded', init); |
| 912 } |
| 913 |
| 914 return {init: init, listen: listen}; |
| 717 } | 915 } |
| 718 | 916 |
| 719 | |
| 720 /** | |
| 721 * Event handler for the focus changed and blacklist messages on link elements. | |
| 722 * Used to toggle visual treatment on the tiles (depending on the message). | |
| 723 * @param {Event} event Event received. | |
| 724 */ | |
| 725 function handlePostMessage(event) { | |
| 726 var cmd = event.data.cmd; | |
| 727 var args = event.data; | |
| 728 if (cmd == 'tileBlacklisted') { | |
| 729 showNotification(); | |
| 730 lastBlacklistedTile = args.tid; | |
| 731 | |
| 732 ntpApiHandle.deleteMostVisitedItem(args.tid); | |
| 733 } | |
| 734 } | |
| 735 | |
| 736 | |
| 737 /** | |
| 738 * Prepares the New Tab Page by adding listeners, rendering the current | |
| 739 * theme, the most visited pages section, and Google-specific elements for a | |
| 740 * Google-provided page. | |
| 741 */ | |
| 742 function init() { | |
| 743 notification = $(IDS.NOTIFICATION); | |
| 744 attribution = $(IDS.ATTRIBUTION); | |
| 745 ntpContents = $(IDS.NTP_CONTENTS); | |
| 746 | |
| 747 if (configData.isGooglePage) { | |
| 748 var logo = document.createElement('div'); | |
| 749 logo.id = IDS.LOGO; | |
| 750 logo.title = 'Google'; | |
| 751 | |
| 752 fakebox = document.createElement('div'); | |
| 753 fakebox.id = IDS.FAKEBOX; | |
| 754 var fakeboxHtml = []; | |
| 755 fakeboxHtml.push('<div id="' + IDS.FAKEBOX_TEXT + '"></div>'); | |
| 756 fakeboxHtml.push('<input id="' + IDS.FAKEBOX_INPUT + | |
| 757 '" autocomplete="off" tabindex="-1" type="url" aria-hidden="true">'); | |
| 758 fakeboxHtml.push('<div id="cursor"></div>'); | |
| 759 fakebox.innerHTML = fakeboxHtml.join(''); | |
| 760 | |
| 761 ntpContents.insertBefore(fakebox, ntpContents.firstChild); | |
| 762 ntpContents.insertBefore(logo, ntpContents.firstChild); | |
| 763 } else { | |
| 764 document.body.classList.add(CLASSES.NON_GOOGLE_PAGE); | |
| 765 } | |
| 766 | |
| 767 // Modify design for experimental icon NTP, if specified. | |
| 768 if (configData.useIcons) | |
| 769 modifyNtpDesignForIcons(); | |
| 770 document.querySelector('#ntp-contents').classList.add(NTP_DESIGN.mainClass); | |
| 771 | |
| 772 // Hide notifications after fade out, so we can't focus on links via keyboard. | |
| 773 notification.addEventListener('webkitTransitionEnd', hideNotification); | |
| 774 | |
| 775 var notificationMessage = $(IDS.NOTIFICATION_MESSAGE); | |
| 776 notificationMessage.textContent = | |
| 777 configData.translatedStrings.thumbnailRemovedNotification; | |
| 778 | |
| 779 var undoLink = $(IDS.UNDO_LINK); | |
| 780 undoLink.addEventListener('click', onUndo); | |
| 781 registerKeyHandler(undoLink, KEYCODE.ENTER, onUndo); | |
| 782 undoLink.textContent = configData.translatedStrings.undoThumbnailRemove; | |
| 783 | |
| 784 var restoreAllLink = $(IDS.RESTORE_ALL_LINK); | |
| 785 restoreAllLink.addEventListener('click', onRestoreAll); | |
| 786 registerKeyHandler(restoreAllLink, KEYCODE.ENTER, onUndo); | |
| 787 restoreAllLink.textContent = | |
| 788 configData.translatedStrings.restoreThumbnailsShort; | |
| 789 | |
| 790 $(IDS.ATTRIBUTION_TEXT).textContent = | |
| 791 configData.translatedStrings.attributionIntro; | |
| 792 | |
| 793 var notificationCloseButton = $(IDS.NOTIFICATION_CLOSE_BUTTON); | |
| 794 createAndAppendElement( | |
| 795 notificationCloseButton, 'div', CLASSES.BLACKLIST_BUTTON_INNER); | |
| 796 notificationCloseButton.addEventListener('click', hideNotification); | |
| 797 | |
| 798 window.addEventListener('resize', onResize); | |
| 799 updateContentWidth(); | |
| 800 | |
| 801 var topLevelHandle = getEmbeddedSearchApiHandle(); | |
| 802 | |
| 803 ntpApiHandle = topLevelHandle.newTabPage; | |
| 804 ntpApiHandle.onthemechange = onThemeChange; | |
| 805 ntpApiHandle.onmostvisitedchange = onMostVisitedChange; | |
| 806 | |
| 807 ntpApiHandle.oninputstart = onInputStart; | |
| 808 ntpApiHandle.oninputcancel = restoreNtp; | |
| 809 | |
| 810 if (ntpApiHandle.isInputInProgress) | |
| 811 onInputStart(); | |
| 812 | |
| 813 searchboxApiHandle = topLevelHandle.searchBox; | |
| 814 | |
| 815 if (fakebox) { | |
| 816 // Listener for updating the key capture state. | |
| 817 document.body.onmousedown = function(event) { | |
| 818 if (isFakeboxClick(event)) | |
| 819 searchboxApiHandle.startCapturingKeyStrokes(); | |
| 820 else if (isFakeboxFocused()) | |
| 821 searchboxApiHandle.stopCapturingKeyStrokes(); | |
| 822 }; | |
| 823 searchboxApiHandle.onkeycapturechange = function() { | |
| 824 setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled); | |
| 825 }; | |
| 826 var inputbox = $(IDS.FAKEBOX_INPUT); | |
| 827 if (inputbox) { | |
| 828 inputbox.onpaste = function(event) { | |
| 829 event.preventDefault(); | |
| 830 // Send pasted text to Omnibox. | |
| 831 var text = event.clipboardData.getData('text/plain'); | |
| 832 if (text) | |
| 833 searchboxApiHandle.paste(text); | |
| 834 }; | |
| 835 inputbox.ondrop = function(event) { | |
| 836 event.preventDefault(); | |
| 837 var text = event.dataTransfer.getData('text/plain'); | |
| 838 if (text) { | |
| 839 searchboxApiHandle.paste(text); | |
| 840 } | |
| 841 setFakeboxDragFocus(false); | |
| 842 }; | |
| 843 inputbox.ondragenter = function() { | |
| 844 setFakeboxDragFocus(true); | |
| 845 }; | |
| 846 inputbox.ondragleave = function() { | |
| 847 setFakeboxDragFocus(false); | |
| 848 }; | |
| 849 } | |
| 850 | |
| 851 // Update the fakebox style to match the current key capturing state. | |
| 852 setFakeboxFocus(searchboxApiHandle.isKeyCaptureEnabled); | |
| 853 } | |
| 854 | |
| 855 if (searchboxApiHandle.rtl) { | |
| 856 $(IDS.NOTIFICATION).dir = 'rtl'; | |
| 857 // Grabbing the root HTML element. | |
| 858 document.documentElement.setAttribute('dir', 'rtl'); | |
| 859 // Add class for setting alignments based on language directionality. | |
| 860 document.documentElement.classList.add(CLASSES.RTL); | |
| 861 } | |
| 862 | |
| 863 var iframe = document.createElement('iframe'); | |
| 864 // Change the order of tabbing the page to start with NTP tiles. | |
| 865 iframe.setAttribute('tabindex', '1'); | |
| 866 iframe.id = 'mv-single'; | |
| 867 | |
| 868 var args = []; | |
| 869 | |
| 870 if (searchboxApiHandle.rtl) | |
| 871 args.push('rtl=1'); | |
| 872 if (window.configData.useIcons) | |
| 873 args.push('icons=1'); | |
| 874 if (NTP_DESIGN.numTitleLines > 1) | |
| 875 args.push('ntl=' + NTP_DESIGN.numTitleLines); | |
| 876 | |
| 877 args.push('removeTooltip=' + | |
| 878 encodeURIComponent(configData.translatedStrings.removeThumbnailTooltip)); | |
| 879 | |
| 880 iframe.src = '//most-visited/single.html?' + args.join('&'); | |
| 881 $(IDS.TILES).appendChild(iframe); | |
| 882 | |
| 883 iframe.onload = function() { | |
| 884 reloadTiles(); | |
| 885 renderTheme(); | |
| 886 }; | |
| 887 | |
| 888 window.addEventListener('message', handlePostMessage); | |
| 889 } | |
| 890 | |
| 891 | |
| 892 /** | |
| 893 * Binds event listeners. | |
| 894 */ | |
| 895 function listen() { | |
| 896 document.addEventListener('DOMContentLoaded', init); | |
| 897 } | |
| 898 | |
| 899 return { | |
| 900 init: init, | |
| 901 listen: listen | |
| 902 }; | |
| 903 } | |
| 904 | |
| 905 if (!window.localNTPUnitTest) { | 917 if (!window.localNTPUnitTest) { |
| 906 LocalNTP().listen(); | 918 LocalNTP().listen(); |
| 907 } | 919 } |
| OLD | NEW |