1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 cr.define('options', function() { 6 'use strict'; 7 8 /** 9 * A lookup helper function to find the first node that has an id (starting 10 * at |node| and going up the parent chain). 11 * @param {Element} node The node to start looking at. 12 */ 13 function findIdNode(node) { 14 while (node && !node.id) { 15 node = node.parentNode; 16 } 17 return node; 18 } 19 20 /** 21 * Creates a new list of extensions. 22 * @param {Object=} opt_propertyBag Optional properties. 23 * @constructor 24 * @extends {cr.ui.div} 25 */ 26 var ExtensionsList = cr.ui.define('div'); 27 28 /** 29 * @type {Object.<string, boolean>} A map from extension id to a boolean 30 * indicating whether the incognito warning is showing. This persists 31 * between calls to decorate. 32 */ 33 var butterBarVisibility = {}; 34 35 /** 36 * @type {Object.<string, string>} A map from extension id to last reloaded 37 * timestamp. The timestamp is recorded when the user click the 'Reload' 38 * link. It is used to refresh the icon of an unpacked extension. 39 * This persists between calls to decorate. 40 */ 41 var extensionReloadedTimestamp = {}; 42 43 ExtensionsList.prototype = { 44 __proto__: HTMLDivElement.prototype, 45 46 /** @override */ 47 decorate: function() { 48 this.textContent = ''; 49 50 this.showExtensionNodes_(); 51 }, 52 53 getIdQueryParam_: function() { 54 return parseQueryParams(document.location)['id']; 55 }, 56 57 /** 58 * Creates all extension items from scratch. 59 * @private 60 */ 61 showExtensionNodes_: function() { 62 // Iterate over the extension data and add each item to the list. 63 this.data_.extensions.forEach(this.createNode_, this); 64 65 var idToHighlight = this.getIdQueryParam_(); 66 if (idToHighlight && $(idToHighlight)) { 67 // Scroll offset should be calculated slightly higher than the actual 68 // offset of the element being scrolled to, so that it ends up not all 69 // the way at the top. That way it is clear that there are more elements 70 // above the element being scrolled to. 71 var scrollFudge = 1.2; 72 var offset = $(idToHighlight).offsetTop - 73 (scrollFudge * $(idToHighlight).clientHeight); 74 var wrapper = this.parentNode; 75 var list = wrapper.parentNode; 76 list.scrollTop = offset; 77 } 78 79 if (this.data_.extensions.length == 0) 80 this.classList.add('empty-extension-list'); 81 else 82 this.classList.remove('empty-extension-list'); 83 }, 84 85 /** 86 * Synthesizes and initializes an HTML element for the extension metadata 87 * given in |extension|. 88 * @param {Object} extension A dictionary of extension metadata. 89 * @private 90 */ 91 createNode_: function(extension) { 92 var template = $('template-collection').querySelector( 93 '.extension-list-item-wrapper'); 94 var node = template.cloneNode(true); 95 node.id = extension.id; 96 97 if (!extension.enabled || extension.terminated) 98 node.classList.add('inactive-extension'); 99 100 if (!extension.userModifiable) 101 node.classList.add('may-not-disable'); 102 103 var idToHighlight = this.getIdQueryParam_(); 104 if (node.id == idToHighlight) 105 node.classList.add('extension-highlight'); 106 107 var item = node.querySelector('.extension-list-item'); 108 // Prevent the image cache of extension icon by using the reloaded 109 // timestamp as a query string. The timestamp is recorded when the user 110 // clicks the 'Reload' link. http://crbug.com/159302. 111 if (extensionReloadedTimestamp[extension.id]) { 112 item.style.backgroundImage = 113 'url(' + extension.icon + '?' + 114 extensionReloadedTimestamp[extension.id] + ')'; 115 } else { 116 item.style.backgroundImage = 'url(' + extension.icon + ')'; 117 } 118 119 var title = node.querySelector('.extension-title'); 120 title.textContent = extension.name; 121 122 var version = node.querySelector('.extension-version'); 123 version.textContent = extension.version; 124 125 var locationText = node.querySelector('.location-text'); 126 locationText.textContent = extension.locationText; 127 128 var description = node.querySelector('.extension-description span'); 129 description.textContent = extension.description; 130 131 // The 'Show Browser Action' button. 132 if (extension.enable_show_button) { 133 var showButton = node.querySelector('.show-button'); 134 showButton.addEventListener('click', function(e) { 135 chrome.send('extensionSettingsShowButton', [extension.id]); 136 }); 137 showButton.hidden = false; 138 } 139 140 // The 'allow in incognito' checkbox. 141 var incognito = node.querySelector('.incognito-control input'); 142 incognito.disabled = !extension.incognitoCanBeToggled; 143 incognito.checked = extension.enabledIncognito; 144 if (!incognito.disabled) { 145 incognito.addEventListener('change', function(e) { 146 var checked = e.target.checked; 147 butterBarVisibility[extension.id] = checked; 148 butterBar.hidden = !checked || extension.is_hosted_app; 149 chrome.send('extensionSettingsEnableIncognito', 150 [extension.id, String(checked)]); 151 }); 152 } 153 var butterBar = node.querySelector('.butter-bar'); 154 butterBar.hidden = !butterBarVisibility[extension.id]; 155 156 // The 'allow file:// access' checkbox. 157 if (extension.wantsFileAccess) { 158 var fileAccess = node.querySelector('.file-access-control'); 159 fileAccess.addEventListener('click', function(e) { 160 chrome.send('extensionSettingsAllowFileAccess', 161 [extension.id, String(e.target.checked)]); 162 }); 163 fileAccess.querySelector('input').checked = extension.allowFileAccess; 164 fileAccess.hidden = false; 165 } 166 167 // The 'Options' link. 168 if (extension.enabled && extension.optionsUrl) { 169 var options = node.querySelector('.options-link'); 170 options.addEventListener('click', function(e) { 171 chrome.send('extensionSettingsOptions', [extension.id]); 172 e.preventDefault(); 173 }); 174 options.hidden = false; 175 } 176 177 // The 'Permissions' link. 178 var permissions = node.querySelector('.permissions-link'); 179 permissions.addEventListener('click', function(e) { 180 chrome.send('extensionSettingsPermissions', [extension.id]); 181 e.preventDefault(); 182 }); 183 184 // The 'View in Web Store/View Web Site' link. 185 if (extension.homepageUrl) { 186 var siteLink = node.querySelector('.site-link'); 187 siteLink.href = extension.homepageUrl; 188 siteLink.textContent = loadTimeData.getString( 189 extension.homepageProvided ? 'extensionSettingsVisitWebsite' : 190 'extensionSettingsVisitWebStore'); 191 siteLink.hidden = false; 192 } 193 194 if (extension.allow_reload) { 195 // The 'Reload' link. 196 var reload = node.querySelector('.reload-link'); 197 reload.addEventListener('click', function(e) { 198 chrome.send('extensionSettingsReload', [extension.id]); 199 extensionReloadedTimestamp[extension.id] = Date.now(); 200 }); 201 reload.hidden = false; 202 203 if (extension.is_platform_app) { 204 // The 'Launch' link. 205 var launch = node.querySelector('.launch-link'); 206 launch.addEventListener('click', function(e) { 207 chrome.send('extensionSettingsLaunch', [extension.id]); 208 }); 209 launch.hidden = false; 210 211 // The 'Restart' link. 212 var restart = node.querySelector('.restart-link'); 213 restart.addEventListener('click', function(e) { 214 chrome.send('extensionSettingsRestart', [extension.id]); 215 }); 216 restart.hidden = false; 217 } 218 } 219 220 if (!extension.terminated) { 221 // The 'Enabled' checkbox. 222 var enable = node.querySelector('.enable-checkbox'); 223 enable.hidden = false; 224 enable.querySelector('input').disabled = !extension.userModifiable; 225 226 if (extension.userModifiable) { 227 enable.addEventListener('click', function(e) { 228 // When e.target is the label instead of the checkbox, it doesn't 229 // have the checked property and the state of the checkbox is 230 // left unchanged. 231 var checked = e.target.checked; 232 if (checked == undefined) 233 checked = !e.currentTarget.querySelector('input').checked; 234 chrome.send('extensionSettingsEnable', 235 [extension.id, checked ? 'true' : 'false']); 236 237 // This may seem counter-intuitive (to not set/clear the checkmark) 238 // but this page will be updated asynchronously if the extension 239 // becomes enabled/disabled. It also might not become enabled or 240 // disabled, because the user might e.g. get prompted when enabling 241 // and choose not to. 242 e.preventDefault(); 243 }); 244 } 245 246 enable.querySelector('input').checked = extension.enabled; 247 } else { 248 var terminatedReload = node.querySelector('.terminated-reload-link'); 249 terminatedReload.hidden = false; 250 terminatedReload.addEventListener('click', function(e) { 251 chrome.send('extensionSettingsReload', [extension.id]); 252 }); 253 } 254 255 // 'Remove' button. 256 var trashTemplate = $('template-collection').querySelector('.trash'); 257 var trash = trashTemplate.cloneNode(true); 258 trash.title = loadTimeData.getString('extensionUninstall'); 259 trash.addEventListener('click', function(e) { 260 butterBarVisibility[extension.id] = false; 261 chrome.send('extensionSettingsUninstall', [extension.id]); 262 }); 263 node.querySelector('.enable-controls').appendChild(trash); 264 265 // Developer mode //////////////////////////////////////////////////////// 266 267 // First we have the id. 268 var idLabel = node.querySelector('.extension-id'); 269 idLabel.textContent = ' ' + extension.id; 270 271 // Then the path, if provided by unpacked extension. 272 if (extension.isUnpacked) { 273 var loadPath = node.querySelector('.load-path'); 274 loadPath.hidden = false; 275 loadPath.querySelector('span:nth-of-type(2)').textContent = 276 ' ' + extension.path; 277 } 278 279 // Then the 'managed, cannot uninstall/disable' message. 280 if (!extension.userModifiable) 281 node.querySelector('.managed-message').hidden = false; 282 283 // Then active views. 284 if (extension.views.length > 0) { 285 var activeViews = node.querySelector('.active-views'); 286 activeViews.hidden = false; 287 var link = activeViews.querySelector('a'); 288 289 extension.views.forEach(function(view, i) { 290 var label = view.path + 291 (view.incognito ? 292 ' ' + loadTimeData.getString('viewIncognito') : '') + 293 (view.renderProcessId == -1 ? 294 ' ' + loadTimeData.getString('viewInactive') : ''); 295 link.textContent = label; 296 link.addEventListener('click', function(e) { 297 // TODO(estade): remove conversion to string? 298 chrome.send('extensionSettingsInspect', [ 299 String(extension.id), 300 String(view.renderProcessId), 301 String(view.renderViewId), 302 view.incognito 303 ]); 304 }); 305 306 if (i < extension.views.length - 1) { 307 link = link.cloneNode(true); 308 activeViews.appendChild(link); 309 } 310 }); 311 } 312 313 // The extension warnings (describing runtime issues). 314 if (extension.warnings) { 315 var panel = node.querySelector('.extension-warnings'); 316 panel.hidden = false; 317 var list = panel.querySelector('ul'); 318 extension.warnings.forEach(function(warning) { 319 list.appendChild(document.createElement('li')).innerText = warning; 320 }); 321 } 322 323 // The install warnings. 324 if (extension.installWarnings) { 325 var panel = node.querySelector('.install-warnings'); 326 panel.hidden = false; 327 var list = panel.querySelector('ul'); 328 extension.installWarnings.forEach(function(warning) { 329 var li = document.createElement('li'); 330 li[warning.isHTML ? 'innerHTML' : 'innerText'] = warning.message; 331 list.appendChild(li); 332 }); 333 } 334 335 this.appendChild(node); 336 if (location.hash.substr(1) == extension.id) { 337 // Scroll beneath the fixed header so that the extension is not 338 // obscured. 339 var topScroll = node.offsetTop - $('page-header').offsetHeight; 340 var pad = parseInt(getComputedStyle(node, null).marginTop, 10); 341 if (!isNaN(pad)) 342 topScroll -= pad / 2; 343 document.body.scrollTop = topScroll; 344 } 345 } 346 }; 347 348 return { 349 ExtensionsList: ExtensionsList 350 }; 351 }); 352