content_settings_exceptions_area.js revision bda42a81ee5f9b20d2bebedcf0bbef1e30e5b293
1// Copyright (c) 2010 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5cr.define('options.contentSettings', function() { 6 const List = cr.ui.List; 7 const ListItem = cr.ui.ListItem; 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 {Array} exception A pair of the form [filter, setting]. 17 * @constructor 18 * @extends {cr.ui.ListItem} 19 */ 20 function ExceptionsListItem(contentType, mode, enableAskOption, exception) { 21 var el = cr.doc.createElement('li'); 22 el.mode = mode; 23 el.contentType = contentType; 24 el.enableAskOption = enableAskOption; 25 el.dataItem = exception; 26 el.__proto__ = ExceptionsListItem.prototype; 27 el.decorate(); 28 29 return el; 30 } 31 32 ExceptionsListItem.prototype = { 33 __proto__: ListItem.prototype, 34 35 /** 36 * Called when an element is decorated as a list item. 37 */ 38 decorate: function() { 39 ListItem.prototype.decorate.call(this); 40 41 // Labels for display mode. 42 var patternLabel = cr.doc.createElement('span'); 43 patternLabel.textContent = this.pattern; 44 this.appendChild(patternLabel); 45 46 var settingLabel = cr.doc.createElement('span'); 47 settingLabel.textContent = this.settingForDisplay(); 48 settingLabel.className = 'exceptionSetting'; 49 this.appendChild(settingLabel); 50 51 // Elements for edit mode. 52 var input = cr.doc.createElement('input'); 53 input.type = 'text'; 54 this.appendChild(input); 55 input.className = 'exceptionInput hidden'; 56 57 var select = cr.doc.createElement('select'); 58 var optionAllow = cr.doc.createElement('option'); 59 optionAllow.textContent = templateData.allowException; 60 select.appendChild(optionAllow); 61 62 if (this.enableAskOption) { 63 var optionAsk = cr.doc.createElement('option'); 64 optionAsk.textContent = templateData.askException; 65 select.appendChild(optionAsk); 66 this.optionAsk = optionAsk; 67 } 68 69 if (this.contentType == 'cookies') { 70 var optionSession = cr.doc.createElement('option'); 71 optionSession.textContent = templateData.sessionException; 72 select.appendChild(optionSession); 73 this.optionSession = optionSession; 74 } 75 76 var optionBlock = cr.doc.createElement('option'); 77 optionBlock.textContent = templateData.blockException; 78 select.appendChild(optionBlock); 79 80 this.appendChild(select); 81 select.className = 'exceptionSetting hidden'; 82 83 // Used to track whether the URL pattern in the input is valid. 84 // This will be true if the browser process has informed us that the 85 // current text in the input is valid. Changing the text resets this to 86 // false, and getting a response from the browser sets it back to true. 87 // It starts off as false for empty string (new exceptions) or true for 88 // already-existing exceptions (which we assume are valid). 89 this.inputValidityKnown = this.pattern; 90 // This one tracks the actual validity of the pattern in the input. This 91 // starts off as true so as not to annoy the user when he adds a new and 92 // empty input. 93 this.inputIsValid = true; 94 95 this.patternLabel = patternLabel; 96 this.settingLabel = settingLabel; 97 this.input = input; 98 this.select = select; 99 this.optionAllow = optionAllow; 100 this.optionBlock = optionBlock; 101 102 this.updateEditables(); 103 104 var listItem = this; 105 this.ondblclick = function(event) { 106 listItem.editing = true; 107 }; 108 109 // Handle events on the editable nodes. 110 input.oninput = function(event) { 111 listItem.inputValidityKnown = false; 112 chrome.send('checkExceptionPatternValidity', 113 [listItem.contentType, listItem.mode, input.value]); 114 }; 115 116 // Handles enter and escape which trigger reset and commit respectively. 117 function handleKeydown(e) { 118 // Make sure that the tree does not handle the key. 119 e.stopPropagation(); 120 121 // Calling list.focus blurs the input which will stop editing the list 122 // item. 123 switch (e.keyIdentifier) { 124 case 'U+001B': // Esc 125 // Reset the inputs. 126 listItem.updateEditables(); 127 if (listItem.pattern) 128 listItem.maybeSetPatternValidity(listItem.pattern, true); 129 case 'Enter': 130 if (listItem.parentNode) 131 listItem.parentNode.focus(); 132 } 133 } 134 135 function handleBlur(e) { 136 // When the blur event happens we do not know who is getting focus so we 137 // delay this a bit since we want to know if the other input got focus 138 // before deciding if we should exit edit mode. 139 var doc = e.target.ownerDocument; 140 window.setTimeout(function() { 141 var activeElement = doc.activeElement; 142 if (!listItem.contains(activeElement)) { 143 listItem.editing = false; 144 } 145 }, 50); 146 } 147 148 input.addEventListener('keydown', handleKeydown); 149 input.addEventListener('blur', handleBlur); 150 151 select.addEventListener('keydown', handleKeydown); 152 select.addEventListener('blur', handleBlur); 153 }, 154 155 /** 156 * The pattern (e.g., a URL) for the exception. 157 * @type {string} 158 */ 159 get pattern() { 160 return this.dataItem[0]; 161 }, 162 set pattern(pattern) { 163 this.dataItem[0] = pattern; 164 }, 165 166 /** 167 * The setting (allow/block) for the exception. 168 * @type {string} 169 */ 170 get setting() { 171 return this.dataItem[1]; 172 }, 173 set setting(setting) { 174 this.dataItem[1] = setting; 175 }, 176 177 /** 178 * The embedding origin for the location exception. 179 * @type {string} 180 */ 181 get embeddingOrigin() { 182 return this.dataItem[2]; 183 }, 184 set embeddingOrigin(url) { 185 this.dataItem[2] = url; 186 }, 187 188 /** 189 * Gets a human-readable setting string. 190 * @type {string} 191 */ 192 settingForDisplay: function() { 193 var setting = this.setting; 194 if (setting == 'allow') 195 return templateData.allowException; 196 else if (setting == 'block') 197 return templateData.blockException; 198 else if (setting == 'ask') 199 return templateData.askException; 200 else if (setting == 'session') 201 return templateData.sessionException; 202 }, 203 204 /** 205 * Update this list item to reflect whether the input is a valid pattern 206 * if |pattern| matches the text currently in the input. 207 * @param {string} pattern The pattern. 208 * @param {boolean} valid Whether said pattern is valid in the context of 209 * a content exception setting. 210 */ 211 maybeSetPatternValid: function(pattern, valid) { 212 // Don't do anything for messages where we are not the intended recipient, 213 // or if the response is stale (i.e. the input value has changed since we 214 // sent the request to analyze it). 215 if (pattern != this.input.value) 216 return; 217 218 if (valid) 219 this.input.setCustomValidity(''); 220 else 221 this.input.setCustomValidity(' '); 222 this.inputIsValid = valid; 223 this.inputValidityKnown = true; 224 }, 225 226 /** 227 * Copy the data model values to the editable nodes. 228 */ 229 updateEditables: function() { 230 if (!(this.pattern && this.setting)) 231 return; 232 233 this.input.value = this.pattern; 234 235 if (this.setting == 'allow') 236 this.optionAllow.selected = true; 237 else if (this.setting == 'block') 238 this.optionBlock.selected = true; 239 else if (this.setting == 'session' && this.optionSession) 240 this.optionSession.selected = true; 241 else if (this.setting == 'ask' && this.optionAsk) 242 this.optionAsk.selected = true; 243 }, 244 245 /** 246 * Whether the user is currently able to edit the list item. 247 * @type {boolean} 248 */ 249 get editing() { 250 return this.hasAttribute('editing'); 251 }, 252 set editing(editing) { 253 var oldEditing = this.editing; 254 if (oldEditing == editing) 255 return; 256 257 var listItem = this; 258 var pattern = this.pattern; 259 var setting = this.setting; 260 var patternLabel = this.patternLabel; 261 var settingLabel = this.settingLabel; 262 var input = this.input; 263 var select = this.select; 264 var optionAllow = this.optionAllow; 265 var optionBlock = this.optionBlock; 266 var optionSession = this.optionSession; 267 var optionAsk = this.optionAsk; 268 269 // Check that we have a valid pattern and if not we do not change the 270 // editing mode. 271 if (!editing && (!this.inputValidityKnown || !this.inputIsValid)) { 272 if (this.pattern) { 273 input.focus(); 274 input.select(); 275 } else { 276 // Just delete this row if it was added via the Add button. 277 var model = listItem.parentNode.dataModel; 278 model.splice(model.indexOf(listItem.dataItem), 1); 279 } 280 281 return; 282 } 283 284 patternLabel.classList.toggle('hidden'); 285 settingLabel.classList.toggle('hidden'); 286 input.classList.toggle('hidden'); 287 select.classList.toggle('hidden'); 288 289 var doc = this.ownerDocument; 290 if (editing) { 291 this.setAttribute('editing', ''); 292 cr.ui.limitInputWidth(input, this, 20); 293 input.focus(); 294 input.select(); 295 } else { 296 this.removeAttribute('editing'); 297 298 var newPattern = input.value; 299 300 var newSetting; 301 if (optionAllow.selected) 302 newSetting = 'allow'; 303 else if (optionBlock.selected) 304 newSetting = 'block'; 305 else if (optionSession && optionSession.selected) 306 newSetting = 'session'; 307 else if (optionAsk && optionAsk.selected) 308 newSetting = 'ask'; 309 310 // Empty edit - do nothing. 311 if (pattern == newPattern && newSetting == this.setting) 312 return; 313 314 this.pattern = patternLabel.textContent = newPattern; 315 this.setting = newSetting; 316 settingLabel.textContent = this.settingForDisplay(); 317 318 if (pattern != this.pattern) { 319 chrome.send('removeExceptions', 320 [this.contentType, this.mode, pattern]); 321 } 322 323 chrome.send('setException', 324 [this.contentType, this.mode, this.pattern, this.setting]); 325 } 326 } 327 }; 328 329 /** 330 * Creates a new exceptions list. 331 * @constructor 332 * @extends {cr.ui.List} 333 */ 334 var ExceptionsList = cr.ui.define('list'); 335 336 ExceptionsList.prototype = { 337 __proto__: List.prototype, 338 339 /** 340 * Called when an element is decorated as a list. 341 */ 342 decorate: function() { 343 List.prototype.decorate.call(this); 344 345 this.dataModel = new ArrayDataModel([]); 346 347 // Whether the exceptions in this list allow an 'Ask every time' option. 348 this.enableAskOption = false; 349 }, 350 351 /** 352 * Creates an item to go in the list. 353 * @param {Object} entry The element from the data model for this row. 354 */ 355 createItem: function(entry) { 356 return new ExceptionsListItem(this.contentType, 357 this.mode, 358 this.enableAskOption, 359 entry); 360 }, 361 362 /** 363 * Adds an exception to the js model. 364 * @param {Array} entry A pair of the form [filter, setting]. 365 */ 366 addException: function(entry) { 367 this.dataModel.push(entry); 368 369 // When an empty row is added, put it into editing mode. 370 if (!entry[0] && !entry[1]) { 371 var index = this.dataModel.length - 1; 372 var sm = this.selectionModel; 373 sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index; 374 this.scrollIndexIntoView(index); 375 var li = this.getListItemByIndex(index); 376 li.editing = true; 377 } 378 }, 379 380 /** 381 * The browser has finished checking a pattern for validity. Update the 382 * list item to reflect this. 383 * @param {string} pattern The pattern. 384 * @param {bool} valid Whether said pattern is valid in the context of 385 * a content exception setting. 386 */ 387 patternValidityCheckComplete: function(pattern, valid) { 388 for (var i = 0; i < this.dataModel.length; i++) { 389 var listItem = this.getListItemByIndex(i); 390 if (listItem) 391 listItem.maybeSetPatternValid(pattern, valid); 392 } 393 }, 394 395 /** 396 * Removes all exceptions from the js model. 397 */ 398 clear: function() { 399 this.dataModel = new ArrayDataModel([]); 400 }, 401 402 /** 403 * Removes all selected rows from browser's model. 404 */ 405 removeSelectedRows: function() { 406 // The first member is the content type; the rest of the values are 407 // the patterns we are removing. 408 var args = [this.contentType]; 409 var selectedItems = this.selectedItems; 410 for (var i = 0; i < selectedItems.length; i++) { 411 if (this.contentType == 'location') { 412 args.push(selectedItems[i][0]); // origin (pattern) 413 args.push(selectedItems[i][2]); // embedded origin 414 } else if (this.contentType == 'notifications') { 415 args.push(selectedItems[i][0]); // origin (pattern) 416 args.push(selectedItems[i][1]); // setting 417 } else { 418 args.push(this.mode); // browser mode 419 args.push(selectedItems[i][0]); // pattern 420 } 421 } 422 423 chrome.send('removeExceptions', args); 424 }, 425 426 /** 427 * Puts the selected row in editing mode. 428 */ 429 editSelectedRow: function() { 430 var li = this.getListItem(this.selectedItem); 431 if (li) 432 li.editing = true; 433 } 434 }; 435 436 var ExceptionsArea = cr.ui.define('div'); 437 438 ExceptionsArea.prototype = { 439 __proto__: HTMLDivElement.prototype, 440 441 decorate: function() { 442 // TODO(estade): need some sort of visual indication when the list is 443 // empty. 444 this.exceptionsList = this.querySelector('list'); 445 this.exceptionsList.contentType = this.contentType; 446 this.exceptionsList.mode = this.mode; 447 448 ExceptionsList.decorate(this.exceptionsList); 449 this.exceptionsList.selectionModel.addEventListener( 450 'change', this.handleOnSelectionChange_.bind(this)); 451 452 var self = this; 453 if (this.contentType != 'location' && 454 this.contentType != 'notifications') { 455 var addRow = cr.doc.createElement('button'); 456 addRow.textContent = templateData.addExceptionRow; 457 this.appendChild(addRow); 458 459 addRow.onclick = function(event) { 460 self.exceptionsList.addException(['', '']); 461 }; 462 463 var editRow = cr.doc.createElement('button'); 464 editRow.textContent = templateData.editExceptionRow; 465 this.appendChild(editRow); 466 this.editRow = editRow; 467 468 editRow.onclick = function(event) { 469 self.exceptionsList.editSelectedRow(); 470 }; 471 } 472 473 var removeRow = cr.doc.createElement('button'); 474 removeRow.textContent = templateData.removeExceptionRow; 475 this.appendChild(removeRow); 476 this.removeRow = removeRow; 477 478 removeRow.onclick = function(event) { 479 self.exceptionsList.removeSelectedRows(); 480 }; 481 482 this.updateButtonSensitivity(); 483 484 this.classList.add('hidden'); 485 486 this.otrProfileExists = false; 487 }, 488 489 /** 490 * The content type for this exceptions area, such as 'images'. 491 * @type {string} 492 */ 493 get contentType() { 494 return this.getAttribute('contentType'); 495 }, 496 set contentType(type) { 497 return this.setAttribute('contentType', type); 498 }, 499 500 /** 501 * The browser mode type for this exceptions area, 'otr' or 'normal'. 502 * @type {string} 503 */ 504 get mode() { 505 return this.getAttribute('mode'); 506 }, 507 set mode(mode) { 508 return this.setAttribute('mode', mode); 509 }, 510 511 /** 512 * Update the enabled/disabled state of the editing buttons based on which 513 * rows are selected. 514 */ 515 updateButtonSensitivity: function() { 516 var selectionSize = this.exceptionsList.selectedItems.length; 517 if (this.editRow) 518 this.editRow.disabled = selectionSize != 1; 519 this.removeRow.disabled = selectionSize == 0; 520 }, 521 522 /** 523 * Callback from the selection model. 524 * @param {!cr.Event} ce Event with change info. 525 * @private 526 */ 527 handleOnSelectionChange_: function(ce) { 528 this.updateButtonSensitivity(); 529 }, 530 }; 531 532 return { 533 ExceptionsListItem: ExceptionsListItem, 534 ExceptionsList: ExceptionsList, 535 ExceptionsArea: ExceptionsArea 536 }; 537}); 538