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
8cr.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