Home | History | Annotate | Download | only in options
      1 // Copyright (c) 2011 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   const ArrayDataModel = cr.ui.ArrayDataModel;
      7   const DeletableItem = options.DeletableItem;
      8   const DeletableItemList = options.DeletableItemList;
      9   const List = cr.ui.List;
     10   const ListItem = cr.ui.ListItem;
     11   const ListSingleSelectionModel = cr.ui.ListSingleSelectionModel;
     12 
     13   /**
     14    * Creates a new Language list item.
     15    * @param {String} languageCode the languageCode.
     16    * @constructor
     17    * @extends {DeletableItem.ListItem}
     18    */
     19   function LanguageListItem(languageCode) {
     20     var el = cr.doc.createElement('li');
     21     el.__proto__ = LanguageListItem.prototype;
     22     el.languageCode_ = languageCode;
     23     el.decorate();
     24     return el;
     25   };
     26 
     27   LanguageListItem.prototype = {
     28     __proto__: DeletableItem.prototype,
     29 
     30     /**
     31      * The language code of this language.
     32      * @type {String}
     33      * @private
     34      */
     35     languageCode_: null,
     36 
     37     /** @inheritDoc */
     38     decorate: function() {
     39       DeletableItem.prototype.decorate.call(this);
     40 
     41       var languageCode = this.languageCode_;
     42       var languageOptions = options.LanguageOptions.getInstance();
     43       this.deletable = languageOptions.languageIsDeletable(languageCode);
     44       this.languageCode = languageCode;
     45       this.contentElement.textContent =
     46           LanguageList.getDisplayNameFromLanguageCode(languageCode);
     47       this.title =
     48           LanguageList.getNativeDisplayNameFromLanguageCode(languageCode);
     49       this.draggable = true;
     50     },
     51   };
     52 
     53   /**
     54    * Creates a new language list.
     55    * @param {Object=} opt_propertyBag Optional properties.
     56    * @constructor
     57    * @extends {cr.ui.List}
     58    */
     59   var LanguageList = cr.ui.define('list');
     60 
     61   /**
     62    * Gets display name from the given language code.
     63    * @param {string} languageCode Language code (ex. "fr").
     64    */
     65   LanguageList.getDisplayNameFromLanguageCode = function(languageCode) {
     66     // Build the language code to display name dictionary at first time.
     67     if (!this.languageCodeToDisplayName_) {
     68       this.languageCodeToDisplayName_ = {};
     69       var languageList = templateData.languageList;
     70       for (var i = 0; i < languageList.length; i++) {
     71         var language = languageList[i];
     72         this.languageCodeToDisplayName_[language.code] = language.displayName;
     73       }
     74     }
     75 
     76     return this.languageCodeToDisplayName_[languageCode];
     77   }
     78 
     79   /**
     80    * Gets native display name from the given language code.
     81    * @param {string} languageCode Language code (ex. "fr").
     82    */
     83   LanguageList.getNativeDisplayNameFromLanguageCode = function(languageCode) {
     84     // Build the language code to display name dictionary at first time.
     85     if (!this.languageCodeToNativeDisplayName_) {
     86       this.languageCodeToNativeDisplayName_ = {};
     87       var languageList = templateData.languageList;
     88       for (var i = 0; i < languageList.length; i++) {
     89         var language = languageList[i];
     90         this.languageCodeToNativeDisplayName_[language.code] =
     91             language.nativeDisplayName;
     92       }
     93     }
     94 
     95     return this.languageCodeToNativeDisplayName_[languageCode];
     96   }
     97 
     98   /**
     99    * Returns true if the given language code is valid.
    100    * @param {string} languageCode Language code (ex. "fr").
    101    */
    102   LanguageList.isValidLanguageCode = function(languageCode) {
    103     // Having the display name for the language code means that the
    104     // language code is valid.
    105     if (LanguageList.getDisplayNameFromLanguageCode(languageCode)) {
    106       return true;
    107     }
    108     return false;
    109   }
    110 
    111   LanguageList.prototype = {
    112     __proto__: DeletableItemList.prototype,
    113 
    114     // The list item being dragged.
    115     draggedItem: null,
    116     // The drop position information: "below" or "above".
    117     dropPos: null,
    118     // The preference is a CSV string that describes preferred languages
    119     // in Chrome OS. The language list is used for showing the language
    120     // list in "Language and Input" options page.
    121     preferredLanguagesPref: 'settings.language.preferred_languages',
    122     // The preference is a CSV string that describes accept languages used
    123     // for content negotiation. To be more precise, the list will be used
    124     // in "Accept-Language" header in HTTP requests.
    125     acceptLanguagesPref: 'intl.accept_languages',
    126 
    127     /** @inheritDoc */
    128     decorate: function() {
    129       DeletableItemList.prototype.decorate.call(this);
    130       this.selectionModel = new ListSingleSelectionModel;
    131 
    132       // HACK(arv): http://crbug.com/40902
    133       window.addEventListener('resize', this.redraw.bind(this));
    134 
    135       // Listen to pref change.
    136       if (cr.isChromeOS) {
    137         Preferences.getInstance().addEventListener(this.preferredLanguagesPref,
    138             this.handlePreferredLanguagesPrefChange_.bind(this));
    139       } else {
    140         Preferences.getInstance().addEventListener(this.acceptLanguagesPref,
    141             this.handleAcceptLanguagesPrefChange_.bind(this));
    142       }
    143 
    144       // Listen to drag and drop events.
    145       this.addEventListener('dragstart', this.handleDragStart_.bind(this));
    146       this.addEventListener('dragenter', this.handleDragEnter_.bind(this));
    147       this.addEventListener('dragover', this.handleDragOver_.bind(this));
    148       this.addEventListener('drop', this.handleDrop_.bind(this));
    149     },
    150 
    151     createItem: function(languageCode) {
    152       return new LanguageListItem(languageCode);
    153     },
    154 
    155     /*
    156      * For each item, determines whether it's deletable.
    157      */
    158     updateDeletable: function() {
    159       for (var i = 0; i < this.items.length; ++i) {
    160         var item = this.getListItemByIndex(i);
    161         var languageCode = item.languageCode;
    162         var languageOptions = options.LanguageOptions.getInstance();
    163         item.deletable = languageOptions.languageIsDeletable(languageCode);
    164       }
    165     },
    166 
    167     /*
    168      * Adds a language to the language list.
    169      * @param {string} languageCode language code (ex. "fr").
    170      */
    171     addLanguage: function(languageCode) {
    172       // It shouldn't happen but ignore the language code if it's
    173       // null/undefined, or already present.
    174       if (!languageCode || this.dataModel.indexOf(languageCode) >= 0) {
    175         return;
    176       }
    177       this.dataModel.push(languageCode);
    178       // Select the last item, which is the language added.
    179       this.selectionModel.selectedIndex = this.dataModel.length - 1;
    180 
    181       this.savePreference_();
    182     },
    183 
    184     /*
    185      * Gets the language codes of the currently listed languages.
    186      */
    187     getLanguageCodes: function() {
    188       return this.dataModel.slice();
    189     },
    190 
    191     /*
    192      * Gets the language code of the selected language.
    193      */
    194     getSelectedLanguageCode: function() {
    195       return this.selectedItem;
    196     },
    197 
    198     /*
    199      * Selects the language by the given language code.
    200      * @returns {boolean} True if the operation is successful.
    201      */
    202     selectLanguageByCode: function(languageCode) {
    203       var index = this.dataModel.indexOf(languageCode);
    204       if (index >= 0) {
    205         this.selectionModel.selectedIndex = index;
    206         return true;
    207       }
    208       return false;
    209     },
    210 
    211     /** @inheritDoc */
    212     deleteItemAtIndex: function(index) {
    213       if (index >= 0) {
    214         this.dataModel.splice(index, 1);
    215         // Once the selected item is removed, there will be no selected item.
    216         // Select the item pointed by the lead index.
    217         index = this.selectionModel.leadIndex;
    218         this.savePreference_();
    219       }
    220       return index;
    221     },
    222 
    223     /*
    224      * Computes the target item of drop event.
    225      * @param {Event} e The drop or dragover event.
    226      * @private
    227      */
    228     getTargetFromDropEvent_ : function(e) {
    229       var target = e.target;
    230       // e.target may be an inner element of the list item
    231       while (target != null && !(target instanceof ListItem)) {
    232         target = target.parentNode;
    233       }
    234       return target;
    235     },
    236 
    237     /*
    238      * Handles the dragstart event.
    239      * @param {Event} e The dragstart event.
    240      * @private
    241      */
    242     handleDragStart_: function(e) {
    243       var target = e.target;
    244       // ListItem should be the only draggable element type in the page,
    245       // but just in case.
    246       if (target instanceof ListItem) {
    247         this.draggedItem = target;
    248         e.dataTransfer.effectAllowed = 'move';
    249         // We need to put some kind of data in the drag or it will be
    250         // ignored.  Use the display name in case the user drags to a text
    251         // field or the desktop.
    252         e.dataTransfer.setData('text/plain', target.title);
    253       }
    254     },
    255 
    256     /*
    257      * Handles the dragenter event.
    258      * @param {Event} e The dragenter event.
    259      * @private
    260      */
    261     handleDragEnter_: function(e) {
    262       e.preventDefault();
    263     },
    264 
    265     /*
    266      * Handles the dragover event.
    267      * @param {Event} e The dragover event.
    268      * @private
    269      */
    270     handleDragOver_: function(e) {
    271       var dropTarget = this.getTargetFromDropEvent_(e);
    272       // Determines whether the drop target is to accept the drop.
    273       // The drop is only successful on another ListItem.
    274       if (!(dropTarget instanceof ListItem) ||
    275           dropTarget == this.draggedItem) {
    276         return;
    277       }
    278       // Compute the drop postion. Should we move the dragged item to
    279       // below or above the drop target?
    280       var rect = dropTarget.getBoundingClientRect();
    281       var dy = e.clientY - rect.top;
    282       var yRatio = dy / rect.height;
    283       var dropPos = yRatio <= .5 ? 'above' : 'below';
    284       this.dropPos = dropPos;
    285       e.preventDefault();
    286       // TODO(satorux): Show the drop marker just like the bookmark manager.
    287     },
    288 
    289     /*
    290      * Handles the drop event.
    291      * @param {Event} e The drop event.
    292      * @private
    293      */
    294     handleDrop_: function(e) {
    295       var dropTarget = this.getTargetFromDropEvent_(e);
    296 
    297       // Delete the language from the original position.
    298       var languageCode = this.draggedItem.languageCode;
    299       var originalIndex = this.dataModel.indexOf(languageCode);
    300       this.dataModel.splice(originalIndex, 1);
    301       // Insert the language to the new position.
    302       var newIndex = this.dataModel.indexOf(dropTarget.languageCode);
    303       if (this.dropPos == 'below')
    304         newIndex += 1;
    305       this.dataModel.splice(newIndex, 0, languageCode);
    306       // The cursor should move to the moved item.
    307       this.selectionModel.selectedIndex = newIndex;
    308       // Save the preference.
    309       this.savePreference_();
    310     },
    311 
    312     /**
    313      * Handles preferred languages pref change.
    314      * @param {Event} e The change event object.
    315      * @private
    316      */
    317     handlePreferredLanguagesPrefChange_: function(e) {
    318       var languageCodesInCsv = e.value.value;
    319       var languageCodes = this.filterBadLanguageCodes_(
    320           languageCodesInCsv.split(','));
    321       this.load_(languageCodes);
    322     },
    323 
    324     /**
    325      * Handles accept languages pref change.
    326      * @param {Event} e The change event object.
    327      * @private
    328      */
    329     handleAcceptLanguagesPrefChange_: function(e) {
    330       var languageCodesInCsv = e.value.value;
    331       var languageCodes = this.filterBadLanguageCodes_(
    332           languageCodesInCsv.split(','));
    333       this.load_(languageCodes);
    334     },
    335 
    336     /**
    337      * Loads given language list.
    338      * @param {Array} languageCodes List of language codes.
    339      * @private
    340      */
    341     load_: function(languageCodes) {
    342       // Preserve the original selected index. See comments below.
    343       var originalSelectedIndex = (this.selectionModel ?
    344                                    this.selectionModel.selectedIndex : -1);
    345       this.dataModel = new ArrayDataModel(languageCodes);
    346       if (originalSelectedIndex >= 0 &&
    347           originalSelectedIndex < this.dataModel.length) {
    348         // Restore the original selected index if the selected index is
    349         // valid after the data model is loaded. This is neeeded to keep
    350         // the selected language after the languge is added or removed.
    351         this.selectionModel.selectedIndex = originalSelectedIndex;
    352         // The lead index should be updated too.
    353         this.selectionModel.leadIndex = originalSelectedIndex;
    354       } else if (this.dataModel.length > 0){
    355         // Otherwise, select the first item if it's not empty.
    356         // Note that ListSingleSelectionModel won't select an item
    357         // automatically, hence we manually select the first item here.
    358         this.selectionModel.selectedIndex = 0;
    359       }
    360     },
    361 
    362     /**
    363      * Saves the preference.
    364      */
    365     savePreference_: function() {
    366       // Encode the language codes into a CSV string.
    367       if (cr.isChromeOS)
    368         Preferences.setStringPref(this.preferredLanguagesPref,
    369                                   this.dataModel.slice().join(','));
    370       // Save the same language list as accept languages preference as
    371       // well, but we need to expand the language list, to make it more
    372       // acceptable. For instance, some web sites don't understand 'en-US'
    373       // but 'en'. See crosbug.com/9884.
    374       var acceptLanguages = this.expandLanguageCodes(this.dataModel.slice());
    375       Preferences.setStringPref(this.acceptLanguagesPref,
    376                                 acceptLanguages.join(','));
    377       cr.dispatchSimpleEvent(this, 'save');
    378     },
    379 
    380     /**
    381      * Expands language codes to make these more suitable for Accept-Language.
    382      * Example: ['en-US', 'ja', 'en-CA'] => ['en-US', 'en', 'ja', 'en-CA'].
    383      * 'en' won't appear twice as this function eliminates duplicates.
    384      * @param {Array} languageCodes List of language codes.
    385      * @private
    386      */
    387     expandLanguageCodes: function(languageCodes) {
    388       var expandedLanguageCodes = [];
    389       var seen = {};  // Used to eliminiate duplicates.
    390       for (var i = 0; i < languageCodes.length; i++) {
    391         var languageCode = languageCodes[i];
    392         if (!(languageCode in seen)) {
    393           expandedLanguageCodes.push(languageCode);
    394           seen[languageCode] = true;
    395         }
    396         var parts = languageCode.split('-');
    397         if (!(parts[0] in seen)) {
    398           expandedLanguageCodes.push(parts[0]);
    399           seen[parts[0]] = true;
    400         }
    401       }
    402       return expandedLanguageCodes;
    403     },
    404 
    405     /**
    406      * Filters bad language codes in case bad language codes are
    407      * stored in the preference. Removes duplicates as well.
    408      * @param {Array} languageCodes List of language codes.
    409      * @private
    410      */
    411     filterBadLanguageCodes_: function(languageCodes) {
    412       var filteredLanguageCodes = [];
    413       var seen = {};
    414       for (var i = 0; i < languageCodes.length; i++) {
    415         // Check if the the language code is valid, and not
    416         // duplicate. Otherwise, skip it.
    417         if (LanguageList.isValidLanguageCode(languageCodes[i]) &&
    418             !(languageCodes[i] in seen)) {
    419           filteredLanguageCodes.push(languageCodes[i]);
    420           seen[languageCodes[i]] = true;
    421         }
    422       }
    423       return filteredLanguageCodes;
    424     },
    425   };
    426 
    427   return {
    428     LanguageList: LanguageList,
    429     LanguageListItem: LanguageListItem
    430   };
    431 });
    432