// 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. cr.define('options', function() { const ArrayDataModel = cr.ui.ArrayDataModel; const DeletableItem = options.DeletableItem; const DeletableItemList = options.DeletableItemList; const List = cr.ui.List; const ListItem = cr.ui.ListItem; const ListSingleSelectionModel = cr.ui.ListSingleSelectionModel; /** * Creates a new Language list item. * @param {String} languageCode the languageCode. * @constructor * @extends {DeletableItem.ListItem} */ function LanguageListItem(languageCode) { var el = cr.doc.createElement('li'); el.__proto__ = LanguageListItem.prototype; el.languageCode_ = languageCode; el.decorate(); return el; }; LanguageListItem.prototype = { __proto__: DeletableItem.prototype, /** * The language code of this language. * @type {String} * @private */ languageCode_: null, /** @inheritDoc */ decorate: function() { DeletableItem.prototype.decorate.call(this); var languageCode = this.languageCode_; var languageOptions = options.LanguageOptions.getInstance(); this.deletable = languageOptions.languageIsDeletable(languageCode); this.languageCode = languageCode; this.contentElement.textContent = LanguageList.getDisplayNameFromLanguageCode(languageCode); this.title = LanguageList.getNativeDisplayNameFromLanguageCode(languageCode); this.draggable = true; }, }; /** * Creates a new language list. * @param {Object=} opt_propertyBag Optional properties. * @constructor * @extends {cr.ui.List} */ var LanguageList = cr.ui.define('list'); /** * Gets display name from the given language code. * @param {string} languageCode Language code (ex. "fr"). */ LanguageList.getDisplayNameFromLanguageCode = function(languageCode) { // Build the language code to display name dictionary at first time. if (!this.languageCodeToDisplayName_) { this.languageCodeToDisplayName_ = {}; var languageList = templateData.languageList; for (var i = 0; i < languageList.length; i++) { var language = languageList[i]; this.languageCodeToDisplayName_[language.code] = language.displayName; } } return this.languageCodeToDisplayName_[languageCode]; } /** * Gets native display name from the given language code. * @param {string} languageCode Language code (ex. "fr"). */ LanguageList.getNativeDisplayNameFromLanguageCode = function(languageCode) { // Build the language code to display name dictionary at first time. if (!this.languageCodeToNativeDisplayName_) { this.languageCodeToNativeDisplayName_ = {}; var languageList = templateData.languageList; for (var i = 0; i < languageList.length; i++) { var language = languageList[i]; this.languageCodeToNativeDisplayName_[language.code] = language.nativeDisplayName; } } return this.languageCodeToNativeDisplayName_[languageCode]; } /** * Returns true if the given language code is valid. * @param {string} languageCode Language code (ex. "fr"). */ LanguageList.isValidLanguageCode = function(languageCode) { // Having the display name for the language code means that the // language code is valid. if (LanguageList.getDisplayNameFromLanguageCode(languageCode)) { return true; } return false; } LanguageList.prototype = { __proto__: DeletableItemList.prototype, // The list item being dragged. draggedItem: null, // The drop position information: "below" or "above". dropPos: null, // The preference is a CSV string that describes preferred languages // in Chrome OS. The language list is used for showing the language // list in "Language and Input" options page. preferredLanguagesPref: 'settings.language.preferred_languages', // The preference is a CSV string that describes accept languages used // for content negotiation. To be more precise, the list will be used // in "Accept-Language" header in HTTP requests. acceptLanguagesPref: 'intl.accept_languages', /** @inheritDoc */ decorate: function() { DeletableItemList.prototype.decorate.call(this); this.selectionModel = new ListSingleSelectionModel; // HACK(arv): http://crbug.com/40902 window.addEventListener('resize', this.redraw.bind(this)); // Listen to pref change. if (cr.isChromeOS) { Preferences.getInstance().addEventListener(this.preferredLanguagesPref, this.handlePreferredLanguagesPrefChange_.bind(this)); } else { Preferences.getInstance().addEventListener(this.acceptLanguagesPref, this.handleAcceptLanguagesPrefChange_.bind(this)); } // Listen to drag and drop events. this.addEventListener('dragstart', this.handleDragStart_.bind(this)); this.addEventListener('dragenter', this.handleDragEnter_.bind(this)); this.addEventListener('dragover', this.handleDragOver_.bind(this)); this.addEventListener('drop', this.handleDrop_.bind(this)); }, createItem: function(languageCode) { return new LanguageListItem(languageCode); }, /* * For each item, determines whether it's deletable. */ updateDeletable: function() { for (var i = 0; i < this.items.length; ++i) { var item = this.getListItemByIndex(i); var languageCode = item.languageCode; var languageOptions = options.LanguageOptions.getInstance(); item.deletable = languageOptions.languageIsDeletable(languageCode); } }, /* * Adds a language to the language list. * @param {string} languageCode language code (ex. "fr"). */ addLanguage: function(languageCode) { // It shouldn't happen but ignore the language code if it's // null/undefined, or already present. if (!languageCode || this.dataModel.indexOf(languageCode) >= 0) { return; } this.dataModel.push(languageCode); // Select the last item, which is the language added. this.selectionModel.selectedIndex = this.dataModel.length - 1; this.savePreference_(); }, /* * Gets the language codes of the currently listed languages. */ getLanguageCodes: function() { return this.dataModel.slice(); }, /* * Gets the language code of the selected language. */ getSelectedLanguageCode: function() { return this.selectedItem; }, /* * Selects the language by the given language code. * @returns {boolean} True if the operation is successful. */ selectLanguageByCode: function(languageCode) { var index = this.dataModel.indexOf(languageCode); if (index >= 0) { this.selectionModel.selectedIndex = index; return true; } return false; }, /** @inheritDoc */ deleteItemAtIndex: function(index) { if (index >= 0) { this.dataModel.splice(index, 1); // Once the selected item is removed, there will be no selected item. // Select the item pointed by the lead index. index = this.selectionModel.leadIndex; this.savePreference_(); } return index; }, /* * Computes the target item of drop event. * @param {Event} e The drop or dragover event. * @private */ getTargetFromDropEvent_ : function(e) { var target = e.target; // e.target may be an inner element of the list item while (target != null && !(target instanceof ListItem)) { target = target.parentNode; } return target; }, /* * Handles the dragstart event. * @param {Event} e The dragstart event. * @private */ handleDragStart_: function(e) { var target = e.target; // ListItem should be the only draggable element type in the page, // but just in case. if (target instanceof ListItem) { this.draggedItem = target; e.dataTransfer.effectAllowed = 'move'; // We need to put some kind of data in the drag or it will be // ignored. Use the display name in case the user drags to a text // field or the desktop. e.dataTransfer.setData('text/plain', target.title); } }, /* * Handles the dragenter event. * @param {Event} e The dragenter event. * @private */ handleDragEnter_: function(e) { e.preventDefault(); }, /* * Handles the dragover event. * @param {Event} e The dragover event. * @private */ handleDragOver_: function(e) { var dropTarget = this.getTargetFromDropEvent_(e); // Determines whether the drop target is to accept the drop. // The drop is only successful on another ListItem. if (!(dropTarget instanceof ListItem) || dropTarget == this.draggedItem) { return; } // Compute the drop postion. Should we move the dragged item to // below or above the drop target? var rect = dropTarget.getBoundingClientRect(); var dy = e.clientY - rect.top; var yRatio = dy / rect.height; var dropPos = yRatio <= .5 ? 'above' : 'below'; this.dropPos = dropPos; e.preventDefault(); // TODO(satorux): Show the drop marker just like the bookmark manager. }, /* * Handles the drop event. * @param {Event} e The drop event. * @private */ handleDrop_: function(e) { var dropTarget = this.getTargetFromDropEvent_(e); // Delete the language from the original position. var languageCode = this.draggedItem.languageCode; var originalIndex = this.dataModel.indexOf(languageCode); this.dataModel.splice(originalIndex, 1); // Insert the language to the new position. var newIndex = this.dataModel.indexOf(dropTarget.languageCode); if (this.dropPos == 'below') newIndex += 1; this.dataModel.splice(newIndex, 0, languageCode); // The cursor should move to the moved item. this.selectionModel.selectedIndex = newIndex; // Save the preference. this.savePreference_(); }, /** * Handles preferred languages pref change. * @param {Event} e The change event object. * @private */ handlePreferredLanguagesPrefChange_: function(e) { var languageCodesInCsv = e.value.value; var languageCodes = this.filterBadLanguageCodes_( languageCodesInCsv.split(',')); this.load_(languageCodes); }, /** * Handles accept languages pref change. * @param {Event} e The change event object. * @private */ handleAcceptLanguagesPrefChange_: function(e) { var languageCodesInCsv = e.value.value; var languageCodes = this.filterBadLanguageCodes_( languageCodesInCsv.split(',')); this.load_(languageCodes); }, /** * Loads given language list. * @param {Array} languageCodes List of language codes. * @private */ load_: function(languageCodes) { // Preserve the original selected index. See comments below. var originalSelectedIndex = (this.selectionModel ? this.selectionModel.selectedIndex : -1); this.dataModel = new ArrayDataModel(languageCodes); if (originalSelectedIndex >= 0 && originalSelectedIndex < this.dataModel.length) { // Restore the original selected index if the selected index is // valid after the data model is loaded. This is neeeded to keep // the selected language after the languge is added or removed. this.selectionModel.selectedIndex = originalSelectedIndex; // The lead index should be updated too. this.selectionModel.leadIndex = originalSelectedIndex; } else if (this.dataModel.length > 0){ // Otherwise, select the first item if it's not empty. // Note that ListSingleSelectionModel won't select an item // automatically, hence we manually select the first item here. this.selectionModel.selectedIndex = 0; } }, /** * Saves the preference. */ savePreference_: function() { // Encode the language codes into a CSV string. if (cr.isChromeOS) Preferences.setStringPref(this.preferredLanguagesPref, this.dataModel.slice().join(',')); // Save the same language list as accept languages preference as // well, but we need to expand the language list, to make it more // acceptable. For instance, some web sites don't understand 'en-US' // but 'en'. See crosbug.com/9884. var acceptLanguages = this.expandLanguageCodes(this.dataModel.slice()); Preferences.setStringPref(this.acceptLanguagesPref, acceptLanguages.join(',')); cr.dispatchSimpleEvent(this, 'save'); }, /** * Expands language codes to make these more suitable for Accept-Language. * Example: ['en-US', 'ja', 'en-CA'] => ['en-US', 'en', 'ja', 'en-CA']. * 'en' won't appear twice as this function eliminates duplicates. * @param {Array} languageCodes List of language codes. * @private */ expandLanguageCodes: function(languageCodes) { var expandedLanguageCodes = []; var seen = {}; // Used to eliminiate duplicates. for (var i = 0; i < languageCodes.length; i++) { var languageCode = languageCodes[i]; if (!(languageCode in seen)) { expandedLanguageCodes.push(languageCode); seen[languageCode] = true; } var parts = languageCode.split('-'); if (!(parts[0] in seen)) { expandedLanguageCodes.push(parts[0]); seen[parts[0]] = true; } } return expandedLanguageCodes; }, /** * Filters bad language codes in case bad language codes are * stored in the preference. Removes duplicates as well. * @param {Array} languageCodes List of language codes. * @private */ filterBadLanguageCodes_: function(languageCodes) { var filteredLanguageCodes = []; var seen = {}; for (var i = 0; i < languageCodes.length; i++) { // Check if the the language code is valid, and not // duplicate. Otherwise, skip it. if (LanguageList.isValidLanguageCode(languageCodes[i]) && !(languageCodes[i] in seen)) { filteredLanguageCodes.push(languageCodes[i]); seen[languageCodes[i]] = true; } } return filteredLanguageCodes; }, }; return { LanguageList: LanguageList, LanguageListItem: LanguageListItem }; });