- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / options / pref_ui.js
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
5 cr.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
165     /**
166      * Update the associated pref when when the user makes changes to the
167      * checkbox state.
168      * @private
169      */
170     updatePrefFromState_: function() {
171       var value = this.inverted_pref ? !this.checked : this.checked;
172       Preferences.setBooleanPref(this.pref, value,
173                                  !this.dialogPref, this.metric);
174     },
175
176     /**
177      * Update the checkbox state when the associated pref changes.
178      * @param {Event} event Pref change event.
179      * @private
180      */
181     updateStateFromPref_: function(event) {
182       var value = Boolean(event.value.value);
183       this.checked = this.inverted_pref ? !value : value;
184     },
185   };
186
187   /**
188    * Whether the mapping between checkbox state and associated pref is inverted.
189    * @type {boolean}
190    */
191   cr.defineProperty(PrefCheckbox, 'inverted_pref', cr.PropertyKind.BOOL_ATTR);
192
193   /////////////////////////////////////////////////////////////////////////////
194   // PrefNumber class:
195
196   // Define a constructor that uses an input element as its underlying element.
197   var PrefNumber = cr.ui.define('input');
198
199   PrefNumber.prototype = {
200     // Set up the prototype chain
201     __proto__: PrefInputElement.prototype,
202
203     /**
204      * Initialization function for the cr.ui framework.
205      */
206     decorate: function() {
207       PrefInputElement.prototype.decorate.call(this);
208       this.type = 'number';
209     },
210
211     /**
212      * Update the associated pref when when the user inputs a number.
213      * @private
214      */
215     updatePrefFromState_: function() {
216       if (this.validity.valid) {
217         Preferences.setIntegerPref(this.pref, this.value,
218                                    !this.dialogPref, this.metric);
219       }
220     },
221   };
222
223   /////////////////////////////////////////////////////////////////////////////
224   // PrefRadio class:
225
226   //Define a constructor that uses an input element as its underlying element.
227   var PrefRadio = cr.ui.define('input');
228
229   PrefRadio.prototype = {
230     // Set up the prototype chain
231     __proto__: PrefInputElement.prototype,
232
233     /**
234      * Initialization function for the cr.ui framework.
235      */
236     decorate: function() {
237       PrefInputElement.prototype.decorate.call(this);
238       this.type = 'radio';
239     },
240
241     /**
242      * Update the associated pref when when the user selects the radio button.
243      * @private
244      */
245     updatePrefFromState_: function() {
246       if (this.value == 'true' || this.value == 'false') {
247         Preferences.setBooleanPref(this.pref,
248                                    this.value == String(this.checked),
249                                    !this.dialogPref, this.metric);
250       } else {
251         Preferences.setIntegerPref(this.pref, this.value,
252                                    !this.dialogPref, this.metric);
253       }
254     },
255
256     /**
257      * Update the radio button state when the associated pref changes.
258      * @param {Event} event Pref change event.
259      * @private
260      */
261     updateStateFromPref_: function(event) {
262       this.checked = this.value == String(event.value.value);
263     },
264   };
265
266   /////////////////////////////////////////////////////////////////////////////
267   // PrefRange class:
268
269   // Define a constructor that uses an input element as its underlying element.
270   var PrefRange = cr.ui.define('input');
271
272   PrefRange.prototype = {
273     // Set up the prototype chain
274     __proto__: PrefInputElement.prototype,
275
276     /**
277      * The map from slider position to corresponding pref value.
278      */
279     valueMap: undefined,
280
281     /**
282      * Initialization function for the cr.ui framework.
283      */
284     decorate: function() {
285       PrefInputElement.prototype.decorate.call(this);
286       this.type = 'range';
287
288       // Listen for user events.
289       // TODO(jhawkins): Add onmousewheel handling once the associated WK bug is
290       // fixed.
291       // https://bugs.webkit.org/show_bug.cgi?id=52256
292       this.addEventListener('keyup', this.handleRelease_.bind(this));
293       this.addEventListener('mouseup', this.handleRelease_.bind(this));
294     },
295
296     /**
297      * Update the associated pref when when the user releases the slider.
298      * @private
299      */
300     updatePrefFromState_: function() {
301       Preferences.setIntegerPref(this.pref, this.mapPositionToPref(this.value),
302                                  !this.dialogPref, this.metric);
303     },
304
305     /**
306      * Ignore changes to the slider position made by the user while the slider
307      * has not been released.
308      * @private
309      */
310     handleChange_: function() {
311     },
312
313     /**
314      * Handle changes to the slider position made by the user when the slider is
315      * released. If a custom change handler does not suppress it, a default
316      * handler is invoked that updates the associated pref.
317      * @param {Event} event Change event.
318      * @private
319      */
320     handleRelease_: function(event) {
321       if (!this.customChangeHandler(event))
322         this.updatePrefFromState_();
323     },
324
325     /**
326      * Update the slider position when the associated pref changes.
327      * @param {Event} event Pref change event.
328      * @private
329      */
330     updateStateFromPref_: function(event) {
331       var value = event.value.value;
332       this.value = this.valueMap ? this.valueMap.indexOf(value) : value;
333     },
334
335     /**
336      * Map slider position to the range of values provided by the client,
337      * represented by |valueMap|.
338      * @param {number} position The slider position to map.
339      */
340     mapPositionToPref: function(position) {
341       return this.valueMap ? this.valueMap[position] : position;
342     },
343   };
344
345   /////////////////////////////////////////////////////////////////////////////
346   // PrefSelect class:
347
348   // Define a constructor that uses a select element as its underlying element.
349   var PrefSelect = cr.ui.define('select');
350
351   PrefSelect.prototype = {
352     // Set up the prototype chain
353     __proto__: PrefInputElement.prototype,
354
355     /**
356      * Update the associated pref when when the user selects an item.
357      * @private
358      */
359     updatePrefFromState_: function() {
360       var value = this.options[this.selectedIndex].value;
361       switch (this.dataType) {
362         case 'number':
363           Preferences.setIntegerPref(this.pref, value,
364                                      !this.dialogPref, this.metric);
365           break;
366         case 'double':
367           Preferences.setDoublePref(this.pref, value,
368                                     !this.dialogPref, this.metric);
369           break;
370         case 'boolean':
371           Preferences.setBooleanPref(this.pref, value == 'true',
372                                      !this.dialogPref, this.metric);
373           break;
374         case 'string':
375           Preferences.setStringPref(this.pref, value,
376                                     !this.dialogPref, this.metric);
377           break;
378         default:
379           console.error('Unknown data type for <select> UI element: ' +
380                         this.dataType);
381       }
382     },
383
384     /**
385      * Update the selected item when the associated pref changes.
386      * @param {Event} event Pref change event.
387      * @private
388      */
389     updateStateFromPref_: function(event) {
390       // Make sure the value is a string, because the value is stored as a
391       // string in the HTMLOptionElement.
392       value = String(event.value.value);
393
394       var found = false;
395       for (var i = 0; i < this.options.length; i++) {
396         if (this.options[i].value == value) {
397           this.selectedIndex = i;
398           found = true;
399         }
400       }
401
402       // Item not found, select first item.
403       if (!found)
404         this.selectedIndex = 0;
405
406       // The "onchange" event automatically fires when the user makes a manual
407       // change. It should never be fired for a programmatic change. However,
408       // these two lines were here already and it is hard to tell who may be
409       // relying on them.
410       if (this.onchange)
411         this.onchange(event);
412     },
413   };
414
415   /////////////////////////////////////////////////////////////////////////////
416   // PrefTextField class:
417
418   // Define a constructor that uses an input element as its underlying element.
419   var PrefTextField = cr.ui.define('input');
420
421   PrefTextField.prototype = {
422     // Set up the prototype chain
423     __proto__: PrefInputElement.prototype,
424
425     /**
426      * Initialization function for the cr.ui framework.
427      */
428     decorate: function() {
429       PrefInputElement.prototype.decorate.call(this);
430       var self = this;
431
432       // Listen for user events.
433       window.addEventListener('unload', function() {
434         if (document.activeElement == self)
435           self.blur();
436       });
437     },
438
439     /**
440      * Update the associated pref when when the user inputs text.
441      * @private
442      */
443     updatePrefFromState_: function(event) {
444       switch (this.dataType) {
445         case 'number':
446           Preferences.setIntegerPref(this.pref, this.value,
447                                      !this.dialogPref, this.metric);
448           break;
449         case 'double':
450           Preferences.setDoublePref(this.pref, this.value,
451                                     !this.dialogPref, this.metric);
452           break;
453         case 'url':
454           Preferences.setURLPref(this.pref, this.value,
455                                  !this.dialogPref, this.metric);
456           break;
457         default:
458           Preferences.setStringPref(this.pref, this.value,
459                                     !this.dialogPref, this.metric);
460           break;
461       }
462     },
463   };
464
465   /////////////////////////////////////////////////////////////////////////////
466   // PrefPortNumber class:
467
468   // Define a constructor that uses an input element as its underlying element.
469   var PrefPortNumber = cr.ui.define('input');
470
471   PrefPortNumber.prototype = {
472     // Set up the prototype chain
473     __proto__: PrefTextField.prototype,
474
475     /**
476      * Initialization function for the cr.ui framework.
477      */
478     decorate: function() {
479       var self = this;
480       self.type = 'text';
481       self.dataType = 'number';
482       PrefTextField.prototype.decorate.call(this);
483       self.oninput = function() {
484         // Note that using <input type="number"> is insufficient to restrict
485         // the input as it allows negative numbers and does not limit the
486         // number of charactes typed even if a range is set.  Furthermore,
487         // it sometimes produces strange repaint artifacts.
488         var filtered = self.value.replace(/[^0-9]/g, '');
489         if (filtered != self.value)
490           self.value = filtered;
491       };
492     }
493   };
494
495   /////////////////////////////////////////////////////////////////////////////
496   // PrefButton class:
497
498   // Define a constructor that uses a button element as its underlying element.
499   var PrefButton = cr.ui.define('button');
500
501   PrefButton.prototype = {
502     // Set up the prototype chain
503     __proto__: HTMLButtonElement.prototype,
504
505     /**
506      * Initialization function for the cr.ui framework.
507      */
508     decorate: function() {
509       var self = this;
510
511       // Listen for pref changes.
512       // This element behaves like a normal button and does not affect the
513       // underlying preference; it just becomes disabled when the preference is
514       // managed, and its value is false. This is useful for buttons that should
515       // be disabled when the underlying Boolean preference is set to false by a
516       // policy or extension.
517       Preferences.getInstance().addEventListener(this.pref, function(event) {
518         updateDisabledState_(self, 'notUserModifiable',
519                              event.value.disabled && !event.value.value);
520         self.controlledBy = event.value.controlledBy;
521       });
522     },
523
524     /**
525      * See |updateDisabledState_| above.
526      */
527     setDisabled: function(reason, disabled) {
528       updateDisabledState_(this, reason, disabled);
529     },
530   };
531
532   /**
533    * The name of the associated preference.
534    * @type {string}
535    */
536   cr.defineProperty(PrefButton, 'pref', cr.PropertyKind.ATTR);
537
538   /**
539    * Whether the associated preference is controlled by a source other than the
540    * user's setting (can be 'policy', 'extension', 'recommended' or unset).
541    * @type {string}
542    */
543   cr.defineProperty(PrefButton, 'controlledBy', cr.PropertyKind.ATTR);
544
545   // Export
546   return {
547     PrefCheckbox: PrefCheckbox,
548     PrefNumber: PrefNumber,
549     PrefRadio: PrefRadio,
550     PrefRange: PrefRange,
551     PrefSelect: PrefSelect,
552     PrefTextField: PrefTextField,
553     PrefPortNumber: PrefPortNumber,
554     PrefButton: PrefButton
555   };
556
557 });