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