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   /**
     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 });
    596