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