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