1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 cr.define('options.contentSettings', function() { 6 const InlineEditableItemList = options.InlineEditableItemList; 7 const InlineEditableItem = options.InlineEditableItem; 8 const ArrayDataModel = cr.ui.ArrayDataModel; 9 10 /** 11 * Creates a new exceptions list item. 12 * @param {string} contentType The type of the list. 13 * @param {string} mode The browser mode, 'otr' or 'normal'. 14 * @param {boolean} enableAskOption Whether to show an 'ask every time' 15 * option in the select. 16 * @param {Object} exception A dictionary that contains the data of the 17 * exception. 18 * @constructor 19 * @extends {options.InlineEditableItem} 20 */ 21 function ExceptionsListItem(contentType, mode, enableAskOption, exception) { 22 var el = cr.doc.createElement('div'); 23 el.mode = mode; 24 el.contentType = contentType; 25 el.enableAskOption = enableAskOption; 26 el.dataItem = exception; 27 el.__proto__ = ExceptionsListItem.prototype; 28 el.decorate(); 29 30 return el; 31 } 32 33 ExceptionsListItem.prototype = { 34 __proto__: InlineEditableItem.prototype, 35 36 /** 37 * Called when an element is decorated as a list item. 38 */ 39 decorate: function() { 40 InlineEditableItem.prototype.decorate.call(this); 41 42 this.isPlaceholder = !this.pattern; 43 var patternCell = this.createEditableTextCell(this.pattern); 44 patternCell.className = 'exception-pattern'; 45 this.contentElement.appendChild(patternCell); 46 if (this.pattern) 47 this.patternLabel = patternCell.querySelector('.static-text'); 48 var input = patternCell.querySelector('input'); 49 50 // TODO(stuartmorgan): Create an createEditableSelectCell abstracting 51 // this code. 52 // Setting label for display mode. |pattern| will be null for the 'add new 53 // exception' row. 54 if (this.pattern) { 55 var settingLabel = cr.doc.createElement('span'); 56 settingLabel.textContent = this.settingForDisplay(); 57 settingLabel.className = 'exception-setting'; 58 settingLabel.setAttribute('displaymode', 'static'); 59 this.contentElement.appendChild(settingLabel); 60 this.settingLabel = settingLabel; 61 } 62 63 // Setting select element for edit mode. 64 var select = cr.doc.createElement('select'); 65 var optionAllow = cr.doc.createElement('option'); 66 optionAllow.textContent = templateData.allowException; 67 optionAllow.value = 'allow'; 68 select.appendChild(optionAllow); 69 70 if (this.enableAskOption) { 71 var optionAsk = cr.doc.createElement('option'); 72 optionAsk.textContent = templateData.askException; 73 optionAsk.value = 'ask'; 74 select.appendChild(optionAsk); 75 } 76 77 if (this.contentType == 'cookies') { 78 var optionSession = cr.doc.createElement('option'); 79 optionSession.textContent = templateData.sessionException; 80 optionSession.value = 'session'; 81 select.appendChild(optionSession); 82 } 83 84 var optionBlock = cr.doc.createElement('option'); 85 optionBlock.textContent = templateData.blockException; 86 optionBlock.value = 'block'; 87 select.appendChild(optionBlock); 88 89 this.contentElement.appendChild(select); 90 select.className = 'exception-setting'; 91 if (this.pattern) 92 select.setAttribute('displaymode', 'edit'); 93 94 // Used to track whether the URL pattern in the input is valid. 95 // This will be true if the browser process has informed us that the 96 // current text in the input is valid. Changing the text resets this to 97 // false, and getting a response from the browser sets it back to true. 98 // It starts off as false for empty string (new exceptions) or true for 99 // already-existing exceptions (which we assume are valid). 100 this.inputValidityKnown = this.pattern; 101 // This one tracks the actual validity of the pattern in the input. This 102 // starts off as true so as not to annoy the user when he adds a new and 103 // empty input. 104 this.inputIsValid = true; 105 106 this.input = input; 107 this.select = select; 108 109 this.updateEditables(); 110 111 // Editing notifications and geolocation is disabled for now. 112 if (this.contentType == 'notifications' || 113 this.contentType == 'location') { 114 this.editable = false; 115 } 116 117 var listItem = this; 118 // Handle events on the editable nodes. 119 input.oninput = function(event) { 120 listItem.inputValidityKnown = false; 121 chrome.send('checkExceptionPatternValidity', 122 [listItem.contentType, listItem.mode, input.value]); 123 }; 124 125 // Listen for edit events. 126 this.addEventListener('canceledit', this.onEditCancelled_); 127 this.addEventListener('commitedit', this.onEditCommitted_); 128 }, 129 130 /** 131 * The pattern (e.g., a URL) for the exception. 132 * @type {string} 133 */ 134 get pattern() { 135 return this.dataItem['displayPattern']; 136 }, 137 set pattern(pattern) { 138 this.dataItem['displayPattern'] = pattern; 139 }, 140 141 /** 142 * The setting (allow/block) for the exception. 143 * @type {string} 144 */ 145 get setting() { 146 return this.dataItem['setting']; 147 }, 148 set setting(setting) { 149 this.dataItem['setting'] = setting; 150 }, 151 152 /** 153 * Gets a human-readable setting string. 154 * @type {string} 155 */ 156 settingForDisplay: function() { 157 var setting = this.setting; 158 if (setting == 'allow') 159 return templateData.allowException; 160 else if (setting == 'block') 161 return templateData.blockException; 162 else if (setting == 'ask') 163 return templateData.askException; 164 else if (setting == 'session') 165 return templateData.sessionException; 166 }, 167 168 /** 169 * Update this list item to reflect whether the input is a valid pattern. 170 * @param {boolean} valid Whether said pattern is valid in the context of 171 * a content exception setting. 172 */ 173 setPatternValid: function(valid) { 174 if (valid || !this.input.value) 175 this.input.setCustomValidity(''); 176 else 177 this.input.setCustomValidity(' '); 178 this.inputIsValid = valid; 179 this.inputValidityKnown = true; 180 }, 181 182 /** 183 * Set the <input> to its original contents. Used when the user quits 184 * editing. 185 */ 186 resetInput: function() { 187 this.input.value = this.pattern; 188 }, 189 190 /** 191 * Copy the data model values to the editable nodes. 192 */ 193 updateEditables: function() { 194 this.resetInput(); 195 196 var settingOption = 197 this.select.querySelector('[value=\'' + this.setting + '\']'); 198 if (settingOption) 199 settingOption.selected = true; 200 }, 201 202 /** @inheritDoc */ 203 get currentInputIsValid() { 204 return this.inputValidityKnown && this.inputIsValid; 205 }, 206 207 /** @inheritDoc */ 208 get hasBeenEdited() { 209 var livePattern = this.input.value; 210 var liveSetting = this.select.value; 211 return livePattern != this.pattern || liveSetting != this.setting; 212 }, 213 214 /** 215 * Called when committing an edit. 216 * @param {Event} e The end event. 217 * @private 218 */ 219 onEditCommitted_: function(e) { 220 var newPattern = this.input.value; 221 var newSetting = this.select.value; 222 223 this.finishEdit(newPattern, newSetting); 224 }, 225 226 /** 227 * Called when cancelling an edit; resets the control states. 228 * @param {Event} e The cancel event. 229 * @private 230 */ 231 onEditCancelled_: function() { 232 this.updateEditables(); 233 this.setPatternValid(true); 234 }, 235 236 /** 237 * Editing is complete; update the model. 238 * @param {string} newPattern The pattern that the user entered. 239 * @param {string} newSetting The setting the user chose. 240 */ 241 finishEdit: function(newPattern, newSetting) { 242 this.patternLabel.textContent = newPattern; 243 this.settingLabel.textContent = this.settingForDisplay(); 244 var oldPattern = this.pattern; 245 this.pattern = newPattern; 246 this.setting = newSetting; 247 248 // TODO(estade): this will need to be updated if geolocation/notifications 249 // become editable. 250 if (oldPattern != newPattern) { 251 chrome.send('removeException', 252 [this.contentType, this.mode, oldPattern]); 253 } 254 255 chrome.send('setException', 256 [this.contentType, this.mode, newPattern, newSetting]); 257 } 258 }; 259 260 /** 261 * Creates a new list item for the Add New Item row, which doesn't represent 262 * an actual entry in the exceptions list but allows the user to add new 263 * exceptions. 264 * @param {string} contentType The type of the list. 265 * @param {string} mode The browser mode, 'otr' or 'normal'. 266 * @param {boolean} enableAskOption Whether to show an 'ask every time' 267 * option in the select. 268 * @constructor 269 * @extends {cr.ui.ExceptionsListItem} 270 */ 271 function ExceptionsAddRowListItem(contentType, mode, enableAskOption) { 272 var el = cr.doc.createElement('div'); 273 el.mode = mode; 274 el.contentType = contentType; 275 el.enableAskOption = enableAskOption; 276 el.dataItem = []; 277 el.__proto__ = ExceptionsAddRowListItem.prototype; 278 el.decorate(); 279 280 return el; 281 } 282 283 ExceptionsAddRowListItem.prototype = { 284 __proto__: ExceptionsListItem.prototype, 285 286 decorate: function() { 287 ExceptionsListItem.prototype.decorate.call(this); 288 289 this.input.placeholder = templateData.addNewExceptionInstructions; 290 291 // Do we always want a default of allow? 292 this.setting = 'allow'; 293 }, 294 295 /** 296 * Clear the <input> and let the placeholder text show again. 297 */ 298 resetInput: function() { 299 this.input.value = ''; 300 }, 301 302 /** @inheritDoc */ 303 get hasBeenEdited() { 304 return this.input.value != ''; 305 }, 306 307 /** 308 * Editing is complete; update the model. As long as the pattern isn't 309 * empty, we'll just add it. 310 * @param {string} newPattern The pattern that the user entered. 311 * @param {string} newSetting The setting the user chose. 312 */ 313 finishEdit: function(newPattern, newSetting) { 314 chrome.send('setException', 315 [this.contentType, this.mode, newPattern, newSetting]); 316 }, 317 }; 318 319 /** 320 * Creates a new exceptions list. 321 * @constructor 322 * @extends {cr.ui.List} 323 */ 324 var ExceptionsList = cr.ui.define('list'); 325 326 ExceptionsList.prototype = { 327 __proto__: InlineEditableItemList.prototype, 328 329 /** 330 * Called when an element is decorated as a list. 331 */ 332 decorate: function() { 333 InlineEditableItemList.prototype.decorate.call(this); 334 335 this.classList.add('settings-list'); 336 337 for (var parentNode = this.parentNode; parentNode; 338 parentNode = parentNode.parentNode) { 339 if (parentNode.hasAttribute('contentType')) { 340 this.contentType = parentNode.getAttribute('contentType'); 341 break; 342 } 343 } 344 345 this.mode = this.getAttribute('mode'); 346 347 var exceptionList = this; 348 function handleBlur(e) { 349 // When the blur event happens we do not know who is getting focus so we 350 // delay this a bit until we know if the new focus node is outside the 351 // list. 352 var doc = e.target.ownerDocument; 353 window.setTimeout(function() { 354 var activeElement = doc.activeElement; 355 if (!exceptionList.contains(activeElement)) 356 exceptionList.selectionModel.unselectAll(); 357 }, 50); 358 } 359 360 this.addEventListener('blur', handleBlur, true); 361 362 // Whether the exceptions in this list allow an 'Ask every time' option. 363 this.enableAskOption = (this.contentType == 'plugins' && 364 templateData.enable_click_to_play); 365 366 this.autoExpands = true; 367 this.reset(); 368 }, 369 370 /** 371 * Creates an item to go in the list. 372 * @param {Object} entry The element from the data model for this row. 373 */ 374 createItem: function(entry) { 375 if (entry) { 376 return new ExceptionsListItem(this.contentType, 377 this.mode, 378 this.enableAskOption, 379 entry); 380 } else { 381 var addRowItem = new ExceptionsAddRowListItem(this.contentType, 382 this.mode, 383 this.enableAskOption); 384 addRowItem.deletable = false; 385 return addRowItem; 386 } 387 }, 388 389 /** 390 * Sets the exceptions in the js model. 391 * @param {Object} entries A list of dictionaries of values, each dictionary 392 * represents an exception. 393 */ 394 setExceptions: function(entries) { 395 var deleteCount = this.dataModel.length; 396 397 if (this.isEditable()) { 398 // We don't want to remove the Add New Exception row. 399 deleteCount = deleteCount - 1; 400 } 401 402 var args = [0, deleteCount]; 403 args.push.apply(args, entries); 404 this.dataModel.splice.apply(this.dataModel, args); 405 }, 406 407 /** 408 * The browser has finished checking a pattern for validity. Update the 409 * list item to reflect this. 410 * @param {string} pattern The pattern. 411 * @param {bool} valid Whether said pattern is valid in the context of 412 * a content exception setting. 413 */ 414 patternValidityCheckComplete: function(pattern, valid) { 415 var listItems = this.items; 416 for (var i = 0; i < listItems.length; i++) { 417 var listItem = listItems[i]; 418 // Don't do anything for messages for the item if it is not the intended 419 // recipient, or if the response is stale (i.e. the input value has 420 // changed since we sent the request to analyze it). 421 if (pattern == listItem.input.value) 422 listItem.setPatternValid(valid); 423 } 424 }, 425 426 /** 427 * Returns whether the rows are editable in this list. 428 */ 429 isEditable: function() { 430 // Editing notifications and geolocation is disabled for now. 431 return !(this.contentType == 'notifications' || 432 this.contentType == 'location'); 433 }, 434 435 /** 436 * Removes all exceptions from the js model. 437 */ 438 reset: function() { 439 if (this.isEditable()) { 440 // The null creates the Add New Exception row. 441 this.dataModel = new ArrayDataModel([null]); 442 } else { 443 this.dataModel = new ArrayDataModel([]); 444 } 445 }, 446 447 /** @inheritDoc */ 448 deleteItemAtIndex: function(index) { 449 var listItem = this.getListItemByIndex(index); 450 if (listItem.undeletable) 451 return; 452 453 var dataItem = listItem.dataItem; 454 var args = [listItem.contentType]; 455 if (listItem.contentType == 'location') 456 args.push(dataItem['origin'], dataItem['embeddingOrigin']); 457 else if (listItem.contentType == 'notifications') 458 args.push(dataItem['origin'], dataItem['setting']); 459 else 460 args.push(listItem.mode, listItem.pattern); 461 462 chrome.send('removeException', args); 463 }, 464 }; 465 466 var OptionsPage = options.OptionsPage; 467 468 /** 469 * Encapsulated handling of content settings list subpage. 470 * @constructor 471 */ 472 function ContentSettingsExceptionsArea() { 473 OptionsPage.call(this, 'contentExceptions', 474 templateData.contentSettingsPageTabTitle, 475 'content-settings-exceptions-area'); 476 } 477 478 cr.addSingletonGetter(ContentSettingsExceptionsArea); 479 480 ContentSettingsExceptionsArea.prototype = { 481 __proto__: OptionsPage.prototype, 482 483 initializePage: function() { 484 OptionsPage.prototype.initializePage.call(this); 485 486 var exceptionsLists = this.pageDiv.querySelectorAll('list'); 487 for (var i = 0; i < exceptionsLists.length; i++) { 488 options.contentSettings.ExceptionsList.decorate(exceptionsLists[i]); 489 } 490 491 ContentSettingsExceptionsArea.hideOTRLists(); 492 493 // If the user types in the URL without a hash, show just cookies. 494 this.showList('cookies'); 495 }, 496 497 /** 498 * Shows one list and hides all others. 499 * @param {string} type The content type. 500 */ 501 showList: function(type) { 502 var header = this.pageDiv.querySelector('h1'); 503 header.textContent = templateData[type + '_header']; 504 505 var divs = this.pageDiv.querySelectorAll('div[contentType]'); 506 for (var i = 0; i < divs.length; i++) { 507 if (divs[i].getAttribute('contentType') == type) 508 divs[i].classList.remove('hidden'); 509 else 510 divs[i].classList.add('hidden'); 511 } 512 }, 513 514 /** 515 * Called after the page has been shown. Show the content type for the 516 * location's hash. 517 */ 518 didShowPage: function() { 519 var hash = location.hash; 520 if (hash) 521 this.showList(hash.slice(1)); 522 }, 523 }; 524 525 /** 526 * Called when the last incognito window is closed. 527 */ 528 ContentSettingsExceptionsArea.OTRProfileDestroyed = function() { 529 this.hideOTRLists(); 530 }; 531 532 /** 533 * Clears and hides the incognito exceptions lists. 534 */ 535 ContentSettingsExceptionsArea.hideOTRLists = function() { 536 var otrLists = document.querySelectorAll('list[mode=otr]'); 537 538 for (var i = 0; i < otrLists.length; i++) { 539 otrLists[i].reset(); 540 otrLists[i].parentNode.classList.add('hidden'); 541 } 542 }; 543 544 return { 545 ExceptionsListItem: ExceptionsListItem, 546 ExceptionsAddRowListItem: ExceptionsAddRowListItem, 547 ExceptionsList: ExceptionsList, 548 ContentSettingsExceptionsArea: ContentSettingsExceptionsArea, 549 }; 550 }); 551