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