Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / options / content_settings_exceptions_area.js
blob273c130f2182d66a095ade3ccb7093ae445453a6
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    * Returns whether exceptions list for the type is editable.
14    *
15    * @param {string} contentType The type of the list.
16    */
17   function IsEditableType(contentType) {
18     // Exceptions of the following lists are not editable for now.
19     return !(contentType == 'location' ||
20              contentType == 'fullscreen' ||
21              contentType == 'media-stream-mic' ||
22              contentType == 'media-stream-camera' ||
23              contentType == 'midi-sysex' ||
24              contentType == 'zoomlevels');
25   }
27   /**
28    * Creates a new exceptions list item.
29    *
30    * @param {string} contentType The type of the list.
31    * @param {string} mode The browser mode, 'otr' or 'normal'.
32    * @param {Object} exception A dictionary that contains the data of the
33    *     exception.
34    * @constructor
35    * @extends {options.InlineEditableItem}
36    */
37   function ExceptionsListItem(contentType, mode, exception) {
38     var el = cr.doc.createElement('div');
39     el.mode = mode;
40     el.contentType = contentType;
41     el.dataItem = exception;
42     el.__proto__ = ExceptionsListItem.prototype;
43     el.decorate();
45     return el;
46   }
48   ExceptionsListItem.prototype = {
49     __proto__: InlineEditableItem.prototype,
51     /**
52      * Called when an element is decorated as a list item.
53      */
54     decorate: function() {
55       InlineEditableItem.prototype.decorate.call(this);
57       this.isPlaceholder = !this.pattern;
58       var patternCell = this.createEditableTextCell(this.pattern);
59       patternCell.className = 'exception-pattern';
60       patternCell.classList.add('weakrtl');
61       this.contentElement.appendChild(patternCell);
62       if (this.pattern)
63         this.patternLabel = patternCell.querySelector('.static-text');
64       var input = patternCell.querySelector('input');
66       // TODO(stuartmorgan): Create an createEditableSelectCell abstracting
67       // this code.
68       // Setting label for display mode. |pattern| will be null for the 'add new
69       // exception' row.
70       if (this.pattern) {
71         var settingLabel = cr.doc.createElement('span');
72         settingLabel.textContent = this.settingForDisplay();
73         settingLabel.className = 'exception-setting';
74         settingLabel.setAttribute('displaymode', 'static');
75         this.contentElement.appendChild(settingLabel);
76         this.settingLabel = settingLabel;
77       }
79       // Setting select element for edit mode.
80       var select = cr.doc.createElement('select');
81       var optionAllow = cr.doc.createElement('option');
82       optionAllow.textContent = loadTimeData.getString('allowException');
83       optionAllow.value = 'allow';
84       select.appendChild(optionAllow);
86       if (this.contentType == 'plugins') {
87         var optionDetect = cr.doc.createElement('option');
88         optionDetect.textContent = loadTimeData.getString('detectException');
89         optionDetect.value = 'detect';
90         select.appendChild(optionDetect);
91       }
93       if (this.contentType == 'cookies') {
94         var optionSession = cr.doc.createElement('option');
95         optionSession.textContent = loadTimeData.getString('sessionException');
96         optionSession.value = 'session';
97         select.appendChild(optionSession);
98       }
100       if (this.contentType != 'fullscreen') {
101         var optionBlock = cr.doc.createElement('option');
102         optionBlock.textContent = loadTimeData.getString('blockException');
103         optionBlock.value = 'block';
104         select.appendChild(optionBlock);
105       }
107       if (this.isEmbeddingRule()) {
108         this.patternLabel.classList.add('sublabel');
109         this.editable = false;
110       }
112       if (this.setting == 'default') {
113         // Items that don't have their own settings (parents of 'embedded on'
114         // items) aren't deletable.
115         this.deletable = false;
116         this.editable = false;
117       }
119       if (this.contentType != 'zoomlevels') {
120         this.addEditField(select, this.settingLabel);
121         this.contentElement.appendChild(select);
122       }
123       select.className = 'exception-setting';
124       select.setAttribute('aria-labelledby', 'exception-behavior-column');
126       if (this.pattern)
127         select.setAttribute('displaymode', 'edit');
129       if (this.contentType == 'zoomlevels') {
130         this.deletable = true;
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;
139       }
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
150       // empty input.
151       this.inputIsValid = true;
153       this.input = input;
154       this.select = select;
156       this.updateEditables();
157       this.editable = this.editable && IsEditableType(this.contentType);
159       // If the source of the content setting exception is not a user
160       // preference, that source controls the exception and the user cannot edit
161       // or delete it.
162       var controlledBy =
163           this.dataItem.source && this.dataItem.source != 'preference' ?
164               this.dataItem.source : null;
166       if (controlledBy) {
167         this.setAttribute('controlled-by', controlledBy);
168         this.deletable = false;
169         this.editable = false;
170       }
172       if (controlledBy == 'policy' || controlledBy == 'extension') {
173         this.querySelector('.row-delete-button').hidden = true;
174         var indicator = new ControlledSettingIndicator();
175         indicator.setAttribute('content-exception', this.contentType);
176         // Create a synthetic pref change event decorated as
177         // CoreOptionsHandler::CreateValueForPref() does.
178         var event = new Event(this.contentType);
179         event.value = { controlledBy: controlledBy };
180         indicator.handlePrefChange(event);
181         this.appendChild(indicator);
182       }
184       // If the exception comes from a hosted app, display the name and the
185       // icon of the app.
186       if (controlledBy == 'HostedApp') {
187         this.title =
188             loadTimeData.getString('setBy') + ' ' + this.dataItem.appName;
189         var button = this.querySelector('.row-delete-button');
190         // Use the host app's favicon (16px, match bigger size).
191         // See c/b/ui/webui/extensions/extension_icon_source.h
192         // for a description of the chrome://extension-icon URL.
193         button.style.backgroundImage =
194             'url(\'chrome://extension-icon/' + this.dataItem.appId + '/16/1\')';
195       }
197       var listItem = this;
198       // Handle events on the editable nodes.
199       input.oninput = function(event) {
200         listItem.inputValidityKnown = false;
201         chrome.send('checkExceptionPatternValidity',
202                     [listItem.contentType, listItem.mode, input.value]);
203       };
205       // Listen for edit events.
206       this.addEventListener('canceledit', this.onEditCancelled_);
207       this.addEventListener('commitedit', this.onEditCommitted_);
208     },
210     isEmbeddingRule: function() {
211       return this.dataItem.embeddingOrigin &&
212           this.dataItem.embeddingOrigin !== this.dataItem.origin;
213     },
215     /**
216      * The pattern (e.g., a URL) for the exception.
217      *
218      * @type {string}
219      */
220     get pattern() {
221       if (!this.isEmbeddingRule())
222         return this.dataItem.origin;
224       return loadTimeData.getStringF('embeddedOnHost',
225                                      this.dataItem.embeddingOrigin);
226     },
227     set pattern(pattern) {
228       if (!this.editable)
229         console.error('Tried to change uneditable pattern');
231       this.dataItem.displayPattern = pattern;
232     },
234     /**
235      * The setting (allow/block) for the exception.
236      *
237      * @type {string}
238      */
239     get setting() {
240       return this.dataItem.setting;
241     },
242     set setting(setting) {
243       this.dataItem.setting = setting;
244     },
246     /**
247      * Gets a human-readable setting string.
248      *
249      * @return {string} The display string.
250      */
251     settingForDisplay: function() {
252       return this.getDisplayStringForSetting(this.setting);
253     },
255     /**
256      * Gets a human-readable display string for setting.
257      *
258      * @param {string} setting The setting to be displayed.
259      * @return {string} The display string.
260      */
261     getDisplayStringForSetting: function(setting) {
262       if (setting == 'allow')
263         return loadTimeData.getString('allowException');
264       else if (setting == 'block')
265         return loadTimeData.getString('blockException');
266       else if (setting == 'ask')
267         return loadTimeData.getString('askException');
268       else if (setting == 'session')
269         return loadTimeData.getString('sessionException');
270       else if (setting == 'detect')
271         return loadTimeData.getString('detectException');
272       else if (setting == 'default')
273         return '';
275       console.error('Unknown setting: [' + setting + ']');
276       return '';
277     },
279     /**
280      * Update this list item to reflect whether the input is a valid pattern.
281      *
282      * @param {boolean} valid Whether said pattern is valid in the context of a
283      *     content exception setting.
284      */
285     setPatternValid: function(valid) {
286       if (valid || !this.input.value)
287         this.input.setCustomValidity('');
288       else
289         this.input.setCustomValidity(' ');
290       this.inputIsValid = valid;
291       this.inputValidityKnown = true;
292     },
294     /**
295      * Set the <input> to its original contents. Used when the user quits
296      * editing.
297      */
298     resetInput: function() {
299       this.input.value = this.pattern;
300     },
302     /**
303      * Copy the data model values to the editable nodes.
304      */
305     updateEditables: function() {
306       this.resetInput();
308       var settingOption =
309           this.select.querySelector('[value=\'' + this.setting + '\']');
310       if (settingOption)
311         settingOption.selected = true;
312     },
314     /** @override */
315     get currentInputIsValid() {
316       return this.inputValidityKnown && this.inputIsValid;
317     },
319     /** @override */
320     get hasBeenEdited() {
321       var livePattern = this.input.value;
322       var liveSetting = this.select.value;
323       return livePattern != this.pattern || liveSetting != this.setting;
324     },
326     /**
327      * Called when committing an edit.
328      *
329      * @param {Event} e The end event.
330      * @private
331      */
332     onEditCommitted_: function(e) {
333       var newPattern = this.input.value;
334       var newSetting = this.select.value;
336       this.finishEdit(newPattern, newSetting);
337     },
339     /**
340      * Called when cancelling an edit; resets the control states.
341      *
342      * @param {Event} e The cancel event.
343      * @private
344      */
345     onEditCancelled_: function(e) {
346       this.updateEditables();
347       this.setPatternValid(true);
348     },
350     /**
351      * Editing is complete; update the model.
352      *
353      * @param {string} newPattern The pattern that the user entered.
354      * @param {string} newSetting The setting the user chose.
355      */
356     finishEdit: function(newPattern, newSetting) {
357       this.patternLabel.textContent = newPattern;
358       this.settingLabel.textContent = this.settingForDisplay();
359       var oldPattern = this.pattern;
360       this.pattern = newPattern;
361       this.setting = newSetting;
363       // TODO(estade): this will need to be updated if geolocation/notifications
364       // become editable.
365       if (oldPattern != newPattern) {
366         chrome.send('removeException',
367                     [this.contentType, this.mode, oldPattern]);
368       }
370       chrome.send('setException',
371                   [this.contentType, this.mode, newPattern, newSetting]);
372     },
373   };
375   /**
376    * Creates a new list item for the Add New Item row, which doesn't represent
377    * an actual entry in the exceptions list but allows the user to add new
378    * exceptions.
379    *
380    * @param {string} contentType The type of the list.
381    * @param {string} mode The browser mode, 'otr' or 'normal'.
382    * @constructor
383    * @extends {options.contentSettings.ExceptionsListItem}
384    */
385   function ExceptionsAddRowListItem(contentType, mode) {
386     var el = cr.doc.createElement('div');
387     el.mode = mode;
388     el.contentType = contentType;
389     el.dataItem = [];
390     el.__proto__ = ExceptionsAddRowListItem.prototype;
391     el.decorate();
393     return el;
394   }
396   ExceptionsAddRowListItem.prototype = {
397     __proto__: ExceptionsListItem.prototype,
399     decorate: function() {
400       ExceptionsListItem.prototype.decorate.call(this);
402       this.input.placeholder =
403           loadTimeData.getString('addNewExceptionInstructions');
405       // Do we always want a default of allow?
406       this.setting = 'allow';
407     },
409     /**
410      * Clear the <input> and let the placeholder text show again.
411      */
412     resetInput: function() {
413       this.input.value = '';
414     },
416     /** @override */
417     get hasBeenEdited() {
418       return this.input.value != '';
419     },
421     /**
422      * Editing is complete; update the model. As long as the pattern isn't
423      * empty, we'll just add it.
424      *
425      * @param {string} newPattern The pattern that the user entered.
426      * @param {string} newSetting The setting the user chose.
427      */
428     finishEdit: function(newPattern, newSetting) {
429       this.resetInput();
430       chrome.send('setException',
431                   [this.contentType, this.mode, newPattern, newSetting]);
432     },
433   };
435   /**
436    * Creates a new exceptions list.
437    *
438    * @constructor
439    * @extends {options.InlineEditableItemList}
440    */
441   var ExceptionsList = cr.ui.define('list');
443   ExceptionsList.prototype = {
444     __proto__: InlineEditableItemList.prototype,
446     /**
447      * Called when an element is decorated as a list.
448      */
449     decorate: function() {
450       InlineEditableItemList.prototype.decorate.call(this);
452       this.classList.add('settings-list');
454       for (var parentNode = this.parentNode; parentNode;
455            parentNode = parentNode.parentNode) {
456         if (parentNode.hasAttribute('contentType')) {
457           this.contentType = parentNode.getAttribute('contentType');
458           break;
459         }
460       }
462       if (!this.isEditable())
463         this.tabIndex = 0;
465       this.mode = this.getAttribute('mode');
466       this.autoExpands = true;
467       this.reset();
468     },
470     /**
471      * Creates an item to go in the list.
472      *
473      * @param {Object} entry The element from the data model for this row.
474      */
475     createItem: function(entry) {
476       if (entry) {
477         return new ExceptionsListItem(this.contentType,
478                                       this.mode,
479                                       entry);
480       } else {
481         var addRowItem = new ExceptionsAddRowListItem(this.contentType,
482                                                       this.mode);
483         addRowItem.deletable = false;
484         return addRowItem;
485       }
486     },
488     /**
489      * Sets the exceptions in the js model.
490      *
491      * @param {Array<options.Exception>} entries A list of dictionaries of
492      *     values, each dictionary represents an exception.
493      */
494     setExceptions: function(entries) {
495       var deleteCount = this.dataModel.length;
497       if (this.isEditable()) {
498         // We don't want to remove the Add New Exception row.
499         deleteCount = deleteCount - 1;
500       }
502       var args = [0, deleteCount];
503       args.push.apply(args, entries);
504       this.dataModel.splice.apply(this.dataModel, args);
505     },
507     /**
508      * The browser has finished checking a pattern for validity. Update the list
509      * item to reflect this.
510      *
511      * @param {string} pattern The pattern.
512      * @param {boolean} valid Whether said pattern is valid in the context of a
513      *     content exception setting.
514      */
515     patternValidityCheckComplete: function(pattern, valid) {
516       var listItems = this.items;
517       for (var i = 0; i < listItems.length; i++) {
518         var listItem = listItems[i];
519         // Don't do anything for messages for the item if it is not the intended
520         // recipient, or if the response is stale (i.e. the input value has
521         // changed since we sent the request to analyze it).
522         if (pattern == listItem.input.value)
523           listItem.setPatternValid(valid);
524       }
525     },
527     /**
528      * Returns whether the rows are editable in this list.
529      */
530     isEditable: function() {
531       // Exceptions of the following lists are not editable for now.
532       return IsEditableType(this.contentType);
533     },
535     /**
536      * Removes all exceptions from the js model.
537      */
538     reset: function() {
539       if (this.isEditable()) {
540         // The null creates the Add New Exception row.
541         this.dataModel = new ArrayDataModel([null]);
542       } else {
543         this.dataModel = new ArrayDataModel([]);
544       }
545     },
547     /** @override */
548     deleteItemAtIndex: function(index) {
549       var listItem = this.getListItemByIndex(index);
550       if (!listItem.deletable)
551         return;
553       var dataItem = listItem.dataItem;
554       chrome.send('removeException', [listItem.contentType,
555                                       listItem.mode,
556                                       dataItem.origin,
557                                       dataItem.embeddingOrigin]);
558     },
559   };
561   var Page = cr.ui.pageManager.Page;
562   var PageManager = cr.ui.pageManager.PageManager;
564   /**
565    * Encapsulated handling of content settings list subpage.
566    *
567    * @constructor
568    * @extends {cr.ui.pageManager.Page}
569    */
570   function ContentSettingsExceptionsArea() {
571     Page.call(this, 'contentExceptions',
572               loadTimeData.getString('contentSettingsPageTabTitle'),
573               'content-settings-exceptions-area');
574   }
576   cr.addSingletonGetter(ContentSettingsExceptionsArea);
578   ContentSettingsExceptionsArea.prototype = {
579     __proto__: Page.prototype,
581     /** @override */
582     initializePage: function() {
583       Page.prototype.initializePage.call(this);
585       var exceptionsLists = this.pageDiv.querySelectorAll('list');
586       for (var i = 0; i < exceptionsLists.length; i++) {
587         options.contentSettings.ExceptionsList.decorate(exceptionsLists[i]);
588       }
590       ContentSettingsExceptionsArea.hideOTRLists(false);
592       // If the user types in the URL without a hash, show just cookies.
593       this.showList('cookies');
595       $('content-settings-exceptions-overlay-confirm').onclick =
596           PageManager.closeOverlay.bind(PageManager);
597     },
599     /**
600      * Shows one list and hides all others.
601      *
602      * @param {string} type The content type.
603      */
604     showList: function(type) {
605       // Update the title for the type that was shown.
606       this.title = loadTimeData.getString(type + 'TabTitle');
608       var header = this.pageDiv.querySelector('h1');
609       var camelCasedType = type.replace(/-([a-z])/g, function(g) {
610         return g[1].toUpperCase();
611       });
612       header.textContent = loadTimeData.getString(camelCasedType + 'Header');
614       var divs = this.pageDiv.querySelectorAll('div[contentType]');
615       for (var i = 0; i < divs.length; i++) {
616         if (divs[i].getAttribute('contentType') == type)
617           divs[i].hidden = false;
618         else
619           divs[i].hidden = true;
620       }
622       $('exception-behavior-column').hidden = type == 'zoomlevels';
623       $('exception-zoom-column').hidden = type != 'zoomlevels';
624     },
626     /**
627      * Called after the page has been shown. Show the content type for the
628      * location's hash.
629      */
630     didShowPage: function() {
631       if (this.hash)
632         this.showList(this.hash.slice(1));
633     },
634   };
636   /**
637    * Called when the last incognito window is closed.
638    */
639   ContentSettingsExceptionsArea.OTRProfileDestroyed = function() {
640     this.hideOTRLists(true);
641   };
643   /**
644    * Hides the incognito exceptions lists and optionally clears them as well.
645    * @param {boolean} clear Whether to clear the lists.
646    */
647   ContentSettingsExceptionsArea.hideOTRLists = function(clear) {
648     var otrLists = document.querySelectorAll('list[mode=otr]');
650     for (var i = 0; i < otrLists.length; i++) {
651       otrLists[i].parentNode.hidden = true;
652       if (clear)
653         otrLists[i].reset();
654     }
655   };
657   return {
658     ExceptionsListItem: ExceptionsListItem,
659     ExceptionsAddRowListItem: ExceptionsAddRowListItem,
660     ExceptionsList: ExceptionsList,
661     ContentSettingsExceptionsArea: ContentSettingsExceptionsArea,
662   };