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