// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // TODO(kochi): Generalize the notification as a component and put it // in js/cr/ui/notification.js . cr.define('options', function() { const OptionsPage = options.OptionsPage; const LanguageList = options.LanguageList; // Some input methods like Chinese Pinyin have config pages. // This is the map of the input method names to their config page names. const INPUT_METHOD_ID_TO_CONFIG_PAGE_NAME = { 'hangul': 'languageHangul', 'mozc': 'languageMozc', 'mozc-chewing': 'languageChewing', 'mozc-dv': 'languageMozc', 'mozc-jp': 'languageMozc', 'pinyin': 'languagePinyin', }; ///////////////////////////////////////////////////////////////////////////// // LanguageOptions class: /** * Encapsulated handling of ChromeOS language options page. * @constructor */ function LanguageOptions(model) { OptionsPage.call(this, 'languages', templateData.languagePageTabTitle, 'languagePage'); } cr.addSingletonGetter(LanguageOptions); // Inherit LanguageOptions from OptionsPage. LanguageOptions.prototype = { __proto__: OptionsPage.prototype, /** * Initializes LanguageOptions page. * Calls base class implementation to starts preference initialization. */ initializePage: function() { OptionsPage.prototype.initializePage.call(this); var languageOptionsList = $('language-options-list'); LanguageList.decorate(languageOptionsList); languageOptionsList.addEventListener('change', this.handleLanguageOptionsListChange_.bind(this)); languageOptionsList.addEventListener('save', this.handleLanguageOptionsListSave_.bind(this)); this.addEventListener('visibleChange', this.handleVisibleChange_.bind(this)); if (cr.isChromeOS) { this.initializeInputMethodList_(); this.initializeLanguageCodeToInputMethodIdsMap_(); } Preferences.getInstance().addEventListener(this.spellCheckDictionaryPref, this.handleSpellCheckDictionaryPrefChange_.bind(this)); // Set up add button. $('language-options-add-button').onclick = function(e) { // Add the language without showing the overlay if it's specified in // the URL hash (ex. lang_add=ja). Used for automated testing. var match = document.location.hash.match(/\blang_add=([\w-]+)/); if (match) { var addLanguageCode = match[1]; $('language-options-list').addLanguage(addLanguageCode); } else { OptionsPage.navigateToPage('addLanguage'); } }; if (cr.isChromeOS) { // Listen to user clicks on the add language list. var addLanguageList = $('add-language-overlay-language-list'); addLanguageList.addEventListener('click', this.handleAddLanguageListClick_.bind(this)); } else { // Listen to add language dialog ok button. var addLanguageOkButton = $('add-language-overlay-ok-button'); addLanguageOkButton.addEventListener('click', this.handleAddLanguageOkButtonClick_.bind(this)); // Show experimental features if enabled. if (templateData.experimentalSpellCheckFeatures == 'true') { $('auto-spell-correction-option').classList.remove('hidden'); } } }, // The preference is a CSV string that describes preload engines // (i.e. active input methods). preloadEnginesPref: 'settings.language.preload_engines', // The list of preload engines, like ['mozc', 'pinyin']. preloadEngines_: [], // The preference is a string that describes the spell check // dictionary language, like "en-US". spellCheckDictionaryPref: 'spellcheck.dictionary', spellCheckDictionary_: "", // The map of language code to input method IDs, like: // {'ja': ['mozc', 'mozc-jp'], 'zh-CN': ['pinyin'], ...} languageCodeToInputMethodIdsMap_: {}, /** * Initializes the input method list. */ initializeInputMethodList_: function() { var inputMethodList = $('language-options-input-method-list'); var inputMethodListData = templateData.inputMethodList; // Add all input methods, but make all of them invisible here. We'll // change the visibility in handleLanguageOptionsListChange_() based // on the selected language. Note that we only have less than 100 // input methods, so creating DOM nodes at once here should be ok. for (var i = 0; i < inputMethodListData.length; i++) { var inputMethod = inputMethodListData[i]; var input = document.createElement('input'); input.type = 'checkbox'; input.inputMethodId = inputMethod.id; // Listen to user clicks. input.addEventListener('click', this.handleCheckboxClick_.bind(this)); var label = document.createElement('label'); label.appendChild(input); // Adding a space between the checkbox and the text. This is a bit // dirty, but we rely on a space character for all other checkboxes. label.appendChild(document.createTextNode( ' ' + inputMethod.displayName)); label.style.display = 'none'; label.languageCodeSet = inputMethod.languageCodeSet; // Add the configure button if the config page is present for this // input method. if (inputMethod.id in INPUT_METHOD_ID_TO_CONFIG_PAGE_NAME) { var pageName = INPUT_METHOD_ID_TO_CONFIG_PAGE_NAME[inputMethod.id]; var button = this.createConfigureInputMethodButton_(inputMethod.id, pageName); label.appendChild(button); } inputMethodList.appendChild(label); } // Listen to pref change once the input method list is initialized. Preferences.getInstance().addEventListener(this.preloadEnginesPref, this.handlePreloadEnginesPrefChange_.bind(this)); }, /** * Creates a configure button for the given input method ID. * @param {string} inputMethodId Input method ID (ex. "pinyin"). * @param {string} pageName Name of the config page (ex. "languagePinyin"). * @private */ createConfigureInputMethodButton_: function(inputMethodId, pageName) { var button = document.createElement('button'); button.textContent = localStrings.getString('configure'); button.onclick = function(e) { // Prevent the default action (i.e. changing the checked property // of the checkbox). The button click here should not be handled // as checkbox click. e.preventDefault(); chrome.send('inputMethodOptionsOpen', [inputMethodId]); OptionsPage.navigateToPage(pageName); } return button; }, /** * Handles OptionsPage's visible property change event. * @param {Event} e Property change event. * @private */ handleVisibleChange_: function(e) { if (this.visible) { $('language-options-list').redraw(); chrome.send('languageOptionsOpen'); } }, /** * Handles languageOptionsList's change event. * @param {Event} e Change event. * @private */ handleLanguageOptionsListChange_: function(e) { var languageOptionsList = $('language-options-list'); var languageCode = languageOptionsList.getSelectedLanguageCode(); // Select the language if it's specified in the URL hash (ex. lang=ja). // Used for automated testing. var match = document.location.hash.match(/\blang=([\w-]+)/); if (match) { var specifiedLanguageCode = match[1]; if (languageOptionsList.selectLanguageByCode(specifiedLanguageCode)) { languageCode = specifiedLanguageCode; } } this.updateSelectedLanguageName_(languageCode); if (cr.isWindows || cr.isChromeOS) this.updateUiLanguageButton_(languageCode); this.updateSpellCheckLanguageButton_(languageCode); if (cr.isChromeOS) this.updateInputMethodList_(languageCode); this.updateLanguageListInAddLanguageOverlay_(); }, /** * Handles languageOptionsList's save event. * @param {Event} e Save event. * @private */ handleLanguageOptionsListSave_: function(e) { if (cr.isChromeOS) { // Sort the preload engines per the saved languages before save. this.preloadEngines_ = this.sortPreloadEngines_(this.preloadEngines_); this.savePreloadEnginesPref_(); } }, /** * Sorts preloadEngines_ by languageOptionsList's order. * @param {Array} preloadEngines List of preload engines. * @return {Array} Returns sorted preloadEngines. * @private */ sortPreloadEngines_: function(preloadEngines) { // For instance, suppose we have two languages and associated input // methods: // // - Korean: hangul // - Chinese: pinyin // // The preloadEngines preference should look like "hangul,pinyin". // If the user reverse the order, the preference should be reorderd // to "pinyin,hangul". var languageOptionsList = $('language-options-list'); var languageCodes = languageOptionsList.getLanguageCodes(); // Convert the list into a dictonary for simpler lookup. var preloadEngineSet = {}; for (var i = 0; i < preloadEngines.length; i++) { preloadEngineSet[preloadEngines[i]] = true; } // Create the new preload engine list per the language codes. var newPreloadEngines = []; for (var i = 0; i < languageCodes.length; i++) { var languageCode = languageCodes[i]; var inputMethodIds = this.languageCodeToInputMethodIdsMap_[ languageCode]; // Check if we have active input methods associated with the language. for (var j = 0; j < inputMethodIds.length; j++) { var inputMethodId = inputMethodIds[j]; if (inputMethodId in preloadEngineSet) { // If we have, add it to the new engine list. newPreloadEngines.push(inputMethodId); // And delete it from the set. This is necessary as one input // method can be associated with more than one language thus // we should avoid having duplicates in the new list. delete preloadEngineSet[inputMethodId]; } } } return newPreloadEngines; }, /** * Initializes the map of language code to input method IDs. * @private */ initializeLanguageCodeToInputMethodIdsMap_: function() { var inputMethodList = templateData.inputMethodList; for (var i = 0; i < inputMethodList.length; i++) { var inputMethod = inputMethodList[i]; for (var languageCode in inputMethod.languageCodeSet) { if (languageCode in this.languageCodeToInputMethodIdsMap_) { this.languageCodeToInputMethodIdsMap_[languageCode].push( inputMethod.id); } else { this.languageCodeToInputMethodIdsMap_[languageCode] = [inputMethod.id]; } } } }, /** * Updates the currently selected language name. * @param {string} languageCode Language code (ex. "fr"). * @private */ updateSelectedLanguageName_: function(languageCode) { var languageDisplayName = LanguageList.getDisplayNameFromLanguageCode( languageCode); var languageNativeDisplayName = LanguageList.getNativeDisplayNameFromLanguageCode(languageCode); // If the native name is different, add it. if (languageDisplayName != languageNativeDisplayName) { languageDisplayName += ' - ' + languageNativeDisplayName; } // Update the currently selected language name. $('language-options-language-name').textContent = languageDisplayName; }, /** * Updates the UI language button. * @param {string} languageCode Language code (ex. "fr"). * @private */ updateUiLanguageButton_: function(languageCode) { var uiLanguageButton = $('language-options-ui-language-button'); // Check if the language code matches the current UI language. if (languageCode == templateData.currentUiLanguageCode) { // If it matches, the button just says that the UI language is // currently in use. uiLanguageButton.textContent = localStrings.getString('is_displayed_in_this_language'); // Make it look like a text label. uiLanguageButton.className = 'text-button'; // Remove the event listner. uiLanguageButton.onclick = undefined; } else if (languageCode in templateData.uiLanguageCodeSet) { // If the language is supported as UI language, users can click on // the button to change the UI language. uiLanguageButton.textContent = localStrings.getString('display_in_this_language'); uiLanguageButton.className = ''; // Send the change request to Chrome. uiLanguageButton.onclick = function(e) { chrome.send('uiLanguageChange', [languageCode]); } if (cr.isChromeOS) { $('language-options-ui-restart-button').onclick = function(e) { chrome.send('uiLanguageRestart'); } } } else { // If the language is not supported as UI language, the button // just says that Chromium OS cannot be displayed in this language. uiLanguageButton.textContent = localStrings.getString('cannot_be_displayed_in_this_language'); uiLanguageButton.className = 'text-button'; uiLanguageButton.onclick = undefined; } uiLanguageButton.style.display = 'block'; $('language-options-ui-notification-bar').style.display = 'none'; }, /** * Updates the spell check language button. * @param {string} languageCode Language code (ex. "fr"). * @private */ updateSpellCheckLanguageButton_: function(languageCode) { var spellCheckLanguageButton = $( 'language-options-spell-check-language-button'); // Check if the language code matches the current spell check language. if (languageCode == this.spellCheckDictionary_) { // If it matches, the button just says that the spell check language is // currently in use. spellCheckLanguageButton.textContent = localStrings.getString('is_used_for_spell_checking'); // Make it look like a text label. spellCheckLanguageButton.className = 'text-button'; // Remove the event listner. spellCheckLanguageButton.onclick = undefined; } else if (languageCode in templateData.spellCheckLanguageCodeSet) { // If the language is supported as spell check language, users can // click on the button to change the spell check language. spellCheckLanguageButton.textContent = localStrings.getString('use_this_for_spell_checking'); spellCheckLanguageButton.className = ''; spellCheckLanguageButton.languageCode = languageCode; // Add an event listner to the click event. spellCheckLanguageButton.addEventListener('click', this.handleSpellCheckLanguageButtonClick_.bind(this)); } else { // If the language is not supported as spell check language, the // button just says that this language cannot be used for spell // checking. spellCheckLanguageButton.textContent = localStrings.getString('cannot_be_used_for_spell_checking'); spellCheckLanguageButton.className = 'text-button'; spellCheckLanguageButton.onclick = undefined; } spellCheckLanguageButton.style.display = 'block'; $('language-options-ui-notification-bar').style.display = 'none'; }, /** * Updates the input method list. * @param {string} languageCode Language code (ex. "fr"). * @private */ updateInputMethodList_: function(languageCode) { // Give one of the checkboxes or buttons focus, if it's specified in the // URL hash (ex. focus=mozc). Used for automated testing. var focusInputMethodId = -1; var match = document.location.hash.match(/\bfocus=([\w:-]+)\b/); if (match) { focusInputMethodId = match[1]; } // Change the visibility of the input method list. Input methods that // matches |languageCode| will become visible. var inputMethodList = $('language-options-input-method-list'); var labels = inputMethodList.querySelectorAll('label'); for (var i = 0; i < labels.length; i++) { var label = labels[i]; if (languageCode in label.languageCodeSet) { label.style.display = 'block'; var input = label.childNodes[0]; // Give it focus if the ID matches. if (input.inputMethodId == focusInputMethodId) { input.focus(); } } else { label.style.display = 'none'; } } if (focusInputMethodId == 'add') { $('language-options-add-button').focus(); } }, /** * Updates the language list in the add language overlay. * @param {string} languageCode Language code (ex. "fr"). * @private */ updateLanguageListInAddLanguageOverlay_: function(languageCode) { // Change the visibility of the language list in the add language // overlay. Languages that are already active will become invisible, // so that users don't add the same language twice. var languageOptionsList = $('language-options-list'); var languageCodes = languageOptionsList.getLanguageCodes(); var languageCodeSet = {}; for (var i = 0; i < languageCodes.length; i++) { languageCodeSet[languageCodes[i]] = true; } var addLanguageList = $('add-language-overlay-language-list'); var lis = addLanguageList.querySelectorAll('li'); for (var i = 0; i < lis.length; i++) { // The first child button knows the language code. var button = lis[i].childNodes[0]; if (button.languageCode in languageCodeSet) { lis[i].style.display = 'none'; } else { lis[i].style.display = 'block'; } } }, /** * Handles preloadEnginesPref change. * @param {Event} e Change event. * @private */ handlePreloadEnginesPrefChange_: function(e) { var value = e.value.value; this.preloadEngines_ = this.filterBadPreloadEngines_(value.split(',')); this.updateCheckboxesFromPreloadEngines_(); $('language-options-list').updateDeletable(); }, /** * Handles input method checkbox's click event. * @param {Event} e Click event. * @private */ handleCheckboxClick_ : function(e) { var checkbox = e.target; if (this.preloadEngines_.length == 1 && !checkbox.checked) { // Don't allow disabling the last input method. this.showNotification_( localStrings.getString('please_add_another_input_method'), localStrings.getString('ok_button')); checkbox.checked = true; return; } if (checkbox.checked) { chrome.send('inputMethodEnable', [checkbox.inputMethodId]); } else { chrome.send('inputMethodDisable', [checkbox.inputMethodId]); } this.updatePreloadEnginesFromCheckboxes_(); this.preloadEngines_ = this.sortPreloadEngines_(this.preloadEngines_); this.savePreloadEnginesPref_(); }, /** * Handles add language list's click event. * @param {Event} e Click event. */ handleAddLanguageListClick_ : function(e) { var languageOptionsList = $('language-options-list'); var languageCode = e.target.languageCode; // languageCode can be undefined, if click was made on some random // place in the overlay, rather than a button. Ignore it. if (!languageCode) { return; } languageOptionsList.addLanguage(languageCode); var inputMethodIds = this.languageCodeToInputMethodIdsMap_[languageCode]; // Enable the first input method for the language added. if (inputMethodIds && inputMethodIds[0] && // Don't add the input method it's already present. This can // happen if the same input method is shared among multiple // languages (ex. English US keyboard is used for English US and // Filipino). this.preloadEngines_.indexOf(inputMethodIds[0]) == -1) { this.preloadEngines_.push(inputMethodIds[0]); this.updateCheckboxesFromPreloadEngines_(); this.savePreloadEnginesPref_(); } OptionsPage.closeOverlay(); }, /** * Handles add language dialog ok button. */ handleAddLanguageOkButtonClick_ : function() { var languagesSelect = $('add-language-overlay-language-list'); var selectedIndex = languagesSelect.selectedIndex; if (selectedIndex >= 0) { var selection = languagesSelect.options[selectedIndex]; $('language-options-list').addLanguage(String(selection.value)); OptionsPage.closeOverlay(); } }, /** * Checks if languageCode is deletable or not. * @param {String} languageCode the languageCode to check for deletability. */ languageIsDeletable: function(languageCode) { // Don't allow removing the language if it's as UI language. if (languageCode == templateData.currentUiLanguageCode) return false; return (!cr.isChromeOS || this.canDeleteLanguage_(languageCode)); }, /** * Handles spellCheckDictionaryPref change. * @param {Event} e Change event. * @private */ handleSpellCheckDictionaryPrefChange_: function(e) { var languageCode = e.value.value this.spellCheckDictionary_ = languageCode; var languageOptionsList = $('language-options-list'); var selectedLanguageCode = languageOptionsList.getSelectedLanguageCode(); this.updateSpellCheckLanguageButton_(selectedLanguageCode); }, /** * Handles spellCheckLanguageButton click. * @param {Event} e Click event. * @private */ handleSpellCheckLanguageButtonClick_: function(e) { var languageCode = e.target.languageCode; // Save the preference. Preferences.setStringPref(this.spellCheckDictionaryPref, languageCode); chrome.send('spellCheckLanguageChange', [languageCode]); }, /** * Checks whether it's possible to remove the language specified by * languageCode and returns true if possible. This function returns false * if the removal causes the number of preload engines to be zero. * * @param {string} languageCode Language code (ex. "fr"). * @return {boolean} Returns true on success. * @private */ canDeleteLanguage_: function(languageCode) { // First create the set of engines to be removed from input methods // associated with the language code. var enginesToBeRemovedSet = {}; var inputMethodIds = this.languageCodeToInputMethodIdsMap_[languageCode]; for (var i = 0; i < inputMethodIds.length; i++) { enginesToBeRemovedSet[inputMethodIds[i]] = true; } // Then eliminate engines that are also used for other active languages. // For instance, if "xkb:us::eng" is used for both English and Filipino. var languageCodes = $('language-options-list').getLanguageCodes(); for (var i = 0; i < languageCodes.length; i++) { // Skip the target language code. if (languageCodes[i] == languageCode) { continue; } // Check if input methods used in this language are included in // enginesToBeRemovedSet. If so, eliminate these from the set, so // we don't remove this time. var inputMethodIdsForAnotherLanguage = this.languageCodeToInputMethodIdsMap_[languageCodes[i]]; for (var j = 0; j < inputMethodIdsForAnotherLanguage.length; j++) { var inputMethodId = inputMethodIdsForAnotherLanguage[j]; if (inputMethodId in enginesToBeRemovedSet) { delete enginesToBeRemovedSet[inputMethodId]; } } } // Update the preload engine list with the to-be-removed set. var newPreloadEngines = []; for (var i = 0; i < this.preloadEngines_.length; i++) { if (!(this.preloadEngines_[i] in enginesToBeRemovedSet)) { newPreloadEngines.push(this.preloadEngines_[i]); } } // Don't allow this operation if it causes the number of preload // engines to be zero. return (newPreloadEngines.length > 0); }, /** * Saves the preload engines preference. * @private */ savePreloadEnginesPref_: function() { Preferences.setStringPref(this.preloadEnginesPref, this.preloadEngines_.join(',')); }, /** * Updates the checkboxes in the input method list from the preload * engines preference. * @private */ updateCheckboxesFromPreloadEngines_: function() { // Convert the list into a dictonary for simpler lookup. var dictionary = {}; for (var i = 0; i < this.preloadEngines_.length; i++) { dictionary[this.preloadEngines_[i]] = true; } var inputMethodList = $('language-options-input-method-list'); var checkboxes = inputMethodList.querySelectorAll('input'); for (var i = 0; i < checkboxes.length; i++) { checkboxes[i].checked = (checkboxes[i].inputMethodId in dictionary); } }, /** * Updates the preload engines preference from the checkboxes in the * input method list. * @private */ updatePreloadEnginesFromCheckboxes_: function() { this.preloadEngines_ = []; var inputMethodList = $('language-options-input-method-list'); var checkboxes = inputMethodList.querySelectorAll('input'); for (var i = 0; i < checkboxes.length; i++) { if (checkboxes[i].checked) { this.preloadEngines_.push(checkboxes[i].inputMethodId); } } var languageOptionsList = $('language-options-list'); languageOptionsList.updateDeletable(); }, /** * Filters bad preload engines in case bad preload engines are * stored in the preference. Removes duplicates as well. * @param {Array} preloadEngines List of preload engines. * @private */ filterBadPreloadEngines_: function(preloadEngines) { // Convert the list into a dictonary for simpler lookup. var dictionary = {}; for (var i = 0; i < templateData.inputMethodList.length; i++) { dictionary[templateData.inputMethodList[i].id] = true; } var filteredPreloadEngines = []; var seen = {}; for (var i = 0; i < preloadEngines.length; i++) { // Check if the preload engine is present in the // dictionary, and not duplicate. Otherwise, skip it. if (preloadEngines[i] in dictionary && !(preloadEngines[i] in seen)) { filteredPreloadEngines.push(preloadEngines[i]); seen[preloadEngines[i]] = true; } } return filteredPreloadEngines; }, // TODO(kochi): This is an adapted copy from new_new_tab.js. // If this will go as final UI, refactor this to share the component with // new new tab page. /** * Shows notification * @private */ notificationTimeout_: null, showNotification_ : function(text, actionText, opt_delay) { var notificationElement = $('notification'); var actionLink = notificationElement.querySelector('.link-color'); var delay = opt_delay || 10000; function show() { window.clearTimeout(this.notificationTimeout_); notificationElement.classList.add('show'); document.body.classList.add('notification-shown'); } function hide() { window.clearTimeout(this.notificationTimeout_); notificationElement.classList.remove('show'); document.body.classList.remove('notification-shown'); // Prevent tabbing to the hidden link. actionLink.tabIndex = -1; // Setting tabIndex to -1 only prevents future tabbing to it. If, // however, the user switches window or a tab and then moves back to // this tab the element may gain focus. We therefore make sure that we // blur the element so that the element focus is not restored when // coming back to this window. actionLink.blur(); } function delayedHide() { this.notificationTimeout_ = window.setTimeout(hide, delay); } notificationElement.firstElementChild.textContent = text; actionLink.textContent = actionText; actionLink.onclick = hide; actionLink.onkeydown = function(e) { if (e.keyIdentifier == 'Enter') { hide(); } }; notificationElement.onmouseover = show; notificationElement.onmouseout = delayedHide; actionLink.onfocus = show; actionLink.onblur = delayedHide; // Enable tabbing to the link now that it is shown. actionLink.tabIndex = 0; show(); delayedHide(); } }; /** * Chrome callback for when the UI language preference is saved. */ LanguageOptions.uiLanguageSaved = function() { $('language-options-ui-language-button').style.display = 'none'; $('language-options-ui-notification-bar').style.display = 'block'; }; // Export return { LanguageOptions: LanguageOptions }; });