Home | History | Annotate | Download | only in options
      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 });
    558