content_settings_exceptions_area.js revision 4e180b6a0b4720a9b8e9e959a882386f690f08ff
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 5cr.define('options.contentSettings', function() { 6 /** @const */ var ControlledSettingIndicator = 7 options.ControlledSettingIndicator; 8 /** @const */ var InlineEditableItemList = options.InlineEditableItemList; 9 /** @const */ var InlineEditableItem = options.InlineEditableItem; 10 /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel; 11 12 /** 13 * Creates a new exceptions list item. 14 * 15 * @param {string} contentType The type of the list. 16 * @param {string} mode The browser mode, 'otr' or 'normal'. 17 * @param {boolean} enableAskOption Whether to show an 'ask every time' 18 * option in the select. 19 * @param {Object} exception A dictionary that contains the data of the 20 * exception. 21 * @constructor 22 * @extends {options.InlineEditableItem} 23 */ 24 function ExceptionsListItem(contentType, mode, enableAskOption, exception) { 25 var el = cr.doc.createElement('div'); 26 el.mode = mode; 27 el.contentType = contentType; 28 el.enableAskOption = enableAskOption; 29 el.dataItem = exception; 30 el.__proto__ = ExceptionsListItem.prototype; 31 el.decorate(); 32 33 return el; 34 } 35 36 ExceptionsListItem.prototype = { 37 __proto__: InlineEditableItem.prototype, 38 39 /** 40 * Called when an element is decorated as a list item. 41 */ 42 decorate: function() { 43 InlineEditableItem.prototype.decorate.call(this); 44 45 this.isPlaceholder = !this.pattern; 46 var patternCell = this.createEditableTextCell(this.pattern); 47 patternCell.className = 'exception-pattern'; 48 patternCell.classList.add('weakrtl'); 49 this.contentElement.appendChild(patternCell); 50 if (this.pattern) 51 this.patternLabel = patternCell.querySelector('.static-text'); 52 var input = patternCell.querySelector('input'); 53 54 // TODO(stuartmorgan): Create an createEditableSelectCell abstracting 55 // this code. 56 // Setting label for display mode. |pattern| will be null for the 'add new 57 // exception' row. 58 if (this.pattern) { 59 var settingLabel = cr.doc.createElement('span'); 60 settingLabel.textContent = this.settingForDisplay(); 61 settingLabel.className = 'exception-setting'; 62 settingLabel.setAttribute('displaymode', 'static'); 63 this.contentElement.appendChild(settingLabel); 64 this.settingLabel = settingLabel; 65 } 66 67 // Setting select element for edit mode. 68 var select = cr.doc.createElement('select'); 69 var optionAllow = cr.doc.createElement('option'); 70 optionAllow.textContent = loadTimeData.getString('allowException'); 71 optionAllow.value = 'allow'; 72 select.appendChild(optionAllow); 73 74 if (this.enableAskOption) { 75 var optionAsk = cr.doc.createElement('option'); 76 optionAsk.textContent = loadTimeData.getString('askException'); 77 optionAsk.value = 'ask'; 78 select.appendChild(optionAsk); 79 } 80 81 if (this.contentType == 'cookies') { 82 var optionSession = cr.doc.createElement('option'); 83 optionSession.textContent = loadTimeData.getString('sessionException'); 84 optionSession.value = 'session'; 85 select.appendChild(optionSession); 86 } 87 88 if (this.contentType != 'fullscreen') { 89 var optionBlock = cr.doc.createElement('option'); 90 optionBlock.textContent = loadTimeData.getString('blockException'); 91 optionBlock.value = 'block'; 92 select.appendChild(optionBlock); 93 } 94 95 if (this.isEmbeddingRule()) { 96 this.patternLabel.classList.add('sublabel'); 97 this.editable = false; 98 } 99 100 if (this.setting == 'default') { 101 // Items that don't have their own settings (parents of 'embedded on' 102 // items) aren't deletable. 103 this.deletable = false; 104 this.editable = false; 105 } 106 107 this.contentElement.appendChild(select); 108 select.className = 'exception-setting'; 109 select.setAttribute('aria-labelledby', 'exception-behavior-column'); 110 111 if (this.pattern) 112 select.setAttribute('displaymode', 'edit'); 113 114 if (this.contentType == 'media-stream') { 115 this.settingLabel.classList.add('media-audio-setting'); 116 117 var videoSettingLabel = cr.doc.createElement('span'); 118 videoSettingLabel.textContent = this.videoSettingForDisplay(); 119 videoSettingLabel.className = 'exception-setting'; 120 videoSettingLabel.classList.add('media-video-setting'); 121 videoSettingLabel.setAttribute('displaymode', 'static'); 122 this.contentElement.appendChild(videoSettingLabel); 123 } 124 125 // Used to track whether the URL pattern in the input is valid. 126 // This will be true if the browser process has informed us that the 127 // current text in the input is valid. Changing the text resets this to 128 // false, and getting a response from the browser sets it back to true. 129 // It starts off as false for empty string (new exceptions) or true for 130 // already-existing exceptions (which we assume are valid). 131 this.inputValidityKnown = this.pattern; 132 // This one tracks the actual validity of the pattern in the input. This 133 // starts off as true so as not to annoy the user when he adds a new and 134 // empty input. 135 this.inputIsValid = true; 136 137 this.input = input; 138 this.select = select; 139 140 this.updateEditables(); 141 142 // Editing notifications, geolocation and media-stream is disabled for 143 // now. 144 if (this.contentType == 'notifications' || 145 this.contentType == 'location' || 146 this.contentType == 'media-stream') { 147 this.editable = false; 148 } 149 150 // If the source of the content setting exception is not a user 151 // preference, that source controls the exception and the user cannot edit 152 // or delete it. 153 var controlledBy = 154 this.dataItem.source && this.dataItem.source != 'preference' ? 155 this.dataItem.source : null; 156 157 if (controlledBy) { 158 this.setAttribute('controlled-by', controlledBy); 159 this.deletable = false; 160 this.editable = false; 161 } 162 163 if (controlledBy == 'policy' || controlledBy == 'extension') { 164 this.querySelector('.row-delete-button').hidden = true; 165 var indicator = ControlledSettingIndicator(); 166 indicator.setAttribute('content-exception', this.contentType); 167 // Create a synthetic pref change event decorated as 168 // CoreOptionsHandler::CreateValueForPref() does. 169 var event = new Event(this.contentType); 170 event.value = { controlledBy: controlledBy }; 171 indicator.handlePrefChange(event); 172 this.appendChild(indicator); 173 } 174 175 // If the exception comes from a hosted app, display the name and the 176 // icon of the app. 177 if (controlledBy == 'HostedApp') { 178 this.title = 179 loadTimeData.getString('set_by') + ' ' + this.dataItem.appName; 180 var button = this.querySelector('.row-delete-button'); 181 // Use the host app's favicon (16px, match bigger size). 182 // See c/b/ui/webui/extensions/extension_icon_source.h 183 // for a description of the chrome://extension-icon URL. 184 button.style.backgroundImage = 185 'url(\'chrome://extension-icon/' + this.dataItem.appId + '/16/1\')'; 186 } 187 188 var listItem = this; 189 // Handle events on the editable nodes. 190 input.oninput = function(event) { 191 listItem.inputValidityKnown = false; 192 chrome.send('checkExceptionPatternValidity', 193 [listItem.contentType, listItem.mode, input.value]); 194 }; 195 196 // Listen for edit events. 197 this.addEventListener('canceledit', this.onEditCancelled_); 198 this.addEventListener('commitedit', this.onEditCommitted_); 199 }, 200 201 isEmbeddingRule: function() { 202 return this.dataItem.embeddingOrigin && 203 this.dataItem.embeddingOrigin !== this.dataItem.origin; 204 }, 205 206 /** 207 * The pattern (e.g., a URL) for the exception. 208 * 209 * @type {string} 210 */ 211 get pattern() { 212 if (!this.isEmbeddingRule()) { 213 return this.dataItem.origin; 214 } else { 215 return loadTimeData.getStringF('embeddedOnHost', 216 this.dataItem.embeddingOrigin); 217 } 218 219 return this.dataItem.displayPattern; 220 }, 221 set pattern(pattern) { 222 if (!this.editable) 223 console.error('Tried to change uneditable pattern'); 224 225 this.dataItem.displayPattern = pattern; 226 }, 227 228 /** 229 * The setting (allow/block) for the exception. 230 * 231 * @type {string} 232 */ 233 get setting() { 234 return this.dataItem.setting; 235 }, 236 set setting(setting) { 237 this.dataItem.setting = setting; 238 }, 239 240 /** 241 * Gets a human-readable setting string. 242 * 243 * @return {string} The display string. 244 */ 245 settingForDisplay: function() { 246 return this.getDisplayStringForSetting(this.setting); 247 }, 248 249 /** 250 * media video specific function. 251 * Gets a human-readable video setting string. 252 * 253 * @return {string} The display string. 254 */ 255 videoSettingForDisplay: function() { 256 return this.getDisplayStringForSetting(this.dataItem.video); 257 }, 258 259 /** 260 * Gets a human-readable display string for setting. 261 * 262 * @param {string} setting The setting to be displayed. 263 * @return {string} The display string. 264 */ 265 getDisplayStringForSetting: function(setting) { 266 if (setting == 'allow') 267 return loadTimeData.getString('allowException'); 268 else if (setting == 'block') 269 return loadTimeData.getString('blockException'); 270 else if (setting == 'ask') 271 return loadTimeData.getString('askException'); 272 else if (setting == 'session') 273 return loadTimeData.getString('sessionException'); 274 else if (setting == 'default') 275 return ''; 276 277 console.error('Unknown setting: [' + setting + ']'); 278 return ''; 279 }, 280 281 /** 282 * Update this list item to reflect whether the input is a valid pattern. 283 * 284 * @param {boolean} valid Whether said pattern is valid in the context of a 285 * content exception setting. 286 */ 287 setPatternValid: function(valid) { 288 if (valid || !this.input.value) 289 this.input.setCustomValidity(''); 290 else 291 this.input.setCustomValidity(' '); 292 this.inputIsValid = valid; 293 this.inputValidityKnown = true; 294 }, 295 296 /** 297 * Set the <input> to its original contents. Used when the user quits 298 * editing. 299 */ 300 resetInput: function() { 301 this.input.value = this.pattern; 302 }, 303 304 /** 305 * Copy the data model values to the editable nodes. 306 */ 307 updateEditables: function() { 308 this.resetInput(); 309 310 var settingOption = 311 this.select.querySelector('[value=\'' + this.setting + '\']'); 312 if (settingOption) 313 settingOption.selected = true; 314 }, 315 316 /** @override */ 317 get currentInputIsValid() { 318 return this.inputValidityKnown && this.inputIsValid; 319 }, 320 321 /** @override */ 322 get hasBeenEdited() { 323 var livePattern = this.input.value; 324 var liveSetting = this.select.value; 325 return livePattern != this.pattern || liveSetting != this.setting; 326 }, 327 328 /** 329 * Called when committing an edit. 330 * 331 * @param {Event} e The end event. 332 * @private 333 */ 334 onEditCommitted_: function(e) { 335 var newPattern = this.input.value; 336 var newSetting = this.select.value; 337 338 this.finishEdit(newPattern, newSetting); 339 }, 340 341 /** 342 * Called when cancelling an edit; resets the control states. 343 * 344 * @param {Event} e The cancel event. 345 * @private 346 */ 347 onEditCancelled_: function() { 348 this.updateEditables(); 349 this.setPatternValid(true); 350 }, 351 352 /** 353 * Editing is complete; update the model. 354 * 355 * @param {string} newPattern The pattern that the user entered. 356 * @param {string} newSetting The setting the user chose. 357 */ 358 finishEdit: function(newPattern, newSetting) { 359 this.patternLabel.textContent = newPattern; 360 this.settingLabel.textContent = this.settingForDisplay(); 361 var oldPattern = this.pattern; 362 this.pattern = newPattern; 363 this.setting = newSetting; 364 365 // TODO(estade): this will need to be updated if geolocation/notifications 366 // become editable. 367 if (oldPattern != newPattern) { 368 chrome.send('removeException', 369 [this.contentType, this.mode, oldPattern]); 370 } 371 372 chrome.send('setException', 373 [this.contentType, this.mode, newPattern, newSetting]); 374 }, 375 376 /** @override */ 377 isExtraFocusableControl: function(element) { 378 return element === this.select; 379 }, 380 }; 381 382 /** 383 * Creates a new list item for the Add New Item row, which doesn't represent 384 * an actual entry in the exceptions list but allows the user to add new 385 * exceptions. 386 * 387 * @param {string} contentType The type of the list. 388 * @param {string} mode The browser mode, 'otr' or 'normal'. 389 * @param {boolean} enableAskOption Whether to show an 'ask every time' option 390 * in the select. 391 * @constructor 392 * @extends {cr.ui.ExceptionsListItem} 393 */ 394 function ExceptionsAddRowListItem(contentType, mode, enableAskOption) { 395 var el = cr.doc.createElement('div'); 396 el.mode = mode; 397 el.contentType = contentType; 398 el.enableAskOption = enableAskOption; 399 el.dataItem = []; 400 el.__proto__ = ExceptionsAddRowListItem.prototype; 401 el.decorate(); 402 403 return el; 404 } 405 406 ExceptionsAddRowListItem.prototype = { 407 __proto__: ExceptionsListItem.prototype, 408 409 decorate: function() { 410 ExceptionsListItem.prototype.decorate.call(this); 411 412 this.input.placeholder = 413 loadTimeData.getString('addNewExceptionInstructions'); 414 415 // Do we always want a default of allow? 416 this.setting = 'allow'; 417 }, 418 419 /** 420 * Clear the <input> and let the placeholder text show again. 421 */ 422 resetInput: function() { 423 this.input.value = ''; 424 }, 425 426 /** @override */ 427 get hasBeenEdited() { 428 return this.input.value != ''; 429 }, 430 431 /** 432 * Editing is complete; update the model. As long as the pattern isn't 433 * empty, we'll just add it. 434 * 435 * @param {string} newPattern The pattern that the user entered. 436 * @param {string} newSetting The setting the user chose. 437 */ 438 finishEdit: function(newPattern, newSetting) { 439 this.resetInput(); 440 chrome.send('setException', 441 [this.contentType, this.mode, newPattern, newSetting]); 442 }, 443 }; 444 445 /** 446 * Creates a new exceptions list. 447 * 448 * @constructor 449 * @extends {cr.ui.List} 450 */ 451 var ExceptionsList = cr.ui.define('list'); 452 453 ExceptionsList.prototype = { 454 __proto__: InlineEditableItemList.prototype, 455 456 /** 457 * Called when an element is decorated as a list. 458 */ 459 decorate: function() { 460 InlineEditableItemList.prototype.decorate.call(this); 461 462 this.classList.add('settings-list'); 463 464 for (var parentNode = this.parentNode; parentNode; 465 parentNode = parentNode.parentNode) { 466 if (parentNode.hasAttribute('contentType')) { 467 this.contentType = parentNode.getAttribute('contentType'); 468 break; 469 } 470 } 471 472 this.mode = this.getAttribute('mode'); 473 474 // Whether the exceptions in this list allow an 'Ask every time' option. 475 this.enableAskOption = this.contentType == 'plugins'; 476 477 this.autoExpands = true; 478 this.reset(); 479 }, 480 481 /** 482 * Creates an item to go in the list. 483 * 484 * @param {Object} entry The element from the data model for this row. 485 */ 486 createItem: function(entry) { 487 if (entry) { 488 return new ExceptionsListItem(this.contentType, 489 this.mode, 490 this.enableAskOption, 491 entry); 492 } else { 493 var addRowItem = new ExceptionsAddRowListItem(this.contentType, 494 this.mode, 495 this.enableAskOption); 496 addRowItem.deletable = false; 497 return addRowItem; 498 } 499 }, 500 501 /** 502 * Sets the exceptions in the js model. 503 * 504 * @param {Object} entries A list of dictionaries of values, each dictionary 505 * represents an exception. 506 */ 507 setExceptions: function(entries) { 508 var deleteCount = this.dataModel.length; 509 510 if (this.isEditable()) { 511 // We don't want to remove the Add New Exception row. 512 deleteCount = deleteCount - 1; 513 } 514 515 var args = [0, deleteCount]; 516 args.push.apply(args, entries); 517 this.dataModel.splice.apply(this.dataModel, args); 518 }, 519 520 /** 521 * The browser has finished checking a pattern for validity. Update the list 522 * item to reflect this. 523 * 524 * @param {string} pattern The pattern. 525 * @param {bool} valid Whether said pattern is valid in the context of a 526 * content exception setting. 527 */ 528 patternValidityCheckComplete: function(pattern, valid) { 529 var listItems = this.items; 530 for (var i = 0; i < listItems.length; i++) { 531 var listItem = listItems[i]; 532 // Don't do anything for messages for the item if it is not the intended 533 // recipient, or if the response is stale (i.e. the input value has 534 // changed since we sent the request to analyze it). 535 if (pattern == listItem.input.value) 536 listItem.setPatternValid(valid); 537 } 538 }, 539 540 /** 541 * Returns whether the rows are editable in this list. 542 */ 543 isEditable: function() { 544 // Exceptions of the following lists are not editable for now. 545 return !(this.contentType == 'notifications' || 546 this.contentType == 'location' || 547 this.contentType == 'fullscreen' || 548 this.contentType == 'media-stream'); 549 }, 550 551 /** 552 * Removes all exceptions from the js model. 553 */ 554 reset: function() { 555 if (this.isEditable()) { 556 // The null creates the Add New Exception row. 557 this.dataModel = new ArrayDataModel([null]); 558 } else { 559 this.dataModel = new ArrayDataModel([]); 560 } 561 }, 562 563 /** @override */ 564 deleteItemAtIndex: function(index) { 565 var listItem = this.getListItemByIndex(index); 566 if (!listItem.deletable) 567 return; 568 569 var dataItem = listItem.dataItem; 570 var args = [listItem.contentType]; 571 if (listItem.contentType == 'notifications') 572 args.push(dataItem.origin, dataItem.setting); 573 else 574 args.push(listItem.mode, dataItem.origin, dataItem.embeddingOrigin); 575 576 chrome.send('removeException', args); 577 }, 578 }; 579 580 var OptionsPage = options.OptionsPage; 581 582 /** 583 * Encapsulated handling of content settings list subpage. 584 * 585 * @constructor 586 */ 587 function ContentSettingsExceptionsArea() { 588 OptionsPage.call(this, 'contentExceptions', 589 loadTimeData.getString('contentSettingsPageTabTitle'), 590 'content-settings-exceptions-area'); 591 } 592 593 cr.addSingletonGetter(ContentSettingsExceptionsArea); 594 595 ContentSettingsExceptionsArea.prototype = { 596 __proto__: OptionsPage.prototype, 597 598 initializePage: function() { 599 OptionsPage.prototype.initializePage.call(this); 600 601 var exceptionsLists = this.pageDiv.querySelectorAll('list'); 602 for (var i = 0; i < exceptionsLists.length; i++) { 603 options.contentSettings.ExceptionsList.decorate(exceptionsLists[i]); 604 } 605 606 ContentSettingsExceptionsArea.hideOTRLists(false); 607 608 // If the user types in the URL without a hash, show just cookies. 609 this.showList('cookies'); 610 611 $('content-settings-exceptions-overlay-confirm').onclick = 612 OptionsPage.closeOverlay.bind(OptionsPage); 613 }, 614 615 /** 616 * Shows one list and hides all others. 617 * 618 * @param {string} type The content type. 619 */ 620 showList: function(type) { 621 var header = this.pageDiv.querySelector('h1'); 622 header.textContent = loadTimeData.getString(type + '_header'); 623 624 var divs = this.pageDiv.querySelectorAll('div[contentType]'); 625 for (var i = 0; i < divs.length; i++) { 626 if (divs[i].getAttribute('contentType') == type) 627 divs[i].hidden = false; 628 else 629 divs[i].hidden = true; 630 } 631 632 var mediaHeader = this.pageDiv.querySelector('.media-header'); 633 mediaHeader.hidden = type != 'media-stream'; 634 }, 635 636 /** 637 * Called after the page has been shown. Show the content type for the 638 * location's hash. 639 */ 640 didShowPage: function() { 641 var hash = location.hash; 642 if (hash) 643 this.showList(hash.slice(1)); 644 }, 645 }; 646 647 /** 648 * Called when the last incognito window is closed. 649 */ 650 ContentSettingsExceptionsArea.OTRProfileDestroyed = function() { 651 this.hideOTRLists(true); 652 }; 653 654 /** 655 * Hides the incognito exceptions lists and optionally clears them as well. 656 * @param {boolean} clear Whether to clear the lists. 657 */ 658 ContentSettingsExceptionsArea.hideOTRLists = function(clear) { 659 var otrLists = document.querySelectorAll('list[mode=otr]'); 660 661 for (var i = 0; i < otrLists.length; i++) { 662 otrLists[i].parentNode.hidden = true; 663 if (clear) 664 otrLists[i].reset(); 665 } 666 }; 667 668 return { 669 ExceptionsListItem: ExceptionsListItem, 670 ExceptionsAddRowListItem: ExceptionsAddRowListItem, 671 ExceptionsList: ExceptionsList, 672 ContentSettingsExceptionsArea: ContentSettingsExceptionsArea, 673 }; 674}); 675