language_list.js revision 72a454cd3513ac24fbdd0e0cb9ad70b86a99b801
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 5cr.define('options', function() { 6 const ArrayDataModel = cr.ui.ArrayDataModel; 7 const LanguageOptions = options.LanguageOptions; 8 const List = cr.ui.List; 9 const ListItem = cr.ui.ListItem; 10 const ListSingleSelectionModel = cr.ui.ListSingleSelectionModel; 11 12 /** 13 * Creates a new language list. 14 * @param {Object=} opt_propertyBag Optional properties. 15 * @constructor 16 * @extends {cr.ui.List} 17 */ 18 var LanguageList = cr.ui.define('list'); 19 20 /** 21 * Gets display name from the given language code. 22 * @param {string} languageCode Language code (ex. "fr"). 23 */ 24 LanguageList.getDisplayNameFromLanguageCode = function(languageCode) { 25 // Build the language code to display name dictionary at first time. 26 if (!this.languageCodeToDisplayName_) { 27 this.languageCodeToDisplayName_ = {}; 28 var languageList = templateData.languageList; 29 for (var i = 0; i < languageList.length; i++) { 30 var language = languageList[i]; 31 this.languageCodeToDisplayName_[language.code] = language.displayName; 32 } 33 } 34 35 return this.languageCodeToDisplayName_[languageCode]; 36 } 37 38 /** 39 * Gets native display name from the given language code. 40 * @param {string} languageCode Language code (ex. "fr"). 41 */ 42 LanguageList.getNativeDisplayNameFromLanguageCode = function(languageCode) { 43 // Build the language code to display name dictionary at first time. 44 if (!this.languageCodeToNativeDisplayName_) { 45 this.languageCodeToNativeDisplayName_ = {}; 46 var languageList = templateData.languageList; 47 for (var i = 0; i < languageList.length; i++) { 48 var language = languageList[i]; 49 this.languageCodeToNativeDisplayName_[language.code] = 50 language.nativeDisplayName; 51 } 52 } 53 54 return this.languageCodeToNativeDisplayName_[languageCode]; 55 } 56 57 /** 58 * Returns true if the given language code is valid. 59 * @param {string} languageCode Language code (ex. "fr"). 60 */ 61 LanguageList.isValidLanguageCode = function(languageCode) { 62 // Having the display name for the language code means that the 63 // language code is valid. 64 if (LanguageList.getDisplayNameFromLanguageCode(languageCode)) { 65 return true; 66 } 67 return false; 68 } 69 70 LanguageList.prototype = { 71 __proto__: List.prototype, 72 73 // The list item being dragged. 74 draggedItem: null, 75 // The drop position information: "below" or "above". 76 dropPos: null, 77 // The preference is a CSV string that describes preferred languages 78 // in Chrome OS. The language list is used for showing the language 79 // list in "Language and Input" options page. 80 preferredLanguagesPref: 'settings.language.preferred_languages', 81 // The preference is a CSV string that describes accept languages used 82 // for content negotiation. To be more precise, the list will be used 83 // in "Accept-Language" header in HTTP requests. 84 acceptLanguagesPref: 'intl.accept_languages', 85 86 /** @inheritDoc */ 87 decorate: function() { 88 List.prototype.decorate.call(this); 89 this.selectionModel = new ListSingleSelectionModel; 90 91 // HACK(arv): http://crbug.com/40902 92 window.addEventListener('resize', this.redraw.bind(this)); 93 94 // Listen to pref change. 95 if (cr.isChromeOS) { 96 Preferences.getInstance().addEventListener(this.preferredLanguagesPref, 97 this.handlePreferredLanguagesPrefChange_.bind(this)); 98 } else { 99 Preferences.getInstance().addEventListener(this.acceptLanguagesPref, 100 this.handleAcceptLanguagesPrefChange_.bind(this)); 101 } 102 103 // Listen to drag and drop events. 104 this.addEventListener('dragstart', this.handleDragStart_.bind(this)); 105 this.addEventListener('dragenter', this.handleDragEnter_.bind(this)); 106 this.addEventListener('dragover', this.handleDragOver_.bind(this)); 107 this.addEventListener('drop', this.handleDrop_.bind(this)); 108 }, 109 110 createItem: function(languageCode) { 111 var languageDisplayName = 112 LanguageList.getDisplayNameFromLanguageCode(languageCode); 113 var languageNativeDisplayName = 114 LanguageList.getNativeDisplayNameFromLanguageCode(languageCode); 115 return new ListItem({ 116 label: languageDisplayName, 117 draggable: true, 118 languageCode: languageCode, 119 title: languageNativeDisplayName // Show native name as tooltip. 120 }); 121 }, 122 123 /* 124 * Adds a language to the language list. 125 * @param {string} languageCode language code (ex. "fr"). 126 */ 127 addLanguage: function(languageCode) { 128 // It shouldn't happen but ignore the language code if it's 129 // null/undefined, or already present. 130 if (!languageCode || this.dataModel.indexOf(languageCode) >= 0) { 131 return; 132 } 133 this.dataModel.push(languageCode); 134 // Select the last item, which is the language added. 135 this.selectionModel.selectedIndex = this.dataModel.length - 1; 136 137 this.savePreference_(); 138 }, 139 140 /* 141 * Gets the language codes of the currently listed languages. 142 */ 143 getLanguageCodes: function() { 144 return this.dataModel.slice(); 145 }, 146 147 /* 148 * Gets the language code of the selected language. 149 */ 150 getSelectedLanguageCode: function() { 151 return this.selectedItem; 152 }, 153 154 /* 155 * Selects the language by the given language code. 156 * @returns {boolean} True if the operation is successful. 157 */ 158 selectLanguageByCode: function(languageCode) { 159 var index = this.dataModel.indexOf(languageCode); 160 if (index >= 0) { 161 this.selectionModel.selectedIndex = index; 162 return true; 163 } 164 return false; 165 }, 166 167 /* 168 * Removes the currently selected language. 169 */ 170 removeSelectedLanguage: function() { 171 if (this.selectionModel.selectedIndex >= 0) { 172 this.dataModel.splice(this.selectionModel.selectedIndex, 1); 173 // Once the selected item is removed, there will be no selected item. 174 // Select the item pointed by the lead index. 175 this.selectionModel.selectedIndex = this.selectionModel.leadIndex; 176 this.savePreference_(); 177 } 178 }, 179 180 /* 181 * Handles the dragstart event. 182 * @param {Event} e The dragstart event. 183 * @private 184 */ 185 handleDragStart_: function(e) { 186 var target = e.target; 187 // ListItem should be the only draggable element type in the page, 188 // but just in case. 189 if (target instanceof ListItem) { 190 this.draggedItem = target; 191 e.dataTransfer.effectAllowed = 'move'; 192 // We need to put some kind of data in the drag or it will be 193 // ignored. Use the display name in case the user drags to a text 194 // field or the desktop. 195 e.dataTransfer.setData('text/plain', target.title); 196 } 197 }, 198 199 /* 200 * Handles the dragenter event. 201 * @param {Event} e The dragenter event. 202 * @private 203 */ 204 handleDragEnter_: function(e) { 205 e.preventDefault(); 206 }, 207 208 /* 209 * Handles the dragover event. 210 * @param {Event} e The dragover event. 211 * @private 212 */ 213 handleDragOver_: function(e) { 214 var dropTarget = e.target; 215 // Determins whether the drop target is to accept the drop. 216 // The drop is only successful on another ListItem. 217 if (!(dropTarget instanceof ListItem) || 218 dropTarget == this.draggedItem) { 219 return; 220 } 221 // Compute the drop postion. Should we move the dragged item to 222 // below or above the drop target? 223 var rect = dropTarget.getBoundingClientRect(); 224 var dy = e.clientY - rect.top; 225 var yRatio = dy / rect.height; 226 var dropPos = yRatio <= .5 ? 'above' : 'below'; 227 this.dropPos = dropPos; 228 e.preventDefault(); 229 // TODO(satorux): Show the drop marker just like the bookmark manager. 230 }, 231 232 /* 233 * Handles the drop event. 234 * @param {Event} e The drop event. 235 * @private 236 */ 237 handleDrop_: function(e) { 238 var dropTarget = e.target; 239 240 // Delete the language from the original position. 241 var languageCode = this.draggedItem.languageCode; 242 var originalIndex = this.dataModel.indexOf(languageCode); 243 this.dataModel.splice(originalIndex, 1); 244 // Insert the language to the new position. 245 var newIndex = this.dataModel.indexOf(dropTarget.languageCode); 246 if (this.dropPos == 'below') 247 newIndex += 1; 248 this.dataModel.splice(newIndex, 0, languageCode); 249 // The cursor should move to the moved item. 250 this.selectionModel.selectedIndex = newIndex; 251 // Save the preference. 252 this.savePreference_(); 253 }, 254 255 /** 256 * Handles preferred languages pref change. 257 * @param {Event} e The change event object. 258 * @private 259 */ 260 handlePreferredLanguagesPrefChange_: function(e) { 261 var languageCodesInCsv = e.value.value; 262 var languageCodes = this.filterBadLanguageCodes_( 263 languageCodesInCsv.split(',')); 264 this.load_(languageCodes); 265 }, 266 267 /** 268 * Handles accept languages pref change. 269 * @param {Event} e The change event object. 270 * @private 271 */ 272 handleAcceptLanguagesPrefChange_: function(e) { 273 var languageCodesInCsv = e.value.value; 274 var languageCodes = this.filterBadLanguageCodes_( 275 languageCodesInCsv.split(',')); 276 this.load_(languageCodes); 277 }, 278 279 /** 280 * Loads given language list. 281 * @param {Array} languageCodes List of language codes. 282 * @private 283 */ 284 load_: function(languageCodes) { 285 // Preserve the original selected index. See comments below. 286 var originalSelectedIndex = (this.selectionModel ? 287 this.selectionModel.selectedIndex : -1); 288 this.dataModel = new ArrayDataModel(languageCodes); 289 if (originalSelectedIndex >= 0 && 290 originalSelectedIndex < this.dataModel.length) { 291 // Restore the original selected index if the selected index is 292 // valid after the data model is loaded. This is neeeded to keep 293 // the selected language after the languge is added or removed. 294 this.selectionModel.selectedIndex = originalSelectedIndex; 295 // The lead index should be updated too. 296 this.selectionModel.leadIndex = originalSelectedIndex; 297 } else if (this.dataModel.length > 0){ 298 // Otherwise, select the first item if it's not empty. 299 // Note that ListSingleSelectionModel won't select an item 300 // automatically, hence we manually select the first item here. 301 this.selectionModel.selectedIndex = 0; 302 } 303 }, 304 305 /** 306 * Saves the preference. 307 */ 308 savePreference_: function() { 309 // Encode the language codes into a CSV string. 310 if (cr.isChromeOS) 311 Preferences.setStringPref(this.preferredLanguagesPref, 312 this.dataModel.slice().join(',')); 313 // Save the same language list as accept languages preference as 314 // well, but we need to expand the language list, to make it more 315 // acceptable. For instance, some web sites don't understand 'en-US' 316 // but 'en'. See crosbug.com/9884. 317 var acceptLanguages = this.expandLanguageCodes(this.dataModel.slice()); 318 Preferences.setStringPref(this.acceptLanguagesPref, 319 acceptLanguages.join(',')); 320 cr.dispatchSimpleEvent(this, 'save'); 321 }, 322 323 /** 324 * Expands language codes to make these more suitable for Accept-Language. 325 * Example: ['en-US', 'ja', 'en-CA'] => ['en-US', 'en', 'ja', 'en-CA']. 326 * 'en' won't appear twice as this function eliminates duplicates. 327 * @param {Array} languageCodes List of language codes. 328 * @private 329 */ 330 expandLanguageCodes: function(languageCodes) { 331 var expandedLanguageCodes = []; 332 var seen = {}; // Used to eliminiate duplicates. 333 for (var i = 0; i < languageCodes.length; i++) { 334 var languageCode = languageCodes[i]; 335 if (!(languageCode in seen)) { 336 expandedLanguageCodes.push(languageCode); 337 seen[languageCode] = true; 338 } 339 var parts = languageCode.split('-'); 340 if (!(parts[0] in seen)) { 341 expandedLanguageCodes.push(parts[0]); 342 seen[parts[0]] = true; 343 } 344 } 345 return expandedLanguageCodes; 346 }, 347 348 /** 349 * Filters bad language codes in case bad language codes are 350 * stored in the preference. Removes duplicates as well. 351 * @param {Array} languageCodes List of language codes. 352 * @private 353 */ 354 filterBadLanguageCodes_: function(languageCodes) { 355 var filteredLanguageCodes = []; 356 var seen = {}; 357 for (var i = 0; i < languageCodes.length; i++) { 358 // Check if the the language code is valid, and not 359 // duplicate. Otherwise, skip it. 360 if (LanguageList.isValidLanguageCode(languageCodes[i]) && 361 !(languageCodes[i] in seen)) { 362 filteredLanguageCodes.push(languageCodes[i]); 363 seen[languageCodes[i]] = true; 364 } 365 } 366 return filteredLanguageCodes; 367 }, 368 }; 369 370 return { 371 LanguageList: LanguageList 372 }; 373}); 374