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