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
5cr.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