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 * Returns whether exceptions list for the type is editable.
15 * @param {string} contentType The type of the list.
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');
29 * Creates a new exceptions list item.
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
36 * @extends {options.InlineEditableItem}
38 function ExceptionsListItem(contentType, mode, exception) {
39 var el = cr.doc.createElement('div');
41 el.contentType = contentType;
42 el.dataItem = exception;
43 el.__proto__ = ExceptionsListItem.prototype;
49 ExceptionsListItem.prototype = {
50 __proto__: InlineEditableItem.prototype,
53 * Called when an element is decorated as a list item.
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);
64 this.patternLabel = patternCell.querySelector('.static-text');
65 var input = patternCell.querySelector('input');
67 // TODO(stuartmorgan): Create an createEditableSelectCell abstracting
69 // Setting label for display mode. |pattern| will be null for the 'add new
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;
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);
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);
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);
108 if (this.isEmbeddingRule()) {
109 this.patternLabel.classList.add('sublabel');
110 this.editable = false;
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;
120 if (this.contentType != 'zoomlevels') {
121 this.addEditField(select, this.settingLabel);
122 this.contentElement.appendChild(select);
124 select.className = 'exception-setting';
125 select.setAttribute('aria-labelledby', 'exception-behavior-column');
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;
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
152 this.inputIsValid = true;
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
164 this.dataItem.source && this.dataItem.source != 'preference' ?
165 this.dataItem.source : null;
168 this.setAttribute('controlled-by', controlledBy);
169 this.deletable = false;
170 this.editable = false;
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);
185 // If the exception comes from a hosted app, display the name and the
187 if (controlledBy == 'HostedApp') {
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\')';
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]);
206 // Listen for edit events.
207 this.addEventListener('canceledit', this.onEditCancelled_);
208 this.addEventListener('commitedit', this.onEditCommitted_);
211 isEmbeddingRule: function() {
212 return this.dataItem.embeddingOrigin &&
213 this.dataItem.embeddingOrigin !== this.dataItem.origin;
217 * The pattern (e.g., a URL) for the exception.
222 if (!this.isEmbeddingRule())
223 return this.dataItem.origin;
225 return loadTimeData.getStringF('embeddedOnHost',
226 this.dataItem.embeddingOrigin);
228 set pattern(pattern) {
230 console.error('Tried to change uneditable pattern');
232 this.dataItem.displayPattern = pattern;
236 * The setting (allow/block) for the exception.
241 return this.dataItem.setting;
243 set setting(setting) {
244 this.dataItem.setting = setting;
248 * Gets a human-readable setting string.
250 * @return {string} The display string.
252 settingForDisplay: function() {
253 return this.getDisplayStringForSetting(this.setting);
257 * Gets a human-readable display string for setting.
259 * @param {string} setting The setting to be displayed.
260 * @return {string} The display string.
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')
276 console.error('Unknown setting: [' + setting + ']');
281 * Update this list item to reflect whether the input is a valid pattern.
283 * @param {boolean} valid Whether said pattern is valid in the context of a
284 * content exception setting.
286 setPatternValid: function(valid) {
287 if (valid || !this.input.value)
288 this.input.setCustomValidity('');
290 this.input.setCustomValidity(' ');
291 this.inputIsValid = valid;
292 this.inputValidityKnown = true;
296 * Set the <input> to its original contents. Used when the user quits
299 resetInput: function() {
300 this.input.value = this.pattern;
304 * Copy the data model values to the editable nodes.
306 updateEditables: function() {
310 this.select.querySelector('[value=\'' + this.setting + '\']');
312 settingOption.selected = true;
316 get currentInputIsValid() {
317 return this.inputValidityKnown && this.inputIsValid;
321 get hasBeenEdited() {
322 var livePattern = this.input.value;
323 var liveSetting = this.select.value;
324 return livePattern != this.pattern || liveSetting != this.setting;
328 * Called when committing an edit.
330 * @param {Event} e The end event.
333 onEditCommitted_: function(e) {
334 var newPattern = this.input.value;
335 var newSetting = this.select.value;
337 this.finishEdit(newPattern, newSetting);
341 * Called when cancelling an edit; resets the control states.
343 * @param {Event} e The cancel event.
346 onEditCancelled_: function(e) {
347 this.updateEditables();
348 this.setPatternValid(true);
352 * Editing is complete; update the model.
354 * @param {string} newPattern The pattern that the user entered.
355 * @param {string} newSetting The setting the user chose.
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
366 if (oldPattern != newPattern) {
367 chrome.send('removeException',
368 [this.contentType, this.mode, oldPattern]);
371 chrome.send('setException',
372 [this.contentType, this.mode, newPattern, newSetting]);
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
381 * @param {string} contentType The type of the list.
382 * @param {string} mode The browser mode, 'otr' or 'normal'.
384 * @extends {options.contentSettings.ExceptionsListItem}
386 function ExceptionsAddRowListItem(contentType, mode) {
387 var el = cr.doc.createElement('div');
389 el.contentType = contentType;
391 el.__proto__ = ExceptionsAddRowListItem.prototype;
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';
411 * Clear the <input> and let the placeholder text show again.
413 resetInput: function() {
414 this.input.value = '';
418 get hasBeenEdited() {
419 return this.input.value != '';
423 * Editing is complete; update the model. As long as the pattern isn't
424 * empty, we'll just add it.
426 * @param {string} newPattern The pattern that the user entered.
427 * @param {string} newSetting The setting the user chose.
429 finishEdit: function(newPattern, newSetting) {
431 chrome.send('setException',
432 [this.contentType, this.mode, newPattern, newSetting]);
437 * Creates a new exceptions list.
440 * @extends {options.InlineEditableItemList}
442 var ExceptionsList = cr.ui.define('list');
444 ExceptionsList.prototype = {
445 __proto__: InlineEditableItemList.prototype,
448 * Called when an element is decorated as a list.
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');
463 if (!this.isEditable())
466 this.mode = this.getAttribute('mode');
467 this.autoExpands = true;
472 * Creates an item to go in the list.
474 * @param {Object} entry The element from the data model for this row.
476 createItem: function(entry) {
478 return new ExceptionsListItem(this.contentType,
482 var addRowItem = new ExceptionsAddRowListItem(this.contentType,
484 addRowItem.deletable = false;
490 * Sets the exceptions in the js model.
492 * @param {Array<options.Exception>} entries A list of dictionaries of
493 * values, each dictionary represents an exception.
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;
503 var args = [0, deleteCount];
504 args.push.apply(args, entries);
505 this.dataModel.splice.apply(this.dataModel, args);
509 * The browser has finished checking a pattern for validity. Update the list
510 * item to reflect this.
512 * @param {string} pattern The pattern.
513 * @param {boolean} valid Whether said pattern is valid in the context of a
514 * content exception setting.
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);
529 * Returns whether the rows are editable in this list.
531 isEditable: function() {
532 // Exceptions of the following lists are not editable for now.
533 return IsEditableType(this.contentType);
537 * Removes all exceptions from the js model.
540 if (this.isEditable()) {
541 // The null creates the Add New Exception row.
542 this.dataModel = new ArrayDataModel([null]);
544 this.dataModel = new ArrayDataModel([]);
549 deleteItemAtIndex: function(index) {
550 var listItem = this.getListItemByIndex(index);
551 if (!listItem.deletable)
554 var dataItem = listItem.dataItem;
555 chrome.send('removeException', [listItem.contentType,
558 dataItem.embeddingOrigin]);
562 var Page = cr.ui.pageManager.Page;
563 var PageManager = cr.ui.pageManager.PageManager;
566 * Encapsulated handling of content settings list subpage.
569 * @extends {cr.ui.pageManager.Page}
571 function ContentSettingsExceptionsArea() {
572 Page.call(this, 'contentExceptions',
573 loadTimeData.getString('contentSettingsPageTabTitle'),
574 'content-settings-exceptions-area');
577 cr.addSingletonGetter(ContentSettingsExceptionsArea);
579 ContentSettingsExceptionsArea.prototype = {
580 __proto__: Page.prototype,
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]);
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);
601 * Shows one list and hides all others.
603 * @param {string} type The content type.
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();
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;
620 divs[i].hidden = true;
623 $('exception-behavior-column').hidden = type == 'zoomlevels';
624 $('exception-zoom-column').hidden = type != 'zoomlevels';
628 * Called after the page has been shown. Show the content type for the
631 didShowPage: function() {
633 this.showList(this.hash.slice(1));
638 * Called when the last incognito window is closed.
640 ContentSettingsExceptionsArea.OTRProfileDestroyed = function() {
641 this.hideOTRLists(true);
645 * Hides the incognito exceptions lists and optionally clears them as well.
646 * @param {boolean} clear Whether to clear the lists.
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;
659 ExceptionsListItem: ExceptionsListItem,
660 ExceptionsAddRowListItem: ExceptionsAddRowListItem,
661 ExceptionsList: ExceptionsList,
662 ContentSettingsExceptionsArea: ContentSettingsExceptionsArea,