content_settings_exceptions_area.js revision bda42a81ee5f9b20d2bebedcf0bbef1e30e5b293
1// Copyright (c) 2010 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 List = cr.ui.List;
7  const ListItem = cr.ui.ListItem;
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 {Array} exception A pair of the form [filter, setting].
17   * @constructor
18   * @extends {cr.ui.ListItem}
19   */
20  function ExceptionsListItem(contentType, mode, enableAskOption, exception) {
21    var el = cr.doc.createElement('li');
22    el.mode = mode;
23    el.contentType = contentType;
24    el.enableAskOption = enableAskOption;
25    el.dataItem = exception;
26    el.__proto__ = ExceptionsListItem.prototype;
27    el.decorate();
28
29    return el;
30  }
31
32  ExceptionsListItem.prototype = {
33    __proto__: ListItem.prototype,
34
35    /**
36     * Called when an element is decorated as a list item.
37     */
38    decorate: function() {
39      ListItem.prototype.decorate.call(this);
40
41      // Labels for display mode.
42      var patternLabel = cr.doc.createElement('span');
43      patternLabel.textContent = this.pattern;
44      this.appendChild(patternLabel);
45
46      var settingLabel = cr.doc.createElement('span');
47      settingLabel.textContent = this.settingForDisplay();
48      settingLabel.className = 'exceptionSetting';
49      this.appendChild(settingLabel);
50
51      // Elements for edit mode.
52      var input = cr.doc.createElement('input');
53      input.type = 'text';
54      this.appendChild(input);
55      input.className = 'exceptionInput hidden';
56
57      var select = cr.doc.createElement('select');
58      var optionAllow = cr.doc.createElement('option');
59      optionAllow.textContent = templateData.allowException;
60      select.appendChild(optionAllow);
61
62      if (this.enableAskOption) {
63        var optionAsk = cr.doc.createElement('option');
64        optionAsk.textContent = templateData.askException;
65        select.appendChild(optionAsk);
66        this.optionAsk = optionAsk;
67      }
68
69      if (this.contentType == 'cookies') {
70        var optionSession = cr.doc.createElement('option');
71        optionSession.textContent = templateData.sessionException;
72        select.appendChild(optionSession);
73        this.optionSession = optionSession;
74      }
75
76      var optionBlock = cr.doc.createElement('option');
77      optionBlock.textContent = templateData.blockException;
78      select.appendChild(optionBlock);
79
80      this.appendChild(select);
81      select.className = 'exceptionSetting hidden';
82
83      // Used to track whether the URL pattern in the input is valid.
84      // This will be true if the browser process has informed us that the
85      // current text in the input is valid. Changing the text resets this to
86      // false, and getting a response from the browser sets it back to true.
87      // It starts off as false for empty string (new exceptions) or true for
88      // already-existing exceptions (which we assume are valid).
89      this.inputValidityKnown = this.pattern;
90      // This one tracks the actual validity of the pattern in the input. This
91      // starts off as true so as not to annoy the user when he adds a new and
92      // empty input.
93      this.inputIsValid = true;
94
95      this.patternLabel = patternLabel;
96      this.settingLabel = settingLabel;
97      this.input = input;
98      this.select = select;
99      this.optionAllow = optionAllow;
100      this.optionBlock = optionBlock;
101
102      this.updateEditables();
103
104      var listItem = this;
105      this.ondblclick = function(event) {
106        listItem.editing = true;
107      };
108
109      // Handle events on the editable nodes.
110      input.oninput = function(event) {
111        listItem.inputValidityKnown = false;
112        chrome.send('checkExceptionPatternValidity',
113                    [listItem.contentType, listItem.mode, input.value]);
114      };
115
116      // Handles enter and escape which trigger reset and commit respectively.
117      function handleKeydown(e) {
118        // Make sure that the tree does not handle the key.
119        e.stopPropagation();
120
121        // Calling list.focus blurs the input which will stop editing the list
122        // item.
123        switch (e.keyIdentifier) {
124          case 'U+001B':  // Esc
125            // Reset the inputs.
126            listItem.updateEditables();
127            if (listItem.pattern)
128              listItem.maybeSetPatternValidity(listItem.pattern, true);
129          case 'Enter':
130            if (listItem.parentNode)
131              listItem.parentNode.focus();
132        }
133      }
134
135      function handleBlur(e) {
136        // When the blur event happens we do not know who is getting focus so we
137        // delay this a bit since we want to know if the other input got focus
138        // before deciding if we should exit edit mode.
139        var doc = e.target.ownerDocument;
140        window.setTimeout(function() {
141          var activeElement = doc.activeElement;
142          if (!listItem.contains(activeElement)) {
143            listItem.editing = false;
144          }
145        }, 50);
146      }
147
148      input.addEventListener('keydown', handleKeydown);
149      input.addEventListener('blur', handleBlur);
150
151      select.addEventListener('keydown', handleKeydown);
152      select.addEventListener('blur', handleBlur);
153    },
154
155    /**
156     * The pattern (e.g., a URL) for the exception.
157     * @type {string}
158     */
159    get pattern() {
160      return this.dataItem[0];
161    },
162    set pattern(pattern) {
163      this.dataItem[0] = pattern;
164    },
165
166    /**
167     * The setting (allow/block) for the exception.
168     * @type {string}
169     */
170    get setting() {
171      return this.dataItem[1];
172    },
173    set setting(setting) {
174      this.dataItem[1] = setting;
175    },
176
177    /**
178     * The embedding origin for the location exception.
179     * @type {string}
180     */
181    get embeddingOrigin() {
182      return this.dataItem[2];
183    },
184    set embeddingOrigin(url) {
185      this.dataItem[2] = url;
186    },
187
188    /**
189     * Gets a human-readable setting string.
190     * @type {string}
191     */
192    settingForDisplay: function() {
193      var setting = this.setting;
194      if (setting == 'allow')
195        return templateData.allowException;
196      else if (setting == 'block')
197        return templateData.blockException;
198      else if (setting == 'ask')
199        return templateData.askException;
200      else if (setting == 'session')
201        return templateData.sessionException;
202    },
203
204    /**
205     * Update this list item to reflect whether the input is a valid pattern
206     * if |pattern| matches the text currently in the input.
207     * @param {string} pattern The pattern.
208     * @param {boolean} valid Whether said pattern is valid in the context of
209     *     a content exception setting.
210     */
211    maybeSetPatternValid: function(pattern, valid) {
212      // Don't do anything for messages where we are not the intended recipient,
213      // or if the response is stale (i.e. the input value has changed since we
214      // sent the request to analyze it).
215      if (pattern != this.input.value)
216        return;
217
218      if (valid)
219        this.input.setCustomValidity('');
220      else
221        this.input.setCustomValidity(' ');
222      this.inputIsValid = valid;
223      this.inputValidityKnown = true;
224    },
225
226    /**
227     * Copy the data model values to the editable nodes.
228     */
229    updateEditables: function() {
230      if (!(this.pattern && this.setting))
231        return;
232
233      this.input.value = this.pattern;
234
235      if (this.setting == 'allow')
236        this.optionAllow.selected = true;
237      else if (this.setting == 'block')
238        this.optionBlock.selected = true;
239      else if (this.setting == 'session' && this.optionSession)
240        this.optionSession.selected = true;
241      else if (this.setting == 'ask' && this.optionAsk)
242        this.optionAsk.selected = true;
243    },
244
245    /**
246     * Whether the user is currently able to edit the list item.
247     * @type {boolean}
248     */
249    get editing() {
250      return this.hasAttribute('editing');
251    },
252    set editing(editing) {
253      var oldEditing = this.editing;
254      if (oldEditing == editing)
255        return;
256
257      var listItem = this;
258      var pattern = this.pattern;
259      var setting = this.setting;
260      var patternLabel = this.patternLabel;
261      var settingLabel = this.settingLabel;
262      var input = this.input;
263      var select = this.select;
264      var optionAllow = this.optionAllow;
265      var optionBlock = this.optionBlock;
266      var optionSession = this.optionSession;
267      var optionAsk = this.optionAsk;
268
269      // Check that we have a valid pattern and if not we do not change the
270      // editing mode.
271      if (!editing && (!this.inputValidityKnown || !this.inputIsValid)) {
272        if (this.pattern) {
273          input.focus();
274          input.select();
275        } else {
276          // Just delete this row if it was added via the Add button.
277          var model = listItem.parentNode.dataModel;
278          model.splice(model.indexOf(listItem.dataItem), 1);
279        }
280
281        return;
282      }
283
284      patternLabel.classList.toggle('hidden');
285      settingLabel.classList.toggle('hidden');
286      input.classList.toggle('hidden');
287      select.classList.toggle('hidden');
288
289      var doc = this.ownerDocument;
290      if (editing) {
291        this.setAttribute('editing', '');
292        cr.ui.limitInputWidth(input, this, 20);
293        input.focus();
294        input.select();
295      } else {
296        this.removeAttribute('editing');
297
298        var newPattern = input.value;
299
300        var newSetting;
301        if (optionAllow.selected)
302          newSetting = 'allow';
303        else if (optionBlock.selected)
304          newSetting = 'block';
305        else if (optionSession && optionSession.selected)
306          newSetting = 'session';
307        else if (optionAsk && optionAsk.selected)
308          newSetting = 'ask';
309
310        // Empty edit - do nothing.
311        if (pattern == newPattern && newSetting == this.setting)
312          return;
313
314        this.pattern = patternLabel.textContent = newPattern;
315        this.setting = newSetting;
316        settingLabel.textContent = this.settingForDisplay();
317
318        if (pattern != this.pattern) {
319          chrome.send('removeExceptions',
320                      [this.contentType, this.mode, pattern]);
321        }
322
323        chrome.send('setException',
324                    [this.contentType, this.mode, this.pattern, this.setting]);
325      }
326    }
327  };
328
329  /**
330   * Creates a new exceptions list.
331   * @constructor
332   * @extends {cr.ui.List}
333   */
334  var ExceptionsList = cr.ui.define('list');
335
336  ExceptionsList.prototype = {
337    __proto__: List.prototype,
338
339    /**
340     * Called when an element is decorated as a list.
341     */
342    decorate: function() {
343      List.prototype.decorate.call(this);
344
345      this.dataModel = new ArrayDataModel([]);
346
347      // Whether the exceptions in this list allow an 'Ask every time' option.
348      this.enableAskOption = false;
349    },
350
351    /**
352     * Creates an item to go in the list.
353     * @param {Object} entry The element from the data model for this row.
354     */
355    createItem: function(entry) {
356      return new ExceptionsListItem(this.contentType,
357                                    this.mode,
358                                    this.enableAskOption,
359                                    entry);
360    },
361
362    /**
363     * Adds an exception to the js model.
364     * @param {Array} entry A pair of the form [filter, setting].
365     */
366    addException: function(entry) {
367      this.dataModel.push(entry);
368
369      // When an empty row is added, put it into editing mode.
370      if (!entry[0] && !entry[1]) {
371        var index = this.dataModel.length - 1;
372        var sm = this.selectionModel;
373        sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index;
374        this.scrollIndexIntoView(index);
375        var li = this.getListItemByIndex(index);
376        li.editing = true;
377      }
378    },
379
380    /**
381     * The browser has finished checking a pattern for validity. Update the
382     * list item to reflect this.
383     * @param {string} pattern The pattern.
384     * @param {bool} valid Whether said pattern is valid in the context of
385     *     a content exception setting.
386     */
387    patternValidityCheckComplete: function(pattern, valid) {
388      for (var i = 0; i < this.dataModel.length; i++) {
389        var listItem = this.getListItemByIndex(i);
390        if (listItem)
391          listItem.maybeSetPatternValid(pattern, valid);
392      }
393    },
394
395    /**
396     * Removes all exceptions from the js model.
397     */
398    clear: function() {
399      this.dataModel = new ArrayDataModel([]);
400    },
401
402    /**
403     * Removes all selected rows from browser's model.
404     */
405    removeSelectedRows: function() {
406      // The first member is the content type; the rest of the values are
407      // the patterns we are removing.
408      var args = [this.contentType];
409      var selectedItems = this.selectedItems;
410      for (var i = 0; i < selectedItems.length; i++) {
411        if (this.contentType == 'location') {
412          args.push(selectedItems[i][0]);   // origin (pattern)
413          args.push(selectedItems[i][2]);   // embedded origin
414        } else if (this.contentType == 'notifications') {
415          args.push(selectedItems[i][0]);   // origin (pattern)
416          args.push(selectedItems[i][1]);   // setting
417        } else {
418          args.push(this.mode);             // browser mode
419          args.push(selectedItems[i][0]);   // pattern
420        }
421      }
422
423      chrome.send('removeExceptions', args);
424    },
425
426    /**
427     * Puts the selected row in editing mode.
428     */
429    editSelectedRow: function() {
430      var li = this.getListItem(this.selectedItem);
431      if (li)
432        li.editing = true;
433    }
434  };
435
436  var ExceptionsArea = cr.ui.define('div');
437
438  ExceptionsArea.prototype = {
439    __proto__: HTMLDivElement.prototype,
440
441    decorate: function() {
442      // TODO(estade): need some sort of visual indication when the list is
443      // empty.
444      this.exceptionsList = this.querySelector('list');
445      this.exceptionsList.contentType = this.contentType;
446      this.exceptionsList.mode = this.mode;
447
448      ExceptionsList.decorate(this.exceptionsList);
449      this.exceptionsList.selectionModel.addEventListener(
450          'change', this.handleOnSelectionChange_.bind(this));
451
452      var self = this;
453      if (this.contentType != 'location' &&
454          this.contentType != 'notifications') {
455        var addRow = cr.doc.createElement('button');
456        addRow.textContent = templateData.addExceptionRow;
457        this.appendChild(addRow);
458
459        addRow.onclick = function(event) {
460          self.exceptionsList.addException(['', '']);
461        };
462
463        var editRow = cr.doc.createElement('button');
464        editRow.textContent = templateData.editExceptionRow;
465        this.appendChild(editRow);
466        this.editRow = editRow;
467
468        editRow.onclick = function(event) {
469          self.exceptionsList.editSelectedRow();
470        };
471      }
472
473      var removeRow = cr.doc.createElement('button');
474      removeRow.textContent = templateData.removeExceptionRow;
475      this.appendChild(removeRow);
476      this.removeRow = removeRow;
477
478      removeRow.onclick = function(event) {
479        self.exceptionsList.removeSelectedRows();
480      };
481
482      this.updateButtonSensitivity();
483
484      this.classList.add('hidden');
485
486      this.otrProfileExists = false;
487    },
488
489    /**
490     * The content type for this exceptions area, such as 'images'.
491     * @type {string}
492     */
493    get contentType() {
494      return this.getAttribute('contentType');
495    },
496    set contentType(type) {
497      return this.setAttribute('contentType', type);
498    },
499
500    /**
501     * The browser mode type for this exceptions area, 'otr' or 'normal'.
502     * @type {string}
503     */
504    get mode() {
505      return this.getAttribute('mode');
506    },
507    set mode(mode) {
508      return this.setAttribute('mode', mode);
509    },
510
511    /**
512     * Update the enabled/disabled state of the editing buttons based on which
513     * rows are selected.
514     */
515    updateButtonSensitivity: function() {
516      var selectionSize = this.exceptionsList.selectedItems.length;
517      if (this.editRow)
518        this.editRow.disabled = selectionSize != 1;
519      this.removeRow.disabled = selectionSize == 0;
520    },
521
522    /**
523     * Callback from the selection model.
524     * @param {!cr.Event} ce Event with change info.
525     * @private
526     */
527    handleOnSelectionChange_: function(ce) {
528      this.updateButtonSensitivity();
529   },
530  };
531
532  return {
533    ExceptionsListItem: ExceptionsListItem,
534    ExceptionsList: ExceptionsList,
535    ExceptionsArea: ExceptionsArea
536  };
537});
538