• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
4
5cr.define('options', function() {
6
7  var Preferences = options.Preferences;
8
9  /**
10   * Allows an element to be disabled for several reasons.
11   * The element is disabled if at least one reason is true, and the reasons
12   * can be set separately.
13   * @private
14   * @param {!HTMLElement} el The element to update.
15   * @param {string} reason The reason for disabling the element.
16   * @param {boolean} disabled Whether the element should be disabled or enabled
17   * for the given |reason|.
18   */
19  function updateDisabledState_(el, reason, disabled) {
20    if (!el.disabledReasons)
21      el.disabledReasons = {};
22    if (el.disabled && (Object.keys(el.disabledReasons).length == 0)) {
23      // The element has been previously disabled without a reason, so we add
24      // one to keep it disabled.
25      el.disabledReasons.other = true;
26    }
27    if (!el.disabled) {
28      // If the element is not disabled, there should be no reason, except for
29      // 'other'.
30      delete el.disabledReasons.other;
31      if (Object.keys(el.disabledReasons).length > 0)
32        console.error('Element is not disabled but should be');
33    }
34    if (disabled) {
35      el.disabledReasons[reason] = true;
36    } else {
37      delete el.disabledReasons[reason];
38    }
39    el.disabled = Object.keys(el.disabledReasons).length > 0;
40  }
41
42  /////////////////////////////////////////////////////////////////////////////
43  // PrefInputElement class:
44
45  // Define a constructor that uses an input element as its underlying element.
46  var PrefInputElement = cr.ui.define('input');
47
48  PrefInputElement.prototype = {
49    // Set up the prototype chain
50    __proto__: HTMLInputElement.prototype,
51
52    /**
53     * Initialization function for the cr.ui framework.
54     */
55    decorate: function() {
56      var self = this;
57
58      // Listen for user events.
59      this.addEventListener('change', this.handleChange_.bind(this));
60
61      // Listen for pref changes.
62      Preferences.getInstance().addEventListener(this.pref, function(event) {
63        if (event.value.uncommitted && !self.dialogPref)
64          return;
65        self.updateStateFromPref_(event);
66        updateDisabledState_(self, 'notUserModifiable', event.value.disabled);
67        self.controlledBy = event.value.controlledBy;
68      });
69    },
70
71    /**
72     * Handle changes to the input element's state made by the user. If a custom
73     * change handler does not suppress it, a default handler is invoked that
74     * updates the associated pref.
75     * @param {Event} event Change event.
76     * @private
77     */
78    handleChange_: function(event) {
79      if (!this.customChangeHandler(event))
80        this.updatePrefFromState_();
81    },
82
83    /**
84     * Update the input element's state when the associated pref changes.
85     * @param {Event} event Pref change event.
86     * @private
87     */
88    updateStateFromPref_: function(event) {
89      this.value = event.value.value;
90    },
91
92    /**
93     * See |updateDisabledState_| above.
94     */
95    setDisabled: function(reason, disabled) {
96      updateDisabledState_(this, reason, disabled);
97    },
98
99    /**
100     * Custom change handler that is invoked first when the user makes changes
101     * to the input element's state. If it returns false, a default handler is
102     * invoked next that updates the associated pref. If it returns true, the
103     * default handler is suppressed (i.e., this works like stopPropagation or
104     * cancelBubble).
105     * @param {Event} event Input element change event.
106     */
107    customChangeHandler: function(event) {
108      return false;
109    },
110  };
111
112  /**
113   * The name of the associated preference.
114   * @type {string}
115   */
116  cr.defineProperty(PrefInputElement, 'pref', cr.PropertyKind.ATTR);
117
118  /**
119   * The data type of the associated preference, only relevant for derived
120   * classes that support different data types.
121   * @type {string}
122   */
123  cr.defineProperty(PrefInputElement, 'dataType', cr.PropertyKind.ATTR);
124
125  /**
126   * Whether this input element is part of a dialog. If so, changes take effect
127   * in the settings UI immediately but are only actually committed when the
128   * user confirms the dialog. If the user cancels the dialog instead, the
129   * changes are rolled back in the settings UI and never committed.
130   * @type {boolean}
131   */
132  cr.defineProperty(PrefInputElement, 'dialogPref', cr.PropertyKind.BOOL_ATTR);
133
134  /**
135   * Whether the associated preference is controlled by a source other than the
136   * user's setting (can be 'policy', 'extension', 'recommended' or unset).
137   * @type {string}
138   */
139  cr.defineProperty(PrefInputElement, 'controlledBy', cr.PropertyKind.ATTR);
140
141  /**
142   * The user metric string.
143   * @type {string}
144   */
145  cr.defineProperty(PrefInputElement, 'metric', cr.PropertyKind.ATTR);
146
147  /////////////////////////////////////////////////////////////////////////////
148  // PrefCheckbox class:
149
150  // Define a constructor that uses an input element as its underlying element.
151  var PrefCheckbox = cr.ui.define('input');
152
153  PrefCheckbox.prototype = {
154    // Set up the prototype chain
155    __proto__: PrefInputElement.prototype,
156
157    /**
158     * Initialization function for the cr.ui framework.
159     */
160    decorate: function() {
161      PrefInputElement.prototype.decorate.call(this);
162      this.type = 'checkbox';
163
164      // Consider a checked dialog checkbox as a 'suggestion' which is committed
165      // once the user confirms the dialog.
166      if (this.dialogPref && this.checked)
167        this.updatePrefFromState_();
168    },
169
170    /**
171     * Update the associated pref when when the user makes changes to the
172     * checkbox state.
173     * @private
174     */
175    updatePrefFromState_: function() {
176      var value = this.inverted_pref ? !this.checked : this.checked;
177      Preferences.setBooleanPref(this.pref, value,
178                                 !this.dialogPref, this.metric);
179    },
180
181    /**
182     * Update the checkbox state when the associated pref changes.
183     * @param {Event} event Pref change event.
184     * @private
185     */
186    updateStateFromPref_: function(event) {
187      var value = Boolean(event.value.value);
188      this.checked = this.inverted_pref ? !value : value;
189    },
190  };
191
192  /**
193   * Whether the mapping between checkbox state and associated pref is inverted.
194   * @type {boolean}
195   */
196  cr.defineProperty(PrefCheckbox, 'inverted_pref', cr.PropertyKind.BOOL_ATTR);
197
198  /////////////////////////////////////////////////////////////////////////////
199  // PrefNumber class:
200
201  // Define a constructor that uses an input element as its underlying element.
202  var PrefNumber = cr.ui.define('input');
203
204  PrefNumber.prototype = {
205    // Set up the prototype chain
206    __proto__: PrefInputElement.prototype,
207
208    /**
209     * Initialization function for the cr.ui framework.
210     */
211    decorate: function() {
212      PrefInputElement.prototype.decorate.call(this);
213      this.type = 'number';
214    },
215
216    /**
217     * Update the associated pref when when the user inputs a number.
218     * @private
219     */
220    updatePrefFromState_: function() {
221      if (this.validity.valid) {
222        Preferences.setIntegerPref(this.pref, this.value,
223                                   !this.dialogPref, this.metric);
224      }
225    },
226  };
227
228  /////////////////////////////////////////////////////////////////////////////
229  // PrefRadio class:
230
231  //Define a constructor that uses an input element as its underlying element.
232  var PrefRadio = cr.ui.define('input');
233
234  PrefRadio.prototype = {
235    // Set up the prototype chain
236    __proto__: PrefInputElement.prototype,
237
238    /**
239     * Initialization function for the cr.ui framework.
240     */
241    decorate: function() {
242      PrefInputElement.prototype.decorate.call(this);
243      this.type = 'radio';
244    },
245
246    /**
247     * Update the associated pref when when the user selects the radio button.
248     * @private
249     */
250    updatePrefFromState_: function() {
251      if (this.value == 'true' || this.value == 'false') {
252        Preferences.setBooleanPref(this.pref,
253                                   this.value == String(this.checked),
254                                   !this.dialogPref, this.metric);
255      } else {
256        Preferences.setIntegerPref(this.pref, this.value,
257                                   !this.dialogPref, this.metric);
258      }
259    },
260
261    /**
262     * Update the radio button state when the associated pref changes.
263     * @param {Event} event Pref change event.
264     * @private
265     */
266    updateStateFromPref_: function(event) {
267      this.checked = this.value == String(event.value.value);
268    },
269  };
270
271  /////////////////////////////////////////////////////////////////////////////
272  // PrefRange class:
273
274  // Define a constructor that uses an input element as its underlying element.
275  var PrefRange = cr.ui.define('input');
276
277  PrefRange.prototype = {
278    // Set up the prototype chain
279    __proto__: PrefInputElement.prototype,
280
281    /**
282     * The map from slider position to corresponding pref value.
283     */
284    valueMap: undefined,
285
286    /**
287     * Initialization function for the cr.ui framework.
288     */
289    decorate: function() {
290      PrefInputElement.prototype.decorate.call(this);
291      this.type = 'range';
292
293      // Listen for user events.
294      // TODO(jhawkins): Add onmousewheel handling once the associated WK bug is
295      // fixed.
296      // https://bugs.webkit.org/show_bug.cgi?id=52256
297      this.addEventListener('keyup', this.handleRelease_.bind(this));
298      this.addEventListener('mouseup', this.handleRelease_.bind(this));
299    },
300
301    /**
302     * Update the associated pref when when the user releases the slider.
303     * @private
304     */
305    updatePrefFromState_: function() {
306      Preferences.setIntegerPref(this.pref, this.mapPositionToPref(this.value),
307                                 !this.dialogPref, this.metric);
308    },
309
310    /**
311     * Ignore changes to the slider position made by the user while the slider
312     * has not been released.
313     * @private
314     */
315    handleChange_: function() {
316    },
317
318    /**
319     * Handle changes to the slider position made by the user when the slider is
320     * released. If a custom change handler does not suppress it, a default
321     * handler is invoked that updates the associated pref.
322     * @param {Event} event Change event.
323     * @private
324     */
325    handleRelease_: function(event) {
326      if (!this.customChangeHandler(event))
327        this.updatePrefFromState_();
328    },
329
330    /**
331     * Update the slider position when the associated pref changes.
332     * @param {Event} event Pref change event.
333     * @private
334     */
335    updateStateFromPref_: function(event) {
336      var value = event.value.value;
337      this.value = this.valueMap ? this.valueMap.indexOf(value) : value;
338    },
339
340    /**
341     * Map slider position to the range of values provided by the client,
342     * represented by |valueMap|.
343     * @param {number} position The slider position to map.
344     */
345    mapPositionToPref: function(position) {
346      return this.valueMap ? this.valueMap[position] : position;
347    },
348  };
349
350  /////////////////////////////////////////////////////////////////////////////
351  // PrefSelect class:
352
353  // Define a constructor that uses a select element as its underlying element.
354  var PrefSelect = cr.ui.define('select');
355
356  PrefSelect.prototype = {
357    // Set up the prototype chain
358    __proto__: PrefInputElement.prototype,
359
360    /**
361     * Update the associated pref when when the user selects an item.
362     * @private
363     */
364    updatePrefFromState_: function() {
365      var value = this.options[this.selectedIndex].value;
366      switch (this.dataType) {
367        case 'number':
368          Preferences.setIntegerPref(this.pref, value,
369                                     !this.dialogPref, this.metric);
370          break;
371        case 'double':
372          Preferences.setDoublePref(this.pref, value,
373                                    !this.dialogPref, this.metric);
374          break;
375        case 'boolean':
376          Preferences.setBooleanPref(this.pref, value == 'true',
377                                     !this.dialogPref, this.metric);
378          break;
379        case 'string':
380          Preferences.setStringPref(this.pref, value,
381                                    !this.dialogPref, this.metric);
382          break;
383        default:
384          console.error('Unknown data type for <select> UI element: ' +
385                        this.dataType);
386      }
387    },
388
389    /**
390     * Update the selected item when the associated pref changes.
391     * @param {Event} event Pref change event.
392     * @private
393     */
394    updateStateFromPref_: function(event) {
395      // Make sure the value is a string, because the value is stored as a
396      // string in the HTMLOptionElement.
397      value = String(event.value.value);
398
399      var found = false;
400      for (var i = 0; i < this.options.length; i++) {
401        if (this.options[i].value == value) {
402          this.selectedIndex = i;
403          found = true;
404        }
405      }
406
407      // Item not found, select first item.
408      if (!found)
409        this.selectedIndex = 0;
410
411      // The "onchange" event automatically fires when the user makes a manual
412      // change. It should never be fired for a programmatic change. However,
413      // these two lines were here already and it is hard to tell who may be
414      // relying on them.
415      if (this.onchange)
416        this.onchange(event);
417    },
418  };
419
420  /////////////////////////////////////////////////////////////////////////////
421  // PrefTextField class:
422
423  // Define a constructor that uses an input element as its underlying element.
424  var PrefTextField = cr.ui.define('input');
425
426  PrefTextField.prototype = {
427    // Set up the prototype chain
428    __proto__: PrefInputElement.prototype,
429
430    /**
431     * Initialization function for the cr.ui framework.
432     */
433    decorate: function() {
434      PrefInputElement.prototype.decorate.call(this);
435      var self = this;
436
437      // Listen for user events.
438      window.addEventListener('unload', function() {
439        if (document.activeElement == self)
440          self.blur();
441      });
442    },
443
444    /**
445     * Update the associated pref when when the user inputs text.
446     * @private
447     */
448    updatePrefFromState_: function(event) {
449      switch (this.dataType) {
450        case 'number':
451          Preferences.setIntegerPref(this.pref, this.value,
452                                     !this.dialogPref, this.metric);
453          break;
454        case 'double':
455          Preferences.setDoublePref(this.pref, this.value,
456                                    !this.dialogPref, this.metric);
457          break;
458        case 'url':
459          Preferences.setURLPref(this.pref, this.value,
460                                 !this.dialogPref, this.metric);
461          break;
462        default:
463          Preferences.setStringPref(this.pref, this.value,
464                                    !this.dialogPref, this.metric);
465          break;
466      }
467    },
468  };
469
470  /////////////////////////////////////////////////////////////////////////////
471  // PrefPortNumber class:
472
473  // Define a constructor that uses an input element as its underlying element.
474  var PrefPortNumber = cr.ui.define('input');
475
476  PrefPortNumber.prototype = {
477    // Set up the prototype chain
478    __proto__: PrefTextField.prototype,
479
480    /**
481     * Initialization function for the cr.ui framework.
482     */
483    decorate: function() {
484      var self = this;
485      self.type = 'text';
486      self.dataType = 'number';
487      PrefTextField.prototype.decorate.call(this);
488      self.oninput = function() {
489        // Note that using <input type="number"> is insufficient to restrict
490        // the input as it allows negative numbers and does not limit the
491        // number of charactes typed even if a range is set.  Furthermore,
492        // it sometimes produces strange repaint artifacts.
493        var filtered = self.value.replace(/[^0-9]/g, '');
494        if (filtered != self.value)
495          self.value = filtered;
496      };
497    }
498  };
499
500  /////////////////////////////////////////////////////////////////////////////
501  // PrefButton class:
502
503  // Define a constructor that uses a button element as its underlying element.
504  var PrefButton = cr.ui.define('button');
505
506  PrefButton.prototype = {
507    // Set up the prototype chain
508    __proto__: HTMLButtonElement.prototype,
509
510    /**
511     * Initialization function for the cr.ui framework.
512     */
513    decorate: function() {
514      var self = this;
515
516      // Listen for pref changes.
517      // This element behaves like a normal button and does not affect the
518      // underlying preference; it just becomes disabled when the preference is
519      // managed, and its value is false. This is useful for buttons that should
520      // be disabled when the underlying Boolean preference is set to false by a
521      // policy or extension.
522      Preferences.getInstance().addEventListener(this.pref, function(event) {
523        updateDisabledState_(self, 'notUserModifiable',
524                             event.value.disabled && !event.value.value);
525        self.controlledBy = event.value.controlledBy;
526      });
527    },
528
529    /**
530     * See |updateDisabledState_| above.
531     */
532    setDisabled: function(reason, disabled) {
533      updateDisabledState_(this, reason, disabled);
534    },
535  };
536
537  /**
538   * The name of the associated preference.
539   * @type {string}
540   */
541  cr.defineProperty(PrefButton, 'pref', cr.PropertyKind.ATTR);
542
543  /**
544   * Whether the associated preference is controlled by a source other than the
545   * user's setting (can be 'policy', 'extension', 'recommended' or unset).
546   * @type {string}
547   */
548  cr.defineProperty(PrefButton, 'controlledBy', cr.PropertyKind.ATTR);
549
550  // Export
551  return {
552    PrefCheckbox: PrefCheckbox,
553    PrefNumber: PrefNumber,
554    PrefRadio: PrefRadio,
555    PrefRange: PrefRange,
556    PrefSelect: PrefSelect,
557    PrefTextField: PrefTextField,
558    PrefPortNumber: PrefPortNumber,
559    PrefButton: PrefButton
560  };
561
562});
563