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