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 // TODO(kochi): Generalize the notification as a component and put it
      6 // in js/cr/ui/notification.js .
      7 
      8 cr.define('options', function() {
      9   const OptionsPage = options.OptionsPage;
     10   const LanguageList = options.LanguageList;
     11 
     12   // Some input methods like Chinese Pinyin have config pages.
     13   // This is the map of the input method names to their config page names.
     14   const INPUT_METHOD_ID_TO_CONFIG_PAGE_NAME = {
     15     'hangul': 'languageHangul',
     16     'mozc': 'languageMozc',
     17     'mozc-chewing': 'languageChewing',
     18     'mozc-dv': 'languageMozc',
     19     'mozc-jp': 'languageMozc',
     20     'pinyin': 'languagePinyin',
     21   };
     22 
     23   /////////////////////////////////////////////////////////////////////////////
     24   // LanguageOptions class:
     25 
     26   /**
     27    * Encapsulated handling of ChromeOS language options page.
     28    * @constructor
     29    */
     30   function LanguageOptions(model) {
     31     OptionsPage.call(this, 'languages', templateData.languagePageTabTitle,
     32                      'languagePage');
     33   }
     34 
     35   cr.addSingletonGetter(LanguageOptions);
     36 
     37   // Inherit LanguageOptions from OptionsPage.
     38   LanguageOptions.prototype = {
     39     __proto__: OptionsPage.prototype,
     40 
     41     /**
     42      * Initializes LanguageOptions page.
     43      * Calls base class implementation to starts preference initialization.
     44      */
     45     initializePage: function() {
     46       OptionsPage.prototype.initializePage.call(this);
     47 
     48       var languageOptionsList = $('language-options-list');
     49       LanguageList.decorate(languageOptionsList);
     50 
     51       languageOptionsList.addEventListener('change',
     52           this.handleLanguageOptionsListChange_.bind(this));
     53       languageOptionsList.addEventListener('save',
     54           this.handleLanguageOptionsListSave_.bind(this));
     55 
     56       this.addEventListener('visibleChange',
     57                             this.handleVisibleChange_.bind(this));
     58 
     59       if (cr.isChromeOS) {
     60         this.initializeInputMethodList_();
     61         this.initializeLanguageCodeToInputMethodIdsMap_();
     62       }
     63       Preferences.getInstance().addEventListener(this.spellCheckDictionaryPref,
     64           this.handleSpellCheckDictionaryPrefChange_.bind(this));
     65 
     66       // Set up add button.
     67       $('language-options-add-button').onclick = function(e) {
     68         // Add the language without showing the overlay if it's specified in
     69         // the URL hash (ex. lang_add=ja).  Used for automated testing.
     70         var match = document.location.hash.match(/\blang_add=([\w-]+)/);
     71         if (match) {
     72           var addLanguageCode = match[1];
     73           $('language-options-list').addLanguage(addLanguageCode);
     74         } else {
     75           OptionsPage.navigateToPage('addLanguage');
     76         }
     77       };
     78 
     79       if (cr.isChromeOS) {
     80         // Listen to user clicks on the add language list.
     81         var addLanguageList = $('add-language-overlay-language-list');
     82         addLanguageList.addEventListener('click',
     83             this.handleAddLanguageListClick_.bind(this));
     84       } else {
     85         // Listen to add language dialog ok button.
     86         var addLanguageOkButton = $('add-language-overlay-ok-button');
     87         addLanguageOkButton.addEventListener('click',
     88             this.handleAddLanguageOkButtonClick_.bind(this));
     89 
     90         // Show experimental features if enabled.
     91         if (templateData.experimentalSpellCheckFeatures == 'true') {
     92           $('auto-spell-correction-option').classList.remove('hidden');
     93         }
     94       }
     95     },
     96 
     97     // The preference is a CSV string that describes preload engines
     98     // (i.e. active input methods).
     99     preloadEnginesPref: 'settings.language.preload_engines',
    100     // The list of preload engines, like ['mozc', 'pinyin'].
    101     preloadEngines_: [],
    102     // The preference is a string that describes the spell check
    103     // dictionary language, like "en-US".
    104     spellCheckDictionaryPref: 'spellcheck.dictionary',
    105     spellCheckDictionary_: "",
    106     // The map of language code to input method IDs, like:
    107     // {'ja': ['mozc', 'mozc-jp'], 'zh-CN': ['pinyin'], ...}
    108     languageCodeToInputMethodIdsMap_: {},
    109 
    110     /**
    111      * Initializes the input method list.
    112      */
    113     initializeInputMethodList_: function() {
    114       var inputMethodList = $('language-options-input-method-list');
    115       var inputMethodListData = templateData.inputMethodList;
    116 
    117       // Add all input methods, but make all of them invisible here. We'll
    118       // change the visibility in handleLanguageOptionsListChange_() based
    119       // on the selected language. Note that we only have less than 100
    120       // input methods, so creating DOM nodes at once here should be ok.
    121       for (var i = 0; i < inputMethodListData.length; i++) {
    122         var inputMethod = inputMethodListData[i];
    123         var input = document.createElement('input');
    124         input.type = 'checkbox';
    125         input.inputMethodId = inputMethod.id;
    126         // Listen to user clicks.
    127         input.addEventListener('click',
    128                                this.handleCheckboxClick_.bind(this));
    129         var label = document.createElement('label');
    130         label.appendChild(input);
    131         // Adding a space between the checkbox and the text. This is a bit
    132         // dirty, but we rely on a space character for all other checkboxes.
    133         label.appendChild(document.createTextNode(
    134             ' ' + inputMethod.displayName));
    135         label.style.display = 'none';
    136         label.languageCodeSet = inputMethod.languageCodeSet;
    137         // Add the configure button if the config page is present for this
    138         // input method.
    139         if (inputMethod.id in INPUT_METHOD_ID_TO_CONFIG_PAGE_NAME) {
    140           var pageName = INPUT_METHOD_ID_TO_CONFIG_PAGE_NAME[inputMethod.id];
    141           var button = this.createConfigureInputMethodButton_(inputMethod.id,
    142                                                               pageName);
    143           label.appendChild(button);
    144         }
    145 
    146         inputMethodList.appendChild(label);
    147       }
    148       // Listen to pref change once the input method list is initialized.
    149       Preferences.getInstance().addEventListener(this.preloadEnginesPref,
    150           this.handlePreloadEnginesPrefChange_.bind(this));
    151     },
    152 
    153     /**
    154      * Creates a configure button for the given input method ID.
    155      * @param {string} inputMethodId Input method ID (ex. "pinyin").
    156      * @param {string} pageName Name of the config page (ex. "languagePinyin").
    157      * @private
    158      */
    159     createConfigureInputMethodButton_: function(inputMethodId, pageName) {
    160       var button = document.createElement('button');
    161       button.textContent = localStrings.getString('configure');
    162       button.onclick = function(e) {
    163         // Prevent the default action (i.e. changing the checked property
    164         // of the checkbox). The button click here should not be handled
    165         // as checkbox click.
    166         e.preventDefault();
    167         chrome.send('inputMethodOptionsOpen', [inputMethodId]);
    168         OptionsPage.navigateToPage(pageName);
    169       }
    170       return button;
    171     },
    172 
    173     /**
    174      * Handles OptionsPage's visible property change event.
    175      * @param {Event} e Property change event.
    176      * @private
    177      */
    178     handleVisibleChange_: function(e) {
    179       if (this.visible) {
    180         $('language-options-list').redraw();
    181         chrome.send('languageOptionsOpen');
    182       }
    183     },
    184 
    185     /**
    186      * Handles languageOptionsList's change event.
    187      * @param {Event} e Change event.
    188      * @private
    189      */
    190     handleLanguageOptionsListChange_: function(e) {
    191       var languageOptionsList = $('language-options-list');
    192       var languageCode = languageOptionsList.getSelectedLanguageCode();
    193       // Select the language if it's specified in the URL hash (ex. lang=ja).
    194       // Used for automated testing.
    195       var match = document.location.hash.match(/\blang=([\w-]+)/);
    196       if (match) {
    197         var specifiedLanguageCode = match[1];
    198         if (languageOptionsList.selectLanguageByCode(specifiedLanguageCode)) {
    199           languageCode = specifiedLanguageCode;
    200         }
    201       }
    202       this.updateSelectedLanguageName_(languageCode);
    203       if (cr.isWindows || cr.isChromeOS)
    204         this.updateUiLanguageButton_(languageCode);
    205       this.updateSpellCheckLanguageButton_(languageCode);
    206       if (cr.isChromeOS)
    207         this.updateInputMethodList_(languageCode);
    208       this.updateLanguageListInAddLanguageOverlay_();
    209     },
    210 
    211     /**
    212      * Handles languageOptionsList's save event.
    213      * @param {Event} e Save event.
    214      * @private
    215      */
    216     handleLanguageOptionsListSave_: function(e) {
    217       if (cr.isChromeOS) {
    218         // Sort the preload engines per the saved languages before save.
    219         this.preloadEngines_ = this.sortPreloadEngines_(this.preloadEngines_);
    220         this.savePreloadEnginesPref_();
    221       }
    222     },
    223 
    224     /**
    225      * Sorts preloadEngines_ by languageOptionsList's order.
    226      * @param {Array} preloadEngines List of preload engines.
    227      * @return {Array} Returns sorted preloadEngines.
    228      * @private
    229      */
    230     sortPreloadEngines_: function(preloadEngines) {
    231       // For instance, suppose we have two languages and associated input
    232       // methods:
    233       //
    234       // - Korean: hangul
    235       // - Chinese: pinyin
    236       //
    237       // The preloadEngines preference should look like "hangul,pinyin".
    238       // If the user reverse the order, the preference should be reorderd
    239       // to "pinyin,hangul".
    240       var languageOptionsList = $('language-options-list');
    241       var languageCodes = languageOptionsList.getLanguageCodes();
    242 
    243       // Convert the list into a dictonary for simpler lookup.
    244       var preloadEngineSet = {};
    245       for (var i = 0; i < preloadEngines.length; i++) {
    246         preloadEngineSet[preloadEngines[i]] = true;
    247       }
    248 
    249       // Create the new preload engine list per the language codes.
    250       var newPreloadEngines = [];
    251       for (var i = 0; i < languageCodes.length; i++) {
    252         var languageCode = languageCodes[i];
    253         var inputMethodIds = this.languageCodeToInputMethodIdsMap_[
    254             languageCode];
    255         // Check if we have active input methods associated with the language.
    256         for (var j = 0; j < inputMethodIds.length; j++) {
    257           var inputMethodId = inputMethodIds[j];
    258           if (inputMethodId in preloadEngineSet) {
    259             // If we have, add it to the new engine list.
    260             newPreloadEngines.push(inputMethodId);
    261             // And delete it from the set. This is necessary as one input
    262             // method can be associated with more than one language thus
    263             // we should avoid having duplicates in the new list.
    264             delete preloadEngineSet[inputMethodId];
    265           }
    266         }
    267       }
    268 
    269       return newPreloadEngines;
    270     },
    271 
    272     /**
    273      * Initializes the map of language code to input method IDs.
    274      * @private
    275      */
    276     initializeLanguageCodeToInputMethodIdsMap_: function() {
    277       var inputMethodList = templateData.inputMethodList;
    278       for (var i = 0; i < inputMethodList.length; i++) {
    279         var inputMethod = inputMethodList[i];
    280         for (var languageCode in inputMethod.languageCodeSet) {
    281           if (languageCode in this.languageCodeToInputMethodIdsMap_) {
    282             this.languageCodeToInputMethodIdsMap_[languageCode].push(
    283                 inputMethod.id);
    284           } else {
    285             this.languageCodeToInputMethodIdsMap_[languageCode] =
    286                 [inputMethod.id];
    287           }
    288         }
    289       }
    290     },
    291 
    292     /**
    293      * Updates the currently selected language name.
    294      * @param {string} languageCode Language code (ex. "fr").
    295      * @private
    296      */
    297     updateSelectedLanguageName_: function(languageCode) {
    298       var languageDisplayName = LanguageList.getDisplayNameFromLanguageCode(
    299           languageCode);
    300       var languageNativeDisplayName =
    301           LanguageList.getNativeDisplayNameFromLanguageCode(languageCode);
    302       // If the native name is different, add it.
    303       if (languageDisplayName != languageNativeDisplayName) {
    304         languageDisplayName += ' - ' + languageNativeDisplayName;
    305       }
    306       // Update the currently selected language name.
    307       $('language-options-language-name').textContent = languageDisplayName;
    308     },
    309 
    310     /**
    311      * Updates the UI language button.
    312      * @param {string} languageCode Language code (ex. "fr").
    313      * @private
    314      */
    315     updateUiLanguageButton_: function(languageCode) {
    316       var uiLanguageButton = $('language-options-ui-language-button');
    317       // Check if the language code matches the current UI language.
    318       if (languageCode == templateData.currentUiLanguageCode) {
    319         // If it matches, the button just says that the UI language is
    320         // currently in use.
    321         uiLanguageButton.textContent =
    322             localStrings.getString('is_displayed_in_this_language');
    323         // Make it look like a text label.
    324         uiLanguageButton.className = 'text-button';
    325         // Remove the event listner.
    326         uiLanguageButton.onclick = undefined;
    327       } else if (languageCode in templateData.uiLanguageCodeSet) {
    328         // If the language is supported as UI language, users can click on
    329         // the button to change the UI language.
    330         uiLanguageButton.textContent =
    331             localStrings.getString('display_in_this_language');
    332         uiLanguageButton.className = '';
    333         // Send the change request to Chrome.
    334         uiLanguageButton.onclick = function(e) {
    335           chrome.send('uiLanguageChange', [languageCode]);
    336         }
    337         if (cr.isChromeOS) {
    338           $('language-options-ui-restart-button').onclick = function(e) {
    339             chrome.send('uiLanguageRestart');
    340           }
    341         }
    342       } else {
    343         // If the language is not supported as UI language, the button
    344         // just says that Chromium OS cannot be displayed in this language.
    345         uiLanguageButton.textContent =
    346             localStrings.getString('cannot_be_displayed_in_this_language');
    347         uiLanguageButton.className = 'text-button';
    348         uiLanguageButton.onclick = undefined;
    349       }
    350       uiLanguageButton.style.display = 'block';
    351       $('language-options-ui-notification-bar').style.display = 'none';
    352     },
    353 
    354     /**
    355      * Updates the spell check language button.
    356      * @param {string} languageCode Language code (ex. "fr").
    357      * @private
    358      */
    359     updateSpellCheckLanguageButton_: function(languageCode) {
    360       var spellCheckLanguageButton = $(
    361           'language-options-spell-check-language-button');
    362       // Check if the language code matches the current spell check language.
    363       if (languageCode == this.spellCheckDictionary_) {
    364         // If it matches, the button just says that the spell check language is
    365         // currently in use.
    366         spellCheckLanguageButton.textContent =
    367             localStrings.getString('is_used_for_spell_checking');
    368         // Make it look like a text label.
    369         spellCheckLanguageButton.className = 'text-button';
    370         // Remove the event listner.
    371         spellCheckLanguageButton.onclick = undefined;
    372       } else if (languageCode in templateData.spellCheckLanguageCodeSet) {
    373         // If the language is supported as spell check language, users can
    374         // click on the button to change the spell check language.
    375         spellCheckLanguageButton.textContent =
    376             localStrings.getString('use_this_for_spell_checking');
    377         spellCheckLanguageButton.className = '';
    378         spellCheckLanguageButton.languageCode = languageCode;
    379         // Add an event listner to the click event.
    380         spellCheckLanguageButton.addEventListener('click',
    381             this.handleSpellCheckLanguageButtonClick_.bind(this));
    382       } else {
    383         // If the language is not supported as spell check language, the
    384         // button just says that this language cannot be used for spell
    385         // checking.
    386         spellCheckLanguageButton.textContent =
    387             localStrings.getString('cannot_be_used_for_spell_checking');
    388         spellCheckLanguageButton.className = 'text-button';
    389         spellCheckLanguageButton.onclick = undefined;
    390       }
    391       spellCheckLanguageButton.style.display = 'block';
    392       $('language-options-ui-notification-bar').style.display = 'none';
    393     },
    394 
    395     /**
    396      * Updates the input method list.
    397      * @param {string} languageCode Language code (ex. "fr").
    398      * @private
    399      */
    400     updateInputMethodList_: function(languageCode) {
    401       // Give one of the checkboxes or buttons focus, if it's specified in the
    402       // URL hash (ex. focus=mozc). Used for automated testing.
    403       var focusInputMethodId = -1;
    404       var match = document.location.hash.match(/\bfocus=([\w:-]+)\b/);
    405       if (match) {
    406         focusInputMethodId = match[1];
    407       }
    408       // Change the visibility of the input method list. Input methods that
    409       // matches |languageCode| will become visible.
    410       var inputMethodList = $('language-options-input-method-list');
    411       var labels = inputMethodList.querySelectorAll('label');
    412       for (var i = 0; i < labels.length; i++) {
    413         var label = labels[i];
    414         if (languageCode in label.languageCodeSet) {
    415           label.style.display = 'block';
    416           var input = label.childNodes[0];
    417           // Give it focus if the ID matches.
    418           if (input.inputMethodId == focusInputMethodId) {
    419             input.focus();
    420           }
    421         } else {
    422           label.style.display = 'none';
    423         }
    424       }
    425 
    426       if (focusInputMethodId == 'add') {
    427         $('language-options-add-button').focus();
    428       }
    429     },
    430 
    431     /**
    432      * Updates the language list in the add language overlay.
    433      * @param {string} languageCode Language code (ex. "fr").
    434      * @private
    435      */
    436     updateLanguageListInAddLanguageOverlay_: function(languageCode) {
    437       // Change the visibility of the language list in the add language
    438       // overlay. Languages that are already active will become invisible,
    439       // so that users don't add the same language twice.
    440       var languageOptionsList = $('language-options-list');
    441       var languageCodes = languageOptionsList.getLanguageCodes();
    442       var languageCodeSet = {};
    443       for (var i = 0; i < languageCodes.length; i++) {
    444         languageCodeSet[languageCodes[i]] = true;
    445       }
    446       var addLanguageList = $('add-language-overlay-language-list');
    447       var lis = addLanguageList.querySelectorAll('li');
    448       for (var i = 0; i < lis.length; i++) {
    449         // The first child button knows the language code.
    450         var button = lis[i].childNodes[0];
    451         if (button.languageCode in languageCodeSet) {
    452           lis[i].style.display = 'none';
    453         } else {
    454           lis[i].style.display = 'block';
    455         }
    456       }
    457     },
    458 
    459     /**
    460      * Handles preloadEnginesPref change.
    461      * @param {Event} e Change event.
    462      * @private
    463      */
    464     handlePreloadEnginesPrefChange_: function(e) {
    465       var value = e.value.value;
    466       this.preloadEngines_ = this.filterBadPreloadEngines_(value.split(','));
    467       this.updateCheckboxesFromPreloadEngines_();
    468       $('language-options-list').updateDeletable();
    469     },
    470 
    471     /**
    472      * Handles input method checkbox's click event.
    473      * @param {Event} e Click event.
    474      * @private
    475      */
    476     handleCheckboxClick_ : function(e) {
    477       var checkbox = e.target;
    478       if (this.preloadEngines_.length == 1 && !checkbox.checked) {
    479         // Don't allow disabling the last input method.
    480         this.showNotification_(
    481             localStrings.getString('please_add_another_input_method'),
    482             localStrings.getString('ok_button'));
    483         checkbox.checked = true;
    484         return;
    485       }
    486       if (checkbox.checked) {
    487         chrome.send('inputMethodEnable', [checkbox.inputMethodId]);
    488       } else {
    489         chrome.send('inputMethodDisable', [checkbox.inputMethodId]);
    490       }
    491       this.updatePreloadEnginesFromCheckboxes_();
    492       this.preloadEngines_ = this.sortPreloadEngines_(this.preloadEngines_);
    493       this.savePreloadEnginesPref_();
    494     },
    495 
    496     /**
    497      * Handles add language list's click event.
    498      * @param {Event} e Click event.
    499      */
    500     handleAddLanguageListClick_ : function(e) {
    501       var languageOptionsList = $('language-options-list');
    502       var languageCode = e.target.languageCode;
    503       // languageCode can be undefined, if click was made on some random
    504       // place in the overlay, rather than a button. Ignore it.
    505       if (!languageCode) {
    506         return;
    507       }
    508       languageOptionsList.addLanguage(languageCode);
    509       var inputMethodIds = this.languageCodeToInputMethodIdsMap_[languageCode];
    510       // Enable the first input method for the language added.
    511       if (inputMethodIds && inputMethodIds[0] &&
    512           // Don't add the input method it's already present. This can
    513           // happen if the same input method is shared among multiple
    514           // languages (ex. English US keyboard is used for English US and
    515           // Filipino).
    516           this.preloadEngines_.indexOf(inputMethodIds[0]) == -1) {
    517         this.preloadEngines_.push(inputMethodIds[0]);
    518         this.updateCheckboxesFromPreloadEngines_();
    519         this.savePreloadEnginesPref_();
    520       }
    521       OptionsPage.closeOverlay();
    522     },
    523 
    524     /**
    525      * Handles add language dialog ok button.
    526      */
    527     handleAddLanguageOkButtonClick_ : function() {
    528       var languagesSelect = $('add-language-overlay-language-list');
    529       var selectedIndex = languagesSelect.selectedIndex;
    530       if (selectedIndex >= 0) {
    531         var selection = languagesSelect.options[selectedIndex];
    532         $('language-options-list').addLanguage(String(selection.value));
    533         OptionsPage.closeOverlay();
    534       }
    535     },
    536 
    537     /**
    538      * Checks if languageCode is deletable or not.
    539      * @param {String} languageCode the languageCode to check for deletability.
    540      */
    541     languageIsDeletable: function(languageCode) {
    542       // Don't allow removing the language if it's as UI language.
    543       if (languageCode == templateData.currentUiLanguageCode)
    544         return false;
    545       return (!cr.isChromeOS ||
    546               this.canDeleteLanguage_(languageCode));
    547     },
    548 
    549     /**
    550      * Handles spellCheckDictionaryPref change.
    551      * @param {Event} e Change event.
    552      * @private
    553      */
    554     handleSpellCheckDictionaryPrefChange_: function(e) {
    555       var languageCode = e.value.value
    556       this.spellCheckDictionary_ = languageCode;
    557       var languageOptionsList = $('language-options-list');
    558       var selectedLanguageCode = languageOptionsList.getSelectedLanguageCode();
    559       this.updateSpellCheckLanguageButton_(selectedLanguageCode);
    560     },
    561 
    562     /**
    563      * Handles spellCheckLanguageButton click.
    564      * @param {Event} e Click event.
    565      * @private
    566      */
    567     handleSpellCheckLanguageButtonClick_: function(e) {
    568       var languageCode = e.target.languageCode;
    569       // Save the preference.
    570       Preferences.setStringPref(this.spellCheckDictionaryPref,
    571                                 languageCode);
    572       chrome.send('spellCheckLanguageChange', [languageCode]);
    573     },
    574 
    575     /**
    576      * Checks whether it's possible to remove the language specified by
    577      * languageCode and returns true if possible. This function returns false
    578      * if the removal causes the number of preload engines to be zero.
    579      *
    580      * @param {string} languageCode Language code (ex. "fr").
    581      * @return {boolean} Returns true on success.
    582      * @private
    583      */
    584     canDeleteLanguage_: function(languageCode) {
    585       // First create the set of engines to be removed from input methods
    586       // associated with the language code.
    587       var enginesToBeRemovedSet = {};
    588       var inputMethodIds = this.languageCodeToInputMethodIdsMap_[languageCode];
    589       for (var i = 0; i < inputMethodIds.length; i++) {
    590         enginesToBeRemovedSet[inputMethodIds[i]] = true;
    591       }
    592 
    593       // Then eliminate engines that are also used for other active languages.
    594       // For instance, if "xkb:us::eng" is used for both English and Filipino.
    595       var languageCodes = $('language-options-list').getLanguageCodes();
    596       for (var i = 0; i < languageCodes.length; i++) {
    597         // Skip the target language code.
    598         if (languageCodes[i] == languageCode) {
    599           continue;
    600         }
    601         // Check if input methods used in this language are included in
    602         // enginesToBeRemovedSet. If so, eliminate these from the set, so
    603         // we don't remove this time.
    604         var inputMethodIdsForAnotherLanguage =
    605             this.languageCodeToInputMethodIdsMap_[languageCodes[i]];
    606         for (var j = 0; j < inputMethodIdsForAnotherLanguage.length; j++) {
    607           var inputMethodId = inputMethodIdsForAnotherLanguage[j];
    608           if (inputMethodId in enginesToBeRemovedSet) {
    609             delete enginesToBeRemovedSet[inputMethodId];
    610           }
    611         }
    612       }
    613 
    614       // Update the preload engine list with the to-be-removed set.
    615       var newPreloadEngines = [];
    616       for (var i = 0; i < this.preloadEngines_.length; i++) {
    617         if (!(this.preloadEngines_[i] in enginesToBeRemovedSet)) {
    618           newPreloadEngines.push(this.preloadEngines_[i]);
    619         }
    620       }
    621       // Don't allow this operation if it causes the number of preload
    622       // engines to be zero.
    623       return (newPreloadEngines.length > 0);
    624     },
    625 
    626     /**
    627      * Saves the preload engines preference.
    628      * @private
    629      */
    630     savePreloadEnginesPref_: function() {
    631       Preferences.setStringPref(this.preloadEnginesPref,
    632                                 this.preloadEngines_.join(','));
    633     },
    634 
    635     /**
    636      * Updates the checkboxes in the input method list from the preload
    637      * engines preference.
    638      * @private
    639      */
    640     updateCheckboxesFromPreloadEngines_: function() {
    641       // Convert the list into a dictonary for simpler lookup.
    642       var dictionary = {};
    643       for (var i = 0; i < this.preloadEngines_.length; i++) {
    644         dictionary[this.preloadEngines_[i]] = true;
    645       }
    646 
    647       var inputMethodList = $('language-options-input-method-list');
    648       var checkboxes = inputMethodList.querySelectorAll('input');
    649       for (var i = 0; i < checkboxes.length; i++) {
    650         checkboxes[i].checked = (checkboxes[i].inputMethodId in dictionary);
    651       }
    652     },
    653 
    654     /**
    655      * Updates the preload engines preference from the checkboxes in the
    656      * input method list.
    657      * @private
    658      */
    659     updatePreloadEnginesFromCheckboxes_: function() {
    660       this.preloadEngines_ = [];
    661       var inputMethodList = $('language-options-input-method-list');
    662       var checkboxes = inputMethodList.querySelectorAll('input');
    663       for (var i = 0; i < checkboxes.length; i++) {
    664         if (checkboxes[i].checked) {
    665           this.preloadEngines_.push(checkboxes[i].inputMethodId);
    666         }
    667       }
    668       var languageOptionsList = $('language-options-list');
    669       languageOptionsList.updateDeletable();
    670     },
    671 
    672     /**
    673      * Filters bad preload engines in case bad preload engines are
    674      * stored in the preference. Removes duplicates as well.
    675      * @param {Array} preloadEngines List of preload engines.
    676      * @private
    677      */
    678     filterBadPreloadEngines_: function(preloadEngines) {
    679       // Convert the list into a dictonary for simpler lookup.
    680       var dictionary = {};
    681       for (var i = 0; i < templateData.inputMethodList.length; i++) {
    682         dictionary[templateData.inputMethodList[i].id] = true;
    683       }
    684 
    685       var filteredPreloadEngines = [];
    686       var seen = {};
    687       for (var i = 0; i < preloadEngines.length; i++) {
    688         // Check if the preload engine is present in the
    689         // dictionary, and not duplicate. Otherwise, skip it.
    690         if (preloadEngines[i] in dictionary && !(preloadEngines[i] in seen)) {
    691           filteredPreloadEngines.push(preloadEngines[i]);
    692           seen[preloadEngines[i]] = true;
    693         }
    694       }
    695       return filteredPreloadEngines;
    696     },
    697 
    698     // TODO(kochi): This is an adapted copy from new_new_tab.js.
    699     // If this will go as final UI, refactor this to share the component with
    700     // new new tab page.
    701     /**
    702      * Shows notification
    703      * @private
    704      */
    705     notificationTimeout_: null,
    706     showNotification_ : function(text, actionText, opt_delay) {
    707       var notificationElement = $('notification');
    708       var actionLink = notificationElement.querySelector('.link-color');
    709       var delay = opt_delay || 10000;
    710 
    711       function show() {
    712         window.clearTimeout(this.notificationTimeout_);
    713         notificationElement.classList.add('show');
    714         document.body.classList.add('notification-shown');
    715       }
    716 
    717       function hide() {
    718         window.clearTimeout(this.notificationTimeout_);
    719         notificationElement.classList.remove('show');
    720         document.body.classList.remove('notification-shown');
    721         // Prevent tabbing to the hidden link.
    722         actionLink.tabIndex = -1;
    723         // Setting tabIndex to -1 only prevents future tabbing to it. If,
    724         // however, the user switches window or a tab and then moves back to
    725         // this tab the element may gain focus. We therefore make sure that we
    726         // blur the element so that the element focus is not restored when
    727         // coming back to this window.
    728         actionLink.blur();
    729       }
    730 
    731       function delayedHide() {
    732         this.notificationTimeout_ = window.setTimeout(hide, delay);
    733       }
    734 
    735       notificationElement.firstElementChild.textContent = text;
    736       actionLink.textContent = actionText;
    737 
    738       actionLink.onclick = hide;
    739       actionLink.onkeydown = function(e) {
    740         if (e.keyIdentifier == 'Enter') {
    741           hide();
    742         }
    743       };
    744       notificationElement.onmouseover = show;
    745       notificationElement.onmouseout = delayedHide;
    746       actionLink.onfocus = show;
    747       actionLink.onblur = delayedHide;
    748       // Enable tabbing to the link now that it is shown.
    749       actionLink.tabIndex = 0;
    750 
    751       show();
    752       delayedHide();
    753     }
    754   };
    755 
    756   /**
    757    * Chrome callback for when the UI language preference is saved.
    758    */
    759   LanguageOptions.uiLanguageSaved = function() {
    760     $('language-options-ui-language-button').style.display = 'none';
    761     $('language-options-ui-notification-bar').style.display = 'block';
    762   };
    763 
    764   // Export
    765   return {
    766     LanguageOptions: LanguageOptions
    767   };
    768 });
    769