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