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