Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / options / pref_ui.js
blobc0776fee447b9fc043dab46e16d958609c550cf1
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.
5 cr.define('options', function() {
6   var Preferences = options.Preferences;
8   /**
9    * Allows an element to be disabled for several reasons.
10    * The element is disabled if at least one reason is true, and the reasons
11    * can be set separately.
12    * @private
13    * @param {!HTMLElement} el The element to update.
14    * @param {string} reason The reason for disabling the element.
15    * @param {boolean} disabled Whether the element should be disabled or enabled
16    * for the given |reason|.
17    */
18   function updateDisabledState(el, reason, disabled) {
19     if (!el.disabledReasons)
20       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     }
28     if (!el.disabled) {
29       // If the element is not disabled, there should be no reason, except for
30       // 'other'.
31       delete el.disabledReasons.other;
32       if (Object.keys(el.disabledReasons).length > 0)
33         console.error('Element is not disabled but should be');
34     }
36     if (disabled)
37       el.disabledReasons[reason] = true;
38     else
39       delete el.disabledReasons[reason];
41     el.disabled = Object.keys(el.disabledReasons).length > 0;
42   }
44   /////////////////////////////////////////////////////////////////////////////
45   // PrefInputElement class:
47   /**
48    * Define a constructor that uses an input element as its underlying element.
49    * @constructor
50    * @extends {HTMLInputElement}
51    */
52   var PrefInputElement = cr.ui.define('input');
54   PrefInputElement.prototype = {
55     // Set up the prototype chain
56     __proto__: HTMLInputElement.prototype,
58     /**
59      * Initialization function for the cr.ui framework.
60      */
61     decorate: function() {
62       var self = this;
64       // Listen for user events.
65       this.addEventListener('change', this.handleChange.bind(this));
67       // Listen for pref changes.
68       Preferences.getInstance().addEventListener(this.pref, function(event) {
69         if (event.value.uncommitted && !self.dialogPref)
70           return;
71         self.updateStateFromPref(event);
72         updateDisabledState(self, 'notUserModifiable', event.value.disabled);
73         self.controlledBy = event.value.controlledBy;
74       });
75     },
77     /**
78      * Handle changes to the input element's state made by the user. If a custom
79      * change handler does not suppress it, a default handler is invoked that
80      * updates the associated pref.
81      * @param {Event} event Change event.
82      * @protected
83      */
84     handleChange: function(event) {
85       if (!this.customChangeHandler(event))
86         this.updatePrefFromState();
87     },
89     /**
90      * Handles changes to the pref. If a custom change handler does not suppress
91      * it, a default handler is invoked that updates the input element's state.
92      * @param {Event} event Pref change event.
93      * @protected
94      */
95     updateStateFromPref: function(event) {
96       if (!this.customPrefChangeHandler(event))
97         this.value = event.value.value;
98     },
100     /**
101      * An abstract method for all subclasses to override to update their
102      * preference from existing state.
103      * @protected
104      */
105     updatePrefFromState: assertNotReached,
107     /**
108      * See |updateDisabledState| above.
109      */
110     setDisabled: function(reason, disabled) {
111       updateDisabledState(this, reason, disabled);
112     },
114     /**
115      * Custom change handler that is invoked first when the user makes changes
116      * to the input element's state. If it returns false, a default handler is
117      * invoked next that updates the associated pref. If it returns true, the
118      * default handler is suppressed (i.e., this works like stopPropagation or
119      * cancelBubble).
120      * @param {Event} event Input element change event.
121      */
122     customChangeHandler: function(event) {
123       return false;
124     },
126     /**
127      * Custom change handler that is invoked first when the preference
128      * associated with the input element changes. If it returns false, a default
129      * handler is invoked next that updates the input element. If it returns
130      * true, the default handler is suppressed.
131      * @param {Event} event Input element change event.
132      */
133     customPrefChangeHandler: function(event) {
134       return false;
135     },
136   };
138   /**
139    * The name of the associated preference.
140    */
141   cr.defineProperty(PrefInputElement, 'pref', cr.PropertyKind.ATTR);
143   /**
144    * The data type of the associated preference, only relevant for derived
145    * classes that support different data types.
146    */
147   cr.defineProperty(PrefInputElement, 'dataType', cr.PropertyKind.ATTR);
149   /**
150    * Whether this input element is part of a dialog. If so, changes take effect
151    * in the settings UI immediately but are only actually committed when the
152    * user confirms the dialog. If the user cancels the dialog instead, the
153    * changes are rolled back in the settings UI and never committed.
154    */
155   cr.defineProperty(PrefInputElement, 'dialogPref', cr.PropertyKind.BOOL_ATTR);
157   /**
158    * Whether the associated preference is controlled by a source other than the
159    * user's setting (can be 'policy', 'extension', 'recommended' or unset).
160    */
161   cr.defineProperty(PrefInputElement, 'controlledBy', cr.PropertyKind.ATTR);
163   /**
164    * The user metric string.
165    */
166   cr.defineProperty(PrefInputElement, 'metric', cr.PropertyKind.ATTR);
168   /////////////////////////////////////////////////////////////////////////////
169   // PrefCheckbox class:
171   /**
172    * Define a constructor that uses an input element as its underlying element.
173    * @constructor
174    * @extends {options.PrefInputElement}
175    */
176   var PrefCheckbox = cr.ui.define('input');
178   PrefCheckbox.prototype = {
179     // Set up the prototype chain
180     __proto__: PrefInputElement.prototype,
182     /**
183      * Initialization function for the cr.ui framework.
184      */
185     decorate: function() {
186       PrefInputElement.prototype.decorate.call(this);
187       this.type = 'checkbox';
189       // Consider a checked dialog checkbox as a 'suggestion' which is committed
190       // once the user confirms the dialog.
191       if (this.dialogPref && this.checked)
192         this.updatePrefFromState();
193     },
195     /**
196      * Update the associated pref when when the user makes changes to the
197      * checkbox state.
198      * @override
199      */
200     updatePrefFromState: function() {
201       var value = this.inverted_pref ? !this.checked : this.checked;
202       Preferences.setBooleanPref(this.pref, value,
203                                  !this.dialogPref, this.metric);
204     },
206     /** @override */
207     updateStateFromPref: function(event) {
208       if (!this.customPrefChangeHandler(event))
209         this.defaultPrefChangeHandler(event);
210     },
212     /**
213      * @param {Event} event A pref change event.
214      */
215     defaultPrefChangeHandler: function(event) {
216       var value = Boolean(event.value.value);
217       this.checked = this.inverted_pref ? !value : value;
218     },
219   };
221   /**
222    * Whether the mapping between checkbox state and associated pref is inverted.
223    */
224   cr.defineProperty(PrefCheckbox, 'inverted_pref', cr.PropertyKind.BOOL_ATTR);
226   /////////////////////////////////////////////////////////////////////////////
227   // PrefNumber class:
229   // Define a constructor that uses an input element as its underlying element.
230   var PrefNumber = cr.ui.define('input');
232   PrefNumber.prototype = {
233     // Set up the prototype chain
234     __proto__: PrefInputElement.prototype,
236     /**
237      * Initialization function for the cr.ui framework.
238      */
239     decorate: function() {
240       PrefInputElement.prototype.decorate.call(this);
241       this.type = 'number';
242     },
244     /**
245      * Update the associated pref when the user inputs a number.
246      * @override
247      */
248     updatePrefFromState: function() {
249       if (this.validity.valid) {
250         Preferences.setIntegerPref(this.pref, this.value,
251                                    !this.dialogPref, this.metric);
252       }
253     },
254   };
256   /////////////////////////////////////////////////////////////////////////////
257   // PrefRadio class:
259   //Define a constructor that uses an input element as its underlying element.
260   var PrefRadio = cr.ui.define('input');
262   PrefRadio.prototype = {
263     // Set up the prototype chain
264     __proto__: PrefInputElement.prototype,
266     /**
267      * Initialization function for the cr.ui framework.
268      */
269     decorate: function() {
270       PrefInputElement.prototype.decorate.call(this);
271       this.type = 'radio';
272     },
274     /**
275      * Update the associated pref when when the user selects the radio button.
276      * @override
277      */
278     updatePrefFromState: function() {
279       if (this.value == 'true' || this.value == 'false') {
280         Preferences.setBooleanPref(this.pref,
281                                    this.value == String(this.checked),
282                                    !this.dialogPref, this.metric);
283       } else {
284         Preferences.setIntegerPref(this.pref, this.value,
285                                    !this.dialogPref, this.metric);
286       }
287     },
289     /** @override */
290     updateStateFromPref: function(event) {
291       if (!this.customPrefChangeHandler(event))
292         this.checked = this.value == String(event.value.value);
293     },
294   };
296   /////////////////////////////////////////////////////////////////////////////
297   // PrefRange class:
299   /**
300    * Define a constructor that uses an input element as its underlying element.
301    * @constructor
302    * @extends {options.PrefInputElement}
303    */
304   var PrefRange = cr.ui.define('input');
306   PrefRange.prototype = {
307     // Set up the prototype chain
308     __proto__: PrefInputElement.prototype,
310     /**
311      * The map from slider position to corresponding pref value.
312      */
313     valueMap: undefined,
315     /**
316      * Initialization function for the cr.ui framework.
317      */
318     decorate: function() {
319       PrefInputElement.prototype.decorate.call(this);
320       this.type = 'range';
322       // Listen for user events.
323       // TODO(jhawkins): Add onmousewheel handling once the associated WK bug is
324       // fixed.
325       // https://bugs.webkit.org/show_bug.cgi?id=52256
326       this.addEventListener('keyup', this.handleRelease_.bind(this));
327       this.addEventListener('mouseup', this.handleRelease_.bind(this));
328       this.addEventListener('touchcancel', this.handleRelease_.bind(this));
329       this.addEventListener('touchend', this.handleRelease_.bind(this));
330     },
332     /**
333      * Update the associated pref when when the user releases the slider.
334      * @override
335      */
336     updatePrefFromState: function() {
337       Preferences.setIntegerPref(
338           this.pref,
339           this.mapPositionToPref(parseInt(this.value, 10)),
340           !this.dialogPref,
341           this.metric);
342     },
344     /** @override */
345     handleChange: function() {
346       // Ignore changes to the slider position made by the user while the slider
347       // has not been released.
348     },
350     /**
351      * Handle changes to the slider position made by the user when the slider is
352      * released. If a custom change handler does not suppress it, a default
353      * handler is invoked that updates the associated pref.
354      * @param {Event} event Change event.
355      * @private
356      */
357     handleRelease_: function(event) {
358       if (!this.customChangeHandler(event))
359         this.updatePrefFromState();
360     },
362     /**
363      * Handles changes to the pref associated with the slider. If a custom
364      * change handler does not suppress it, a default handler is invoked that
365      * updates the slider position.
366      * @override.
367      */
368     updateStateFromPref: function(event) {
369       if (this.customPrefChangeHandler(event))
370         return;
371       var value = event.value.value;
372       this.value = this.valueMap ? this.valueMap.indexOf(value) : value;
373     },
375     /**
376      * Map slider position to the range of values provided by the client,
377      * represented by |valueMap|.
378      * @param {number} position The slider position to map.
379      */
380     mapPositionToPref: function(position) {
381       return this.valueMap ? this.valueMap[position] : position;
382     },
383   };
385   /////////////////////////////////////////////////////////////////////////////
386   // PrefSelect class:
388   // Define a constructor that uses a select element as its underlying element.
389   var PrefSelect = cr.ui.define('select');
391   PrefSelect.prototype = {
392     // Set up the prototype chain
393     __proto__: HTMLSelectElement.prototype,
395     /** @override */
396     decorate: PrefInputElement.prototype.decorate,
398     /** @override */
399     handleChange: PrefInputElement.prototype.handleChange,
401     /**
402      * Update the associated pref when when the user selects an item.
403      * @override
404      */
405     updatePrefFromState: function() {
406       var value = this.options[this.selectedIndex].value;
407       switch (this.dataType) {
408         case 'number':
409           Preferences.setIntegerPref(this.pref, value,
410                                      !this.dialogPref, this.metric);
411           break;
412         case 'double':
413           Preferences.setDoublePref(this.pref, value,
414                                     !this.dialogPref, this.metric);
415           break;
416         case 'boolean':
417           Preferences.setBooleanPref(this.pref, value == 'true',
418                                      !this.dialogPref, this.metric);
419           break;
420         case 'string':
421           Preferences.setStringPref(this.pref, value,
422                                     !this.dialogPref, this.metric);
423           break;
424         default:
425           console.error('Unknown data type for <select> UI element: ' +
426                         this.dataType);
427       }
428     },
430     /** @override */
431     updateStateFromPref: function(event) {
432       if (this.customPrefChangeHandler(event))
433         return;
435       // Make sure the value is a string, because the value is stored as a
436       // string in the HTMLOptionElement.
437       var value = String(event.value.value);
439       var found = false;
440       for (var i = 0; i < this.options.length; i++) {
441         if (this.options[i].value == value) {
442           this.selectedIndex = i;
443           found = true;
444         }
445       }
447       // Item not found, select first item.
448       if (!found)
449         this.selectedIndex = 0;
451       // The "onchange" event automatically fires when the user makes a manual
452       // change. It should never be fired for a programmatic change. However,
453       // these two lines were here already and it is hard to tell who may be
454       // relying on them.
455       if (this.onchange)
456         this.onchange(event);
457     },
459     /** @override */
460     setDisabled: PrefInputElement.prototype.setDisabled,
462     /** @override */
463     customChangeHandler: PrefInputElement.prototype.customChangeHandler,
465     /** @override */
466     customPrefChangeHandler: PrefInputElement.prototype.customPrefChangeHandler,
467   };
469   /**
470    * The name of the associated preference.
471    */
472   cr.defineProperty(PrefSelect, 'pref', cr.PropertyKind.ATTR);
474   /**
475    * The data type of the associated preference, only relevant for derived
476    * classes that support different data types.
477    */
478   cr.defineProperty(PrefSelect, 'dataType', cr.PropertyKind.ATTR);
480   /**
481    * Whether this input element is part of a dialog. If so, changes take effect
482    * in the settings UI immediately but are only actually committed when the
483    * user confirms the dialog. If the user cancels the dialog instead, the
484    * changes are rolled back in the settings UI and never committed.
485    */
486   cr.defineProperty(PrefSelect, 'dialogPref', cr.PropertyKind.BOOL_ATTR);
488   /**
489    * Whether the associated preference is controlled by a source other than the
490    * user's setting (can be 'policy', 'extension', 'recommended' or unset).
491    */
492   cr.defineProperty(PrefSelect, 'controlledBy', cr.PropertyKind.ATTR);
494   /**
495    * The user metric string.
496    */
497   cr.defineProperty(PrefSelect, 'metric', cr.PropertyKind.ATTR);
499   /////////////////////////////////////////////////////////////////////////////
500   // PrefTextField class:
502   // Define a constructor that uses an input element as its underlying element.
503   var PrefTextField = cr.ui.define('input');
505   PrefTextField.prototype = {
506     // Set up the prototype chain
507     __proto__: PrefInputElement.prototype,
509     /**
510      * Initialization function for the cr.ui framework.
511      */
512     decorate: function() {
513       PrefInputElement.prototype.decorate.call(this);
514       var self = this;
516       // Listen for user events.
517       window.addEventListener('unload', function() {
518         if (document.activeElement == self)
519           self.blur();
520       });
521     },
523     /**
524      * Update the associated pref when when the user inputs text.
525      * @override
526      */
527     updatePrefFromState: function(event) {
528       switch (this.dataType) {
529         case 'number':
530           Preferences.setIntegerPref(this.pref, this.value,
531                                      !this.dialogPref, this.metric);
532           break;
533         case 'double':
534           Preferences.setDoublePref(this.pref, this.value,
535                                     !this.dialogPref, this.metric);
536           break;
537         case 'url':
538           Preferences.setURLPref(this.pref, this.value,
539                                  !this.dialogPref, this.metric);
540           break;
541         default:
542           Preferences.setStringPref(this.pref, this.value,
543                                     !this.dialogPref, this.metric);
544           break;
545       }
546     },
547   };
549   /////////////////////////////////////////////////////////////////////////////
550   // PrefPortNumber class:
552   // Define a constructor that uses an input element as its underlying element.
553   var PrefPortNumber = cr.ui.define('input');
555   PrefPortNumber.prototype = {
556     // Set up the prototype chain
557     __proto__: PrefTextField.prototype,
559     /**
560      * Initialization function for the cr.ui framework.
561      */
562     decorate: function() {
563       var self = this;
564       self.type = 'text';
565       self.dataType = 'number';
566       PrefTextField.prototype.decorate.call(this);
567       self.oninput = function() {
568         // Note that using <input type="number"> is insufficient to restrict
569         // the input as it allows negative numbers and does not limit the
570         // number of charactes typed even if a range is set.  Furthermore,
571         // it sometimes produces strange repaint artifacts.
572         var filtered = self.value.replace(/[^0-9]/g, '');
573         if (filtered != self.value)
574           self.value = filtered;
575       };
576     }
577   };
579   /////////////////////////////////////////////////////////////////////////////
580   // PrefButton class:
582   // Define a constructor that uses a button element as its underlying element.
583   var PrefButton = cr.ui.define('button');
585   PrefButton.prototype = {
586     // Set up the prototype chain
587     __proto__: HTMLButtonElement.prototype,
589     /**
590      * Initialization function for the cr.ui framework.
591      */
592     decorate: function() {
593       var self = this;
595       // Listen for pref changes.
596       // This element behaves like a normal button and does not affect the
597       // underlying preference; it just becomes disabled when the preference is
598       // managed, and its value is false. This is useful for buttons that should
599       // be disabled when the underlying Boolean preference is set to false by a
600       // policy or extension.
601       Preferences.getInstance().addEventListener(this.pref, function(event) {
602         updateDisabledState(self, 'notUserModifiable',
603                             event.value.disabled && !event.value.value);
604         self.controlledBy = event.value.controlledBy;
605       });
606     },
608     /**
609      * See |updateDisabledState| above.
610      */
611     setDisabled: function(reason, disabled) {
612       updateDisabledState(this, reason, disabled);
613     },
614   };
616   /**
617    * The name of the associated preference.
618    */
619   cr.defineProperty(PrefButton, 'pref', cr.PropertyKind.ATTR);
621   /**
622    * Whether the associated preference is controlled by a source other than the
623    * user's setting (can be 'policy', 'extension', 'recommended' or unset).
624    */
625   cr.defineProperty(PrefButton, 'controlledBy', cr.PropertyKind.ATTR);
627   // Export
628   return {
629     PrefCheckbox: PrefCheckbox,
630     PrefInputElement: PrefInputElement,
631     PrefNumber: PrefNumber,
632     PrefRadio: PrefRadio,
633     PrefRange: PrefRange,
634     PrefSelect: PrefSelect,
635     PrefTextField: PrefTextField,
636     PrefPortNumber: PrefPortNumber,
637     PrefButton: PrefButton
638   };