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;
13 * Creates a new exceptions list item.
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
20 * @extends {options.InlineEditableItem}
22 function ExceptionsListItem(contentType, mode, exception) {
23 var el = cr.doc.createElement('div');
25 el.contentType = contentType;
26 el.dataItem = exception;
27 el.__proto__ = ExceptionsListItem.prototype;
33 ExceptionsListItem.prototype = {
34 __proto__: InlineEditableItem.prototype,
37 * Called when an element is decorated as a list item.
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);
48 this.patternLabel = patternCell.querySelector('.static-text');
49 var input = patternCell.querySelector('input');
51 // TODO(stuartmorgan): Create an createEditableSelectCell abstracting
53 // Setting label for display mode. |pattern| will be null for the 'add new
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;
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);
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);
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);
92 if (this.isEmbeddingRule()) {
93 this.patternLabel.classList.add('sublabel');
94 this.editable = false;
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;
104 if (this.contentType != 'zoomlevels') {
105 this.addEditField(select, this.settingLabel);
106 this.contentElement.appendChild(select);
108 select.className = 'exception-setting';
109 select.setAttribute('aria-labelledby', 'exception-behavior-column');
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);
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;
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
148 this.inputIsValid = true;
151 this.select = select;
153 this.updateEditables();
155 // Editing notifications, geolocation and media-stream is disabled for
157 if (this.contentType == 'notifications' ||
158 this.contentType == 'location' ||
159 this.contentType == 'media-stream') {
160 this.editable = false;
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
167 this.dataItem.source && this.dataItem.source != 'preference' ?
168 this.dataItem.source : null;
171 this.setAttribute('controlled-by', controlledBy);
172 this.deletable = false;
173 this.editable = false;
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);
188 // If the exception comes from a hosted app, display the name and the
190 if (controlledBy == 'HostedApp') {
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\')';
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]);
209 // Listen for edit events.
210 this.addEventListener('canceledit', this.onEditCancelled_);
211 this.addEventListener('commitedit', this.onEditCommitted_);
214 isEmbeddingRule: function() {
215 return this.dataItem.embeddingOrigin &&
216 this.dataItem.embeddingOrigin !== this.dataItem.origin;
220 * The pattern (e.g., a URL) for the exception.
225 if (!this.isEmbeddingRule())
226 return this.dataItem.origin;
228 return loadTimeData.getStringF('embeddedOnHost',
229 this.dataItem.embeddingOrigin);
231 set pattern(pattern) {
233 console.error('Tried to change uneditable pattern');
235 this.dataItem.displayPattern = pattern;
239 * The setting (allow/block) for the exception.
244 return this.dataItem.setting;
246 set setting(setting) {
247 this.dataItem.setting = setting;
251 * Gets a human-readable setting string.
253 * @return {string} The display string.
255 settingForDisplay: function() {
256 return this.getDisplayStringForSetting(this.setting);
260 * media video specific function.
261 * Gets a human-readable video setting string.
263 * @return {string} The display string.
265 videoSettingForDisplay: function() {
266 return this.getDisplayStringForSetting(this.dataItem.video);
270 * Gets a human-readable display string for setting.
272 * @param {string} setting The setting to be displayed.
273 * @return {string} The display string.
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')
289 console.error('Unknown setting: [' + setting + ']');
294 * Update this list item to reflect whether the input is a valid pattern.
296 * @param {boolean} valid Whether said pattern is valid in the context of a
297 * content exception setting.
299 setPatternValid: function(valid) {
300 if (valid || !this.input.value)
301 this.input.setCustomValidity('');
303 this.input.setCustomValidity(' ');
304 this.inputIsValid = valid;
305 this.inputValidityKnown = true;
309 * Set the <input> to its original contents. Used when the user quits
312 resetInput: function() {
313 this.input.value = this.pattern;
317 * Copy the data model values to the editable nodes.
319 updateEditables: function() {
323 this.select.querySelector('[value=\'' + this.setting + '\']');
325 settingOption.selected = true;
329 get currentInputIsValid() {
330 return this.inputValidityKnown && this.inputIsValid;
334 get hasBeenEdited() {
335 var livePattern = this.input.value;
336 var liveSetting = this.select.value;
337 return livePattern != this.pattern || liveSetting != this.setting;
341 * Called when committing an edit.
343 * @param {Event} e The end event.
346 onEditCommitted_: function(e) {
347 var newPattern = this.input.value;
348 var newSetting = this.select.value;
350 this.finishEdit(newPattern, newSetting);
354 * Called when cancelling an edit; resets the control states.
356 * @param {Event} e The cancel event.
359 onEditCancelled_: function(e) {
360 this.updateEditables();
361 this.setPatternValid(true);
365 * Editing is complete; update the model.
367 * @param {string} newPattern The pattern that the user entered.
368 * @param {string} newSetting The setting the user chose.
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
379 if (oldPattern != newPattern) {
380 chrome.send('removeException',
381 [this.contentType, this.mode, oldPattern]);
384 chrome.send('setException',
385 [this.contentType, this.mode, newPattern, newSetting]);
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
394 * @param {string} contentType The type of the list.
395 * @param {string} mode The browser mode, 'otr' or 'normal'.
397 * @extends {options.contentSettings.ExceptionsListItem}
399 function ExceptionsAddRowListItem(contentType, mode) {
400 var el = cr.doc.createElement('div');
402 el.contentType = contentType;
404 el.__proto__ = ExceptionsAddRowListItem.prototype;
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';
424 * Clear the <input> and let the placeholder text show again.
426 resetInput: function() {
427 this.input.value = '';
431 get hasBeenEdited() {
432 return this.input.value != '';
436 * Editing is complete; update the model. As long as the pattern isn't
437 * empty, we'll just add it.
439 * @param {string} newPattern The pattern that the user entered.
440 * @param {string} newSetting The setting the user chose.
442 finishEdit: function(newPattern, newSetting) {
444 chrome.send('setException',
445 [this.contentType, this.mode, newPattern, newSetting]);
450 * Creates a new exceptions list.
453 * @extends {options.InlineEditableItemList}
455 var ExceptionsList = cr.ui.define('list');
457 ExceptionsList.prototype = {
458 __proto__: InlineEditableItemList.prototype,
461 * Called when an element is decorated as a list.
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');
476 this.mode = this.getAttribute('mode');
477 this.autoExpands = true;
482 * Creates an item to go in the list.
484 * @param {Object} entry The element from the data model for this row.
486 createItem: function(entry) {
488 return new ExceptionsListItem(this.contentType,
492 var addRowItem = new ExceptionsAddRowListItem(this.contentType,
494 addRowItem.deletable = false;
500 * Sets the exceptions in the js model.
502 * @param {Array.<options.Exception>} entries A list of dictionaries of
503 * values, each dictionary represents an exception.
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;
513 var args = [0, deleteCount];
514 args.push.apply(args, entries);
515 this.dataModel.splice.apply(this.dataModel, args);
519 * The browser has finished checking a pattern for validity. Update the list
520 * item to reflect this.
522 * @param {string} pattern The pattern.
523 * @param {boolean} valid Whether said pattern is valid in the context of a
524 * content exception setting.
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);
539 * Returns whether the rows are editable in this list.
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');
551 * Removes all exceptions from the js model.
554 if (this.isEditable()) {
555 // The null creates the Add New Exception row.
556 this.dataModel = new ArrayDataModel([null]);
558 this.dataModel = new ArrayDataModel([]);
563 deleteItemAtIndex: function(index) {
564 var listItem = this.getListItemByIndex(index);
565 if (!listItem.deletable)
568 var dataItem = listItem.dataItem;
569 chrome.send('removeException', [listItem.contentType,
572 dataItem.embeddingOrigin]);
576 var Page = cr.ui.pageManager.Page;
577 var PageManager = cr.ui.pageManager.PageManager;
580 * Encapsulated handling of content settings list subpage.
583 * @extends {cr.ui.pageManager.Page}
585 function ContentSettingsExceptionsArea() {
586 Page.call(this, 'contentExceptions',
587 loadTimeData.getString('contentSettingsPageTabTitle'),
588 'content-settings-exceptions-area');
591 cr.addSingletonGetter(ContentSettingsExceptionsArea);
593 ContentSettingsExceptionsArea.prototype = {
594 __proto__: Page.prototype,
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]);
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);
615 * Shows one list and hides all others.
617 * @param {string} type The content type.
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;
631 divs[i].hidden = true;
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';
642 * Called after the page has been shown. Show the content type for the
645 didShowPage: function() {
647 this.showList(this.hash.slice(1));
652 * Called when the last incognito window is closed.
654 ContentSettingsExceptionsArea.OTRProfileDestroyed = function() {
655 this.hideOTRLists(true);
659 * Hides the incognito exceptions lists and optionally clears them as well.
660 * @param {boolean} clear Whether to clear the lists.
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;
673 ExceptionsListItem: ExceptionsListItem,
674 ExceptionsAddRowListItem: ExceptionsAddRowListItem,
675 ExceptionsList: ExceptionsList,
676 ContentSettingsExceptionsArea: ContentSettingsExceptionsArea,