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