Upstream version 10.39.225.0
[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   /**
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 });