Home | History | Annotate | Download | only in options
      1 // Copyright (c) 2011 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 InlineEditableItemList = options.InlineEditableItemList;
      7   const InlineEditableItem = options.InlineEditableItem;
      8   const ArrayDataModel = cr.ui.ArrayDataModel;
      9 
     10   /**
     11    * Creates a new exceptions list item.
     12    * @param {string} contentType The type of the list.
     13    * @param {string} mode The browser mode, 'otr' or 'normal'.
     14    * @param {boolean} enableAskOption Whether to show an 'ask every time'
     15    *     option in the select.
     16    * @param {Object} exception A dictionary that contains the data of the
     17    *     exception.
     18    * @constructor
     19    * @extends {options.InlineEditableItem}
     20    */
     21   function ExceptionsListItem(contentType, mode, enableAskOption, exception) {
     22     var el = cr.doc.createElement('div');
     23     el.mode = mode;
     24     el.contentType = contentType;
     25     el.enableAskOption = enableAskOption;
     26     el.dataItem = exception;
     27     el.__proto__ = ExceptionsListItem.prototype;
     28     el.decorate();
     29 
     30     return el;
     31   }
     32 
     33   ExceptionsListItem.prototype = {
     34     __proto__: InlineEditableItem.prototype,
     35 
     36     /**
     37      * Called when an element is decorated as a list item.
     38      */
     39     decorate: function() {
     40       InlineEditableItem.prototype.decorate.call(this);
     41 
     42       this.isPlaceholder = !this.pattern;
     43       var patternCell = this.createEditableTextCell(this.pattern);
     44       patternCell.className = 'exception-pattern';
     45       this.contentElement.appendChild(patternCell);
     46       if (this.pattern)
     47         this.patternLabel = patternCell.querySelector('.static-text');
     48       var input = patternCell.querySelector('input');
     49 
     50       // TODO(stuartmorgan): Create an createEditableSelectCell abstracting
     51       // this code.
     52       // Setting label for display mode. |pattern| will be null for the 'add new
     53       // exception' row.
     54       if (this.pattern) {
     55         var settingLabel = cr.doc.createElement('span');
     56         settingLabel.textContent = this.settingForDisplay();
     57         settingLabel.className = 'exception-setting';
     58         settingLabel.setAttribute('displaymode', 'static');
     59         this.contentElement.appendChild(settingLabel);
     60         this.settingLabel = settingLabel;
     61       }
     62 
     63       // Setting select element for edit mode.
     64       var select = cr.doc.createElement('select');
     65       var optionAllow = cr.doc.createElement('option');
     66       optionAllow.textContent = templateData.allowException;
     67       optionAllow.value = 'allow';
     68       select.appendChild(optionAllow);
     69 
     70       if (this.enableAskOption) {
     71         var optionAsk = cr.doc.createElement('option');
     72         optionAsk.textContent = templateData.askException;
     73         optionAsk.value = 'ask';
     74         select.appendChild(optionAsk);
     75       }
     76 
     77       if (this.contentType == 'cookies') {
     78         var optionSession = cr.doc.createElement('option');
     79         optionSession.textContent = templateData.sessionException;
     80         optionSession.value = 'session';
     81         select.appendChild(optionSession);
     82       }
     83 
     84       var optionBlock = cr.doc.createElement('option');
     85       optionBlock.textContent = templateData.blockException;
     86       optionBlock.value = 'block';
     87       select.appendChild(optionBlock);
     88 
     89       this.contentElement.appendChild(select);
     90       select.className = 'exception-setting';
     91       if (this.pattern)
     92         select.setAttribute('displaymode', 'edit');
     93 
     94       // Used to track whether the URL pattern in the input is valid.
     95       // This will be true if the browser process has informed us that the
     96       // current text in the input is valid. Changing the text resets this to
     97       // false, and getting a response from the browser sets it back to true.
     98       // It starts off as false for empty string (new exceptions) or true for
     99       // already-existing exceptions (which we assume are valid).
    100       this.inputValidityKnown = this.pattern;
    101       // This one tracks the actual validity of the pattern in the input. This
    102       // starts off as true so as not to annoy the user when he adds a new and
    103       // empty input.
    104       this.inputIsValid = true;
    105 
    106       this.input = input;
    107       this.select = select;
    108 
    109       this.updateEditables();
    110 
    111       // Editing notifications and geolocation is disabled for now.
    112       if (this.contentType == 'notifications' ||
    113           this.contentType == 'location') {
    114         this.editable = false;
    115       }
    116 
    117       var listItem = this;
    118       // Handle events on the editable nodes.
    119       input.oninput = function(event) {
    120         listItem.inputValidityKnown = false;
    121         chrome.send('checkExceptionPatternValidity',
    122                     [listItem.contentType, listItem.mode, input.value]);
    123       };
    124 
    125       // Listen for edit events.
    126       this.addEventListener('canceledit', this.onEditCancelled_);
    127       this.addEventListener('commitedit', this.onEditCommitted_);
    128     },
    129 
    130     /**
    131      * The pattern (e.g., a URL) for the exception.
    132      * @type {string}
    133      */
    134     get pattern() {
    135       return this.dataItem['displayPattern'];
    136     },
    137     set pattern(pattern) {
    138       this.dataItem['displayPattern'] = pattern;
    139     },
    140 
    141     /**
    142      * The setting (allow/block) for the exception.
    143      * @type {string}
    144      */
    145     get setting() {
    146       return this.dataItem['setting'];
    147     },
    148     set setting(setting) {
    149       this.dataItem['setting'] = setting;
    150     },
    151 
    152     /**
    153      * Gets a human-readable setting string.
    154      * @type {string}
    155      */
    156     settingForDisplay: function() {
    157       var setting = this.setting;
    158       if (setting == 'allow')
    159         return templateData.allowException;
    160       else if (setting == 'block')
    161         return templateData.blockException;
    162       else if (setting == 'ask')
    163         return templateData.askException;
    164       else if (setting == 'session')
    165         return templateData.sessionException;
    166     },
    167 
    168     /**
    169      * Update this list item to reflect whether the input is a valid pattern.
    170      * @param {boolean} valid Whether said pattern is valid in the context of
    171      *     a content exception setting.
    172      */
    173     setPatternValid: function(valid) {
    174       if (valid || !this.input.value)
    175         this.input.setCustomValidity('');
    176       else
    177         this.input.setCustomValidity(' ');
    178       this.inputIsValid = valid;
    179       this.inputValidityKnown = true;
    180     },
    181 
    182     /**
    183      * Set the <input> to its original contents. Used when the user quits
    184      * editing.
    185      */
    186     resetInput: function() {
    187       this.input.value = this.pattern;
    188     },
    189 
    190     /**
    191      * Copy the data model values to the editable nodes.
    192      */
    193     updateEditables: function() {
    194       this.resetInput();
    195 
    196       var settingOption =
    197           this.select.querySelector('[value=\'' + this.setting + '\']');
    198       if (settingOption)
    199         settingOption.selected = true;
    200     },
    201 
    202     /** @inheritDoc */
    203     get currentInputIsValid() {
    204       return this.inputValidityKnown && this.inputIsValid;
    205     },
    206 
    207     /** @inheritDoc */
    208     get hasBeenEdited() {
    209       var livePattern = this.input.value;
    210       var liveSetting = this.select.value;
    211       return livePattern != this.pattern || liveSetting != this.setting;
    212     },
    213 
    214     /**
    215      * Called when committing an edit.
    216      * @param {Event} e The end event.
    217      * @private
    218      */
    219     onEditCommitted_: function(e) {
    220       var newPattern = this.input.value;
    221       var newSetting = this.select.value;
    222 
    223       this.finishEdit(newPattern, newSetting);
    224     },
    225 
    226     /**
    227      * Called when cancelling an edit; resets the control states.
    228      * @param {Event} e The cancel event.
    229      * @private
    230      */
    231     onEditCancelled_: function() {
    232       this.updateEditables();
    233       this.setPatternValid(true);
    234     },
    235 
    236     /**
    237      * Editing is complete; update the model.
    238      * @param {string} newPattern The pattern that the user entered.
    239      * @param {string} newSetting The setting the user chose.
    240      */
    241     finishEdit: function(newPattern, newSetting) {
    242       this.patternLabel.textContent = newPattern;
    243       this.settingLabel.textContent = this.settingForDisplay();
    244       var oldPattern = this.pattern;
    245       this.pattern = newPattern;
    246       this.setting = newSetting;
    247 
    248       // TODO(estade): this will need to be updated if geolocation/notifications
    249       // become editable.
    250       if (oldPattern != newPattern) {
    251         chrome.send('removeException',
    252                     [this.contentType, this.mode, oldPattern]);
    253       }
    254 
    255       chrome.send('setException',
    256                   [this.contentType, this.mode, newPattern, newSetting]);
    257     }
    258   };
    259 
    260   /**
    261    * Creates a new list item for the Add New Item row, which doesn't represent
    262    * an actual entry in the exceptions list but allows the user to add new
    263    * exceptions.
    264    * @param {string} contentType The type of the list.
    265    * @param {string} mode The browser mode, 'otr' or 'normal'.
    266    * @param {boolean} enableAskOption Whether to show an 'ask every time'
    267    *     option in the select.
    268    * @constructor
    269    * @extends {cr.ui.ExceptionsListItem}
    270    */
    271   function ExceptionsAddRowListItem(contentType, mode, enableAskOption) {
    272     var el = cr.doc.createElement('div');
    273     el.mode = mode;
    274     el.contentType = contentType;
    275     el.enableAskOption = enableAskOption;
    276     el.dataItem = [];
    277     el.__proto__ = ExceptionsAddRowListItem.prototype;
    278     el.decorate();
    279 
    280     return el;
    281   }
    282 
    283   ExceptionsAddRowListItem.prototype = {
    284     __proto__: ExceptionsListItem.prototype,
    285 
    286     decorate: function() {
    287       ExceptionsListItem.prototype.decorate.call(this);
    288 
    289       this.input.placeholder = templateData.addNewExceptionInstructions;
    290 
    291       // Do we always want a default of allow?
    292       this.setting = 'allow';
    293     },
    294 
    295     /**
    296      * Clear the <input> and let the placeholder text show again.
    297      */
    298     resetInput: function() {
    299       this.input.value = '';
    300     },
    301 
    302     /** @inheritDoc */
    303     get hasBeenEdited() {
    304       return this.input.value != '';
    305     },
    306 
    307     /**
    308      * Editing is complete; update the model. As long as the pattern isn't
    309      * empty, we'll just add it.
    310      * @param {string} newPattern The pattern that the user entered.
    311      * @param {string} newSetting The setting the user chose.
    312      */
    313     finishEdit: function(newPattern, newSetting) {
    314       chrome.send('setException',
    315                   [this.contentType, this.mode, newPattern, newSetting]);
    316     },
    317   };
    318 
    319   /**
    320    * Creates a new exceptions list.
    321    * @constructor
    322    * @extends {cr.ui.List}
    323    */
    324   var ExceptionsList = cr.ui.define('list');
    325 
    326   ExceptionsList.prototype = {
    327     __proto__: InlineEditableItemList.prototype,
    328 
    329     /**
    330      * Called when an element is decorated as a list.
    331      */
    332     decorate: function() {
    333       InlineEditableItemList.prototype.decorate.call(this);
    334 
    335       this.classList.add('settings-list');
    336 
    337       for (var parentNode = this.parentNode; parentNode;
    338            parentNode = parentNode.parentNode) {
    339         if (parentNode.hasAttribute('contentType')) {
    340           this.contentType = parentNode.getAttribute('contentType');
    341           break;
    342         }
    343       }
    344 
    345       this.mode = this.getAttribute('mode');
    346 
    347       var exceptionList = this;
    348       function handleBlur(e) {
    349         // When the blur event happens we do not know who is getting focus so we
    350         // delay this a bit until we know if the new focus node is outside the
    351         // list.
    352         var doc = e.target.ownerDocument;
    353         window.setTimeout(function() {
    354           var activeElement = doc.activeElement;
    355           if (!exceptionList.contains(activeElement))
    356             exceptionList.selectionModel.unselectAll();
    357         }, 50);
    358       }
    359 
    360       this.addEventListener('blur', handleBlur, true);
    361 
    362       // Whether the exceptions in this list allow an 'Ask every time' option.
    363       this.enableAskOption = (this.contentType == 'plugins' &&
    364                               templateData.enable_click_to_play);
    365 
    366       this.autoExpands = true;
    367       this.reset();
    368     },
    369 
    370     /**
    371      * Creates an item to go in the list.
    372      * @param {Object} entry The element from the data model for this row.
    373      */
    374     createItem: function(entry) {
    375       if (entry) {
    376         return new ExceptionsListItem(this.contentType,
    377                                       this.mode,
    378                                       this.enableAskOption,
    379                                       entry);
    380       } else {
    381         var addRowItem = new ExceptionsAddRowListItem(this.contentType,
    382                                                       this.mode,
    383                                                       this.enableAskOption);
    384         addRowItem.deletable = false;
    385         return addRowItem;
    386       }
    387     },
    388 
    389     /**
    390      * Sets the exceptions in the js model.
    391      * @param {Object} entries A list of dictionaries of values, each dictionary
    392      *     represents an exception.
    393      */
    394     setExceptions: function(entries) {
    395       var deleteCount = this.dataModel.length;
    396 
    397       if (this.isEditable()) {
    398         // We don't want to remove the Add New Exception row.
    399         deleteCount = deleteCount - 1;
    400       }
    401 
    402       var args = [0, deleteCount];
    403       args.push.apply(args, entries);
    404       this.dataModel.splice.apply(this.dataModel, args);
    405     },
    406 
    407     /**
    408      * The browser has finished checking a pattern for validity. Update the
    409      * list item to reflect this.
    410      * @param {string} pattern The pattern.
    411      * @param {bool} valid Whether said pattern is valid in the context of
    412      *     a content exception setting.
    413      */
    414     patternValidityCheckComplete: function(pattern, valid) {
    415       var listItems = this.items;
    416       for (var i = 0; i < listItems.length; i++) {
    417         var listItem = listItems[i];
    418         // Don't do anything for messages for the item if it is not the intended
    419         // recipient, or if the response is stale (i.e. the input value has
    420         // changed since we sent the request to analyze it).
    421         if (pattern == listItem.input.value)
    422           listItem.setPatternValid(valid);
    423       }
    424     },
    425 
    426     /**
    427      * Returns whether the rows are editable in this list.
    428      */
    429     isEditable: function() {
    430       // Editing notifications and geolocation is disabled for now.
    431       return !(this.contentType == 'notifications' ||
    432                this.contentType == 'location');
    433     },
    434 
    435     /**
    436      * Removes all exceptions from the js model.
    437      */
    438     reset: function() {
    439       if (this.isEditable()) {
    440         // The null creates the Add New Exception row.
    441         this.dataModel = new ArrayDataModel([null]);
    442       } else {
    443         this.dataModel = new ArrayDataModel([]);
    444       }
    445     },
    446 
    447     /** @inheritDoc */
    448     deleteItemAtIndex: function(index) {
    449       var listItem = this.getListItemByIndex(index);
    450       if (listItem.undeletable)
    451         return;
    452 
    453       var dataItem = listItem.dataItem;
    454       var args = [listItem.contentType];
    455       if (listItem.contentType == 'location')
    456         args.push(dataItem['origin'], dataItem['embeddingOrigin']);
    457       else if (listItem.contentType == 'notifications')
    458         args.push(dataItem['origin'], dataItem['setting']);
    459       else
    460         args.push(listItem.mode, listItem.pattern);
    461 
    462       chrome.send('removeException', args);
    463     },
    464   };
    465 
    466   var OptionsPage = options.OptionsPage;
    467 
    468   /**
    469    * Encapsulated handling of content settings list subpage.
    470    * @constructor
    471    */
    472   function ContentSettingsExceptionsArea() {
    473     OptionsPage.call(this, 'contentExceptions',
    474                      templateData.contentSettingsPageTabTitle,
    475                      'content-settings-exceptions-area');
    476   }
    477 
    478   cr.addSingletonGetter(ContentSettingsExceptionsArea);
    479 
    480   ContentSettingsExceptionsArea.prototype = {
    481     __proto__: OptionsPage.prototype,
    482 
    483     initializePage: function() {
    484       OptionsPage.prototype.initializePage.call(this);
    485 
    486       var exceptionsLists = this.pageDiv.querySelectorAll('list');
    487       for (var i = 0; i < exceptionsLists.length; i++) {
    488         options.contentSettings.ExceptionsList.decorate(exceptionsLists[i]);
    489       }
    490 
    491       ContentSettingsExceptionsArea.hideOTRLists();
    492 
    493       // If the user types in the URL without a hash, show just cookies.
    494       this.showList('cookies');
    495     },
    496 
    497     /**
    498      * Shows one list and hides all others.
    499      * @param {string} type The content type.
    500      */
    501     showList: function(type) {
    502       var header = this.pageDiv.querySelector('h1');
    503       header.textContent = templateData[type + '_header'];
    504 
    505       var divs = this.pageDiv.querySelectorAll('div[contentType]');
    506       for (var i = 0; i < divs.length; i++) {
    507         if (divs[i].getAttribute('contentType') == type)
    508           divs[i].classList.remove('hidden');
    509         else
    510           divs[i].classList.add('hidden');
    511       }
    512     },
    513 
    514     /**
    515      * Called after the page has been shown. Show the content type for the
    516      * location's hash.
    517      */
    518     didShowPage: function() {
    519       var hash = location.hash;
    520       if (hash)
    521         this.showList(hash.slice(1));
    522     },
    523   };
    524 
    525   /**
    526    * Called when the last incognito window is closed.
    527    */
    528   ContentSettingsExceptionsArea.OTRProfileDestroyed = function() {
    529     this.hideOTRLists();
    530   };
    531 
    532   /**
    533    * Clears and hides the incognito exceptions lists.
    534    */
    535   ContentSettingsExceptionsArea.hideOTRLists = function() {
    536     var otrLists = document.querySelectorAll('list[mode=otr]');
    537 
    538     for (var i = 0; i < otrLists.length; i++) {
    539       otrLists[i].reset();
    540       otrLists[i].parentNode.classList.add('hidden');
    541     }
    542   };
    543 
    544   return {
    545     ExceptionsListItem: ExceptionsListItem,
    546     ExceptionsAddRowListItem: ExceptionsAddRowListItem,
    547     ExceptionsList: ExceptionsList,
    548     ContentSettingsExceptionsArea: ContentSettingsExceptionsArea,
    549   };
    550 });
    551