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