Index: chrome/browser/resources/options2/language_list.js |
diff --git a/chrome/browser/resources/options2/language_list.js b/chrome/browser/resources/options2/language_list.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..3cbcb752a91acfa93e8ea97d975b8732fbc0df99 |
--- /dev/null |
+++ b/chrome/browser/resources/options2/language_list.js |
@@ -0,0 +1,487 @@ |
+// 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.languageName = cr.doc.createElement('div'); |
+ this.languageName.className = 'language-name'; |
+ this.languageName.textContent = |
+ LanguageList.getDisplayNameFromLanguageCode(languageCode); |
+ this.contentElement.appendChild(this.languageName); |
+ 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)); |
+ this.addEventListener('dragleave', this.handleDragLeave_.bind(this)); |
+ }, |
+ |
+ createItem: function(languageCode) { |
+ return new LanguageListItem(languageCode); |
+ }, |
+ |
+ /* |
+ * For each item, determines whether it's deletable. |
+ */ |
+ updateDeletable: function() { |
+ var items = this.items; |
+ for (var i = 0; i < items.length; ++i) { |
+ var item = items[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) { |
+ this.hideDropMarker_(); |
+ 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; |
+ this.showDropMarker_(dropTarget, dropPos); |
+ e.preventDefault(); |
+ }, |
+ |
+ /* |
+ * Handles the drop event. |
+ * @param {Event} e The drop event. |
+ * @private |
+ */ |
+ handleDrop_: function(e) { |
+ var dropTarget = this.getTargetFromDropEvent_(e); |
+ this.hideDropMarker_(); |
+ |
+ // 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 the dragleave event. |
+ * @param {Event} e The dragleave event |
+ * @private |
+ */ |
+ handleDragLeave_ : function(e) { |
+ this.hideDropMarker_(); |
+ }, |
+ |
+ /* |
+ * Shows and positions the marker to indicate the drop target. |
+ * @param {HTMLElement} target The current target list item of drop |
+ * @param {string} pos 'below' or 'above' |
+ * @private |
+ */ |
+ showDropMarker_ : function(target, pos) { |
+ window.clearTimeout(this.hideDropMarkerTimer_); |
+ var marker = $('language-options-list-dropmarker'); |
+ var rect = target.getBoundingClientRect(); |
+ var markerHeight = 8; |
+ if (pos == 'above') { |
+ marker.style.top = (rect.top - markerHeight/2) + 'px'; |
+ } else { |
+ marker.style.top = (rect.bottom - markerHeight/2) + 'px'; |
+ } |
+ marker.style.width = rect.width + 'px'; |
+ marker.style.left = rect.left + 'px'; |
+ marker.style.display = 'block'; |
+ }, |
+ |
+ /* |
+ * Hides the drop marker. |
+ * @private |
+ */ |
+ hideDropMarker_ : function() { |
+ // Hide the marker in a timeout to reduce flickering as we move between |
+ // valid drop targets. |
+ window.clearTimeout(this.hideDropMarkerTimer_); |
+ this.hideDropMarkerTimer_ = window.setTimeout(function() { |
+ $('language-options-list-dropmarker').style.display = ''; |
+ }, 100); |
+ }, |
+ |
+ /** |
+ * Handles preferred languages pref change. |
+ * @param {Event} e The change event object. |
+ * @private |
+ */ |
+ handlePreferredLanguagesPrefChange_: function(e) { |
+ var languageCodesInCsv = e.value.value; |
+ var languageCodes = languageCodesInCsv.split(','); |
+ |
+ // Add the UI language to the initial list of languages. This is to avoid |
+ // a bug where the UI language would be removed from the preferred |
+ // language list by sync on first login. |
+ // See: crosbug.com/14283 |
+ languageCodes.push(navigator.language); |
+ languageCodes = this.filterBadLanguageCodes_(languageCodes); |
+ 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 |
+ }; |
+}); |