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.
5 cr.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;
13 * Creates a new exceptions list item.
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
22 * @extends {options.InlineEditableItem}
24 function ExceptionsListItem(contentType, mode, enableAskOption, exception) {
25 var el = cr.doc.createElement('div');
27 el.contentType = contentType;
28 el.enableAskOption = enableAskOption;
29 el.dataItem = exception;
30 el.__proto__ = ExceptionsListItem.prototype;
36 ExceptionsListItem.prototype = {
37 __proto__: InlineEditableItem.prototype,
40 * Called when an element is decorated as a list item.
42 decorate: function() {
43 InlineEditableItem.prototype.decorate.call(this);
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);
51 this.patternLabel = patternCell.querySelector('.static-text');
52 var input = patternCell.querySelector('input');
54 // TODO(stuartmorgan): Create an createEditableSelectCell abstracting
56 // Setting label for display mode. |pattern| will be null for the 'add new
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;
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);
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);
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);
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);
95 if (this.isEmbeddingRule()) {
96 this.patternLabel.classList.add('sublabel');
97 this.editable = false;
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;
107 if (this.contentType != 'zoomlevels') {
108 this.addEditField(select, this.settingLabel);
109 this.contentElement.appendChild(select);
111 select.className = 'exception-setting';
112 select.setAttribute('aria-labelledby', 'exception-behavior-column');
115 select.setAttribute('displaymode', 'edit');
117 if (this.contentType == 'media-stream') {
118 this.settingLabel.classList.add('media-audio-setting');
120 var videoSettingLabel = cr.doc.createElement('span');
121 videoSettingLabel.textContent = this.videoSettingForDisplay();
122 videoSettingLabel.className = 'exception-setting';
123 videoSettingLabel.classList.add('media-video-setting');
124 videoSettingLabel.setAttribute('displaymode', 'static');
125 this.contentElement.appendChild(videoSettingLabel);
128 if (this.contentType == 'zoomlevels') {
129 this.deletable = true;
130 this.editable = false;
132 var zoomLabel = cr.doc.createElement('span');
133 zoomLabel.textContent = this.dataItem.zoom;
134 zoomLabel.className = 'exception-setting';
135 zoomLabel.setAttribute('displaymode', 'static');
136 zoomLabel.setAttribute('aria-labelledby', 'exception-zoom-column');
137 this.contentElement.appendChild(zoomLabel);
138 this.zoomLabel = zoomLabel;
141 // Used to track whether the URL pattern in the input is valid.
142 // This will be true if the browser process has informed us that the
143 // current text in the input is valid. Changing the text resets this to
144 // false, and getting a response from the browser sets it back to true.
145 // It starts off as false for empty string (new exceptions) or true for
146 // already-existing exceptions (which we assume are valid).
147 this.inputValidityKnown = this.pattern;
148 // This one tracks the actual validity of the pattern in the input. This
149 // starts off as true so as not to annoy the user when he adds a new and
151 this.inputIsValid = true;
154 this.select = select;
156 this.updateEditables();
158 // Editing notifications, geolocation and media-stream is disabled for
160 if (this.contentType == 'notifications' ||
161 this.contentType == 'location' ||
162 this.contentType == 'media-stream') {
163 this.editable = false;
166 // If the source of the content setting exception is not a user
167 // preference, that source controls the exception and the user cannot edit
170 this.dataItem.source && this.dataItem.source != 'preference' ?
171 this.dataItem.source : null;
174 this.setAttribute('controlled-by', controlledBy);
175 this.deletable = false;
176 this.editable = false;
179 if (controlledBy == 'policy' || controlledBy == 'extension') {
180 this.querySelector('.row-delete-button').hidden = true;
181 var indicator = ControlledSettingIndicator();
182 indicator.setAttribute('content-exception', this.contentType);
183 // Create a synthetic pref change event decorated as
184 // CoreOptionsHandler::CreateValueForPref() does.
185 var event = new Event(this.contentType);
186 event.value = { controlledBy: controlledBy };
187 indicator.handlePrefChange(event);
188 this.appendChild(indicator);
191 // If the exception comes from a hosted app, display the name and the
193 if (controlledBy == 'HostedApp') {
195 loadTimeData.getString('set_by') + ' ' + this.dataItem.appName;
196 var button = this.querySelector('.row-delete-button');
197 // Use the host app's favicon (16px, match bigger size).
198 // See c/b/ui/webui/extensions/extension_icon_source.h
199 // for a description of the chrome://extension-icon URL.
200 button.style.backgroundImage =
201 'url(\'chrome://extension-icon/' + this.dataItem.appId + '/16/1\')';
205 // Handle events on the editable nodes.
206 input.oninput = function(event) {
207 listItem.inputValidityKnown = false;
208 chrome.send('checkExceptionPatternValidity',
209 [listItem.contentType, listItem.mode, input.value]);
212 // Listen for edit events.
213 this.addEventListener('canceledit', this.onEditCancelled_);
214 this.addEventListener('commitedit', this.onEditCommitted_);
217 isEmbeddingRule: function() {
218 return this.dataItem.embeddingOrigin &&
219 this.dataItem.embeddingOrigin !== this.dataItem.origin;
223 * The pattern (e.g., a URL) for the exception.
228 if (!this.isEmbeddingRule()) {
229 return this.dataItem.origin;
231 return loadTimeData.getStringF('embeddedOnHost',
232 this.dataItem.embeddingOrigin);
235 return this.dataItem.displayPattern;
237 set pattern(pattern) {
239 console.error('Tried to change uneditable pattern');
241 this.dataItem.displayPattern = pattern;
245 * The setting (allow/block) for the exception.
250 return this.dataItem.setting;
252 set setting(setting) {
253 this.dataItem.setting = setting;
257 * Gets a human-readable setting string.
259 * @return {string} The display string.
261 settingForDisplay: function() {
262 return this.getDisplayStringForSetting(this.setting);
266 * media video specific function.
267 * Gets a human-readable video setting string.
269 * @return {string} The display string.
271 videoSettingForDisplay: function() {
272 return this.getDisplayStringForSetting(this.dataItem.video);
276 * Gets a human-readable display string for setting.
278 * @param {string} setting The setting to be displayed.
279 * @return {string} The display string.
281 getDisplayStringForSetting: function(setting) {
282 if (setting == 'allow')
283 return loadTimeData.getString('allowException');
284 else if (setting == 'block')
285 return loadTimeData.getString('blockException');
286 else if (setting == 'ask')
287 return loadTimeData.getString('askException');
288 else if (setting == 'session')
289 return loadTimeData.getString('sessionException');
290 else if (setting == 'default')
293 console.error('Unknown setting: [' + setting + ']');
298 * Update this list item to reflect whether the input is a valid pattern.
300 * @param {boolean} valid Whether said pattern is valid in the context of a
301 * content exception setting.
303 setPatternValid: function(valid) {
304 if (valid || !this.input.value)
305 this.input.setCustomValidity('');
307 this.input.setCustomValidity(' ');
308 this.inputIsValid = valid;
309 this.inputValidityKnown = true;
313 * Set the <input> to its original contents. Used when the user quits
316 resetInput: function() {
317 this.input.value = this.pattern;
321 * Copy the data model values to the editable nodes.
323 updateEditables: function() {
327 this.select.querySelector('[value=\'' + this.setting + '\']');
329 settingOption.selected = true;
333 get currentInputIsValid() {
334 return this.inputValidityKnown && this.inputIsValid;
338 get hasBeenEdited() {
339 var livePattern = this.input.value;
340 var liveSetting = this.select.value;
341 return livePattern != this.pattern || liveSetting != this.setting;
345 * Called when committing an edit.
347 * @param {Event} e The end event.
350 onEditCommitted_: function(e) {
351 var newPattern = this.input.value;
352 var newSetting = this.select.value;
354 this.finishEdit(newPattern, newSetting);
358 * Called when cancelling an edit; resets the control states.
360 * @param {Event} e The cancel event.
363 onEditCancelled_: function() {
364 this.updateEditables();
365 this.setPatternValid(true);
369 * Editing is complete; update the model.
371 * @param {string} newPattern The pattern that the user entered.
372 * @param {string} newSetting The setting the user chose.
374 finishEdit: function(newPattern, newSetting) {
375 this.patternLabel.textContent = newPattern;
376 this.settingLabel.textContent = this.settingForDisplay();
377 var oldPattern = this.pattern;
378 this.pattern = newPattern;
379 this.setting = newSetting;
381 // TODO(estade): this will need to be updated if geolocation/notifications
383 if (oldPattern != newPattern) {
384 chrome.send('removeException',
385 [this.contentType, this.mode, oldPattern]);
388 chrome.send('setException',
389 [this.contentType, this.mode, newPattern, newSetting]);
394 * Creates a new list item for the Add New Item row, which doesn't represent
395 * an actual entry in the exceptions list but allows the user to add new
398 * @param {string} contentType The type of the list.
399 * @param {string} mode The browser mode, 'otr' or 'normal'.
400 * @param {boolean} enableAskOption Whether to show an 'ask every time' option
403 * @extends {cr.ui.ExceptionsListItem}
405 function ExceptionsAddRowListItem(contentType, mode, enableAskOption) {
406 var el = cr.doc.createElement('div');
408 el.contentType = contentType;
409 el.enableAskOption = enableAskOption;
411 el.__proto__ = ExceptionsAddRowListItem.prototype;
417 ExceptionsAddRowListItem.prototype = {
418 __proto__: ExceptionsListItem.prototype,
420 decorate: function() {
421 ExceptionsListItem.prototype.decorate.call(this);
423 this.input.placeholder =
424 loadTimeData.getString('addNewExceptionInstructions');
426 // Do we always want a default of allow?
427 this.setting = 'allow';
431 * Clear the <input> and let the placeholder text show again.
433 resetInput: function() {
434 this.input.value = '';
438 get hasBeenEdited() {
439 return this.input.value != '';
443 * Editing is complete; update the model. As long as the pattern isn't
444 * empty, we'll just add it.
446 * @param {string} newPattern The pattern that the user entered.
447 * @param {string} newSetting The setting the user chose.
449 finishEdit: function(newPattern, newSetting) {
451 chrome.send('setException',
452 [this.contentType, this.mode, newPattern, newSetting]);
457 * Creates a new exceptions list.
460 * @extends {cr.ui.List}
462 var ExceptionsList = cr.ui.define('list');
464 ExceptionsList.prototype = {
465 __proto__: InlineEditableItemList.prototype,
468 * Called when an element is decorated as a list.
470 decorate: function() {
471 InlineEditableItemList.prototype.decorate.call(this);
473 this.classList.add('settings-list');
475 for (var parentNode = this.parentNode; parentNode;
476 parentNode = parentNode.parentNode) {
477 if (parentNode.hasAttribute('contentType')) {
478 this.contentType = parentNode.getAttribute('contentType');
483 this.mode = this.getAttribute('mode');
485 // Whether the exceptions in this list allow an 'Ask every time' option.
486 this.enableAskOption = this.contentType == 'plugins';
488 this.autoExpands = true;
493 * Creates an item to go in the list.
495 * @param {Object} entry The element from the data model for this row.
497 createItem: function(entry) {
499 return new ExceptionsListItem(this.contentType,
501 this.enableAskOption,
504 var addRowItem = new ExceptionsAddRowListItem(this.contentType,
506 this.enableAskOption);
507 addRowItem.deletable = false;
513 * Sets the exceptions in the js model.
515 * @param {Object} entries A list of dictionaries of values, each dictionary
516 * represents an exception.
518 setExceptions: function(entries) {
519 var deleteCount = this.dataModel.length;
521 if (this.isEditable()) {
522 // We don't want to remove the Add New Exception row.
523 deleteCount = deleteCount - 1;
526 var args = [0, deleteCount];
527 args.push.apply(args, entries);
528 this.dataModel.splice.apply(this.dataModel, args);
532 * The browser has finished checking a pattern for validity. Update the list
533 * item to reflect this.
535 * @param {string} pattern The pattern.
536 * @param {bool} valid Whether said pattern is valid in the context of a
537 * content exception setting.
539 patternValidityCheckComplete: function(pattern, valid) {
540 var listItems = this.items;
541 for (var i = 0; i < listItems.length; i++) {
542 var listItem = listItems[i];
543 // Don't do anything for messages for the item if it is not the intended
544 // recipient, or if the response is stale (i.e. the input value has
545 // changed since we sent the request to analyze it).
546 if (pattern == listItem.input.value)
547 listItem.setPatternValid(valid);
552 * Returns whether the rows are editable in this list.
554 isEditable: function() {
555 // Exceptions of the following lists are not editable for now.
556 return !(this.contentType == 'notifications' ||
557 this.contentType == 'location' ||
558 this.contentType == 'fullscreen' ||
559 this.contentType == 'media-stream' ||
560 this.contentType == 'zoomlevels');
564 * Removes all exceptions from the js model.
567 if (this.isEditable()) {
568 // The null creates the Add New Exception row.
569 this.dataModel = new ArrayDataModel([null]);
571 this.dataModel = new ArrayDataModel([]);
576 deleteItemAtIndex: function(index) {
577 var listItem = this.getListItemByIndex(index);
578 if (!listItem.deletable)
581 var dataItem = listItem.dataItem;
582 var args = [listItem.contentType];
583 if (listItem.contentType == 'notifications')
584 args.push(dataItem.origin, dataItem.setting);
586 args.push(listItem.mode, dataItem.origin, dataItem.embeddingOrigin);
588 chrome.send('removeException', args);
592 var Page = cr.ui.pageManager.Page;
593 var PageManager = cr.ui.pageManager.PageManager;
596 * Encapsulated handling of content settings list subpage.
600 function ContentSettingsExceptionsArea() {
601 Page.call(this, 'contentExceptions',
602 loadTimeData.getString('contentSettingsPageTabTitle'),
603 'content-settings-exceptions-area');
606 cr.addSingletonGetter(ContentSettingsExceptionsArea);
608 ContentSettingsExceptionsArea.prototype = {
609 __proto__: Page.prototype,
612 initializePage: function() {
613 Page.prototype.initializePage.call(this);
615 var exceptionsLists = this.pageDiv.querySelectorAll('list');
616 for (var i = 0; i < exceptionsLists.length; i++) {
617 options.contentSettings.ExceptionsList.decorate(exceptionsLists[i]);
620 ContentSettingsExceptionsArea.hideOTRLists(false);
622 // If the user types in the URL without a hash, show just cookies.
623 this.showList('cookies');
625 $('content-settings-exceptions-overlay-confirm').onclick =
626 PageManager.closeOverlay.bind(PageManager);
630 * Shows one list and hides all others.
632 * @param {string} type The content type.
634 showList: function(type) {
635 // Update the title for the type that was shown.
636 this.title = loadTimeData.getString(type + 'TabTitle');
638 var header = this.pageDiv.querySelector('h1');
639 header.textContent = loadTimeData.getString(type + '_header');
641 var divs = this.pageDiv.querySelectorAll('div[contentType]');
642 for (var i = 0; i < divs.length; i++) {
643 if (divs[i].getAttribute('contentType') == type)
644 divs[i].hidden = false;
646 divs[i].hidden = true;
649 var mediaHeader = this.pageDiv.querySelector('.media-header');
650 mediaHeader.hidden = type != 'media-stream';
652 $('exception-behavior-column').hidden = type == 'zoomlevels';
653 $('exception-zoom-column').hidden = type != 'zoomlevels';
657 * Called after the page has been shown. Show the content type for the
660 didShowPage: function() {
661 var hash = location.hash;
663 this.showList(hash.slice(1));
668 * Called when the last incognito window is closed.
670 ContentSettingsExceptionsArea.OTRProfileDestroyed = function() {
671 this.hideOTRLists(true);
675 * Hides the incognito exceptions lists and optionally clears them as well.
676 * @param {boolean} clear Whether to clear the lists.
678 ContentSettingsExceptionsArea.hideOTRLists = function(clear) {
679 var otrLists = document.querySelectorAll('list[mode=otr]');
681 for (var i = 0; i < otrLists.length; i++) {
682 otrLists[i].parentNode.hidden = true;
689 ExceptionsListItem: ExceptionsListItem,
690 ExceptionsAddRowListItem: ExceptionsAddRowListItem,
691 ExceptionsList: ExceptionsList,
692 ContentSettingsExceptionsArea: ContentSettingsExceptionsArea,