| OLD | NEW |
| (Empty) |
| 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 | |
| 5 cr.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 this.addEventListener('dragleave', this.handleDragLeave_.bind(this)); | |
| 150 }, | |
| 151 | |
| 152 createItem: function(languageCode) { | |
| 153 return new LanguageListItem(languageCode); | |
| 154 }, | |
| 155 | |
| 156 /* | |
| 157 * For each item, determines whether it's deletable. | |
| 158 */ | |
| 159 updateDeletable: function() { | |
| 160 for (var i = 0; i < this.items.length; ++i) { | |
| 161 var item = this.getListItemByIndex(i); | |
| 162 var languageCode = item.languageCode; | |
| 163 var languageOptions = options.LanguageOptions.getInstance(); | |
| 164 item.deletable = languageOptions.languageIsDeletable(languageCode); | |
| 165 } | |
| 166 }, | |
| 167 | |
| 168 /* | |
| 169 * Adds a language to the language list. | |
| 170 * @param {string} languageCode language code (ex. "fr"). | |
| 171 */ | |
| 172 addLanguage: function(languageCode) { | |
| 173 // It shouldn't happen but ignore the language code if it's | |
| 174 // null/undefined, or already present. | |
| 175 if (!languageCode || this.dataModel.indexOf(languageCode) >= 0) { | |
| 176 return; | |
| 177 } | |
| 178 this.dataModel.push(languageCode); | |
| 179 // Select the last item, which is the language added. | |
| 180 this.selectionModel.selectedIndex = this.dataModel.length - 1; | |
| 181 | |
| 182 this.savePreference_(); | |
| 183 }, | |
| 184 | |
| 185 /* | |
| 186 * Gets the language codes of the currently listed languages. | |
| 187 */ | |
| 188 getLanguageCodes: function() { | |
| 189 return this.dataModel.slice(); | |
| 190 }, | |
| 191 | |
| 192 /* | |
| 193 * Gets the language code of the selected language. | |
| 194 */ | |
| 195 getSelectedLanguageCode: function() { | |
| 196 return this.selectedItem; | |
| 197 }, | |
| 198 | |
| 199 /* | |
| 200 * Selects the language by the given language code. | |
| 201 * @returns {boolean} True if the operation is successful. | |
| 202 */ | |
| 203 selectLanguageByCode: function(languageCode) { | |
| 204 var index = this.dataModel.indexOf(languageCode); | |
| 205 if (index >= 0) { | |
| 206 this.selectionModel.selectedIndex = index; | |
| 207 return true; | |
| 208 } | |
| 209 return false; | |
| 210 }, | |
| 211 | |
| 212 /** @inheritDoc */ | |
| 213 deleteItemAtIndex: function(index) { | |
| 214 if (index >= 0) { | |
| 215 this.dataModel.splice(index, 1); | |
| 216 // Once the selected item is removed, there will be no selected item. | |
| 217 // Select the item pointed by the lead index. | |
| 218 index = this.selectionModel.leadIndex; | |
| 219 this.savePreference_(); | |
| 220 } | |
| 221 return index; | |
| 222 }, | |
| 223 | |
| 224 /* | |
| 225 * Computes the target item of drop event. | |
| 226 * @param {Event} e The drop or dragover event. | |
| 227 * @private | |
| 228 */ | |
| 229 getTargetFromDropEvent_ : function(e) { | |
| 230 var target = e.target; | |
| 231 // e.target may be an inner element of the list item | |
| 232 while (target != null && !(target instanceof ListItem)) { | |
| 233 target = target.parentNode; | |
| 234 } | |
| 235 return target; | |
| 236 }, | |
| 237 | |
| 238 /* | |
| 239 * Handles the dragstart event. | |
| 240 * @param {Event} e The dragstart event. | |
| 241 * @private | |
| 242 */ | |
| 243 handleDragStart_: function(e) { | |
| 244 var target = e.target; | |
| 245 // ListItem should be the only draggable element type in the page, | |
| 246 // but just in case. | |
| 247 if (target instanceof ListItem) { | |
| 248 this.draggedItem = target; | |
| 249 e.dataTransfer.effectAllowed = 'move'; | |
| 250 // We need to put some kind of data in the drag or it will be | |
| 251 // ignored. Use the display name in case the user drags to a text | |
| 252 // field or the desktop. | |
| 253 e.dataTransfer.setData('text/plain', target.title); | |
| 254 } | |
| 255 }, | |
| 256 | |
| 257 /* | |
| 258 * Handles the dragenter event. | |
| 259 * @param {Event} e The dragenter event. | |
| 260 * @private | |
| 261 */ | |
| 262 handleDragEnter_: function(e) { | |
| 263 e.preventDefault(); | |
| 264 }, | |
| 265 | |
| 266 /* | |
| 267 * Handles the dragover event. | |
| 268 * @param {Event} e The dragover event. | |
| 269 * @private | |
| 270 */ | |
| 271 handleDragOver_: function(e) { | |
| 272 var dropTarget = this.getTargetFromDropEvent_(e); | |
| 273 // Determines whether the drop target is to accept the drop. | |
| 274 // The drop is only successful on another ListItem. | |
| 275 if (!(dropTarget instanceof ListItem) || | |
| 276 dropTarget == this.draggedItem) { | |
| 277 this.hideDropMarker_(); | |
| 278 return; | |
| 279 } | |
| 280 // Compute the drop postion. Should we move the dragged item to | |
| 281 // below or above the drop target? | |
| 282 var rect = dropTarget.getBoundingClientRect(); | |
| 283 var dy = e.clientY - rect.top; | |
| 284 var yRatio = dy / rect.height; | |
| 285 var dropPos = yRatio <= .5 ? 'above' : 'below'; | |
| 286 this.dropPos = dropPos; | |
| 287 this.showDropMarker_(dropTarget, dropPos); | |
| 288 e.preventDefault(); | |
| 289 }, | |
| 290 | |
| 291 /* | |
| 292 * Handles the drop event. | |
| 293 * @param {Event} e The drop event. | |
| 294 * @private | |
| 295 */ | |
| 296 handleDrop_: function(e) { | |
| 297 var dropTarget = this.getTargetFromDropEvent_(e); | |
| 298 this.hideDropMarker_(); | |
| 299 | |
| 300 // Delete the language from the original position. | |
| 301 var languageCode = this.draggedItem.languageCode; | |
| 302 var originalIndex = this.dataModel.indexOf(languageCode); | |
| 303 this.dataModel.splice(originalIndex, 1); | |
| 304 // Insert the language to the new position. | |
| 305 var newIndex = this.dataModel.indexOf(dropTarget.languageCode); | |
| 306 if (this.dropPos == 'below') | |
| 307 newIndex += 1; | |
| 308 this.dataModel.splice(newIndex, 0, languageCode); | |
| 309 // The cursor should move to the moved item. | |
| 310 this.selectionModel.selectedIndex = newIndex; | |
| 311 // Save the preference. | |
| 312 this.savePreference_(); | |
| 313 }, | |
| 314 | |
| 315 /* | |
| 316 * Handles the dragleave event. | |
| 317 * @param {Event} e The dragleave event | |
| 318 * @private | |
| 319 */ | |
| 320 handleDragLeave_ : function(e) { | |
| 321 this.hideDropMarker_(); | |
| 322 }, | |
| 323 | |
| 324 /* | |
| 325 * Shows and positions the marker to indicate the drop target. | |
| 326 * @param {HTMLElement} target The current target list item of drop | |
| 327 * @param {string} pos 'below' or 'above' | |
| 328 * @private | |
| 329 */ | |
| 330 showDropMarker_ : function(target, pos) { | |
| 331 window.clearTimeout(this.hideDropMarkerTimer_); | |
| 332 var marker = $('language-options-list-dropmarker'); | |
| 333 var rect = target.getBoundingClientRect(); | |
| 334 var markerHeight = 8; | |
| 335 if (pos == 'above') { | |
| 336 marker.style.top = (rect.top - markerHeight/2) + 'px'; | |
| 337 } else { | |
| 338 marker.style.top = (rect.bottom - markerHeight/2) + 'px'; | |
| 339 } | |
| 340 marker.style.width = rect.width + 'px'; | |
| 341 marker.style.left = rect.left + 'px'; | |
| 342 marker.style.display = 'block'; | |
| 343 }, | |
| 344 | |
| 345 /* | |
| 346 * Hides the drop marker. | |
| 347 * @private | |
| 348 */ | |
| 349 hideDropMarker_ : function() { | |
| 350 // Hide the marker in a timeout to reduce flickering as we move between | |
| 351 // valid drop targets. | |
| 352 window.clearTimeout(this.hideDropMarkerTimer_); | |
| 353 this.hideDropMarkerTimer_ = window.setTimeout(function() { | |
| 354 $('language-options-list-dropmarker').style.display = ''; | |
| 355 }, 100); | |
| 356 }, | |
| 357 | |
| 358 /** | |
| 359 * Handles preferred languages pref change. | |
| 360 * @param {Event} e The change event object. | |
| 361 * @private | |
| 362 */ | |
| 363 handlePreferredLanguagesPrefChange_: function(e) { | |
| 364 var languageCodesInCsv = e.value.value; | |
| 365 var languageCodes = languageCodesInCsv.split(','); | |
| 366 | |
| 367 // Add the UI language to the initial list of languages. This is to avoid | |
| 368 // a bug where the UI language would be removed from the preferred | |
| 369 // language list by sync on first login. | |
| 370 // See: crosbug.com/14283 | |
| 371 languageCodes.push(navigator.language); | |
| 372 languageCodes = this.filterBadLanguageCodes_(languageCodes); | |
| 373 this.load_(languageCodes); | |
| 374 }, | |
| 375 | |
| 376 /** | |
| 377 * Handles accept languages pref change. | |
| 378 * @param {Event} e The change event object. | |
| 379 * @private | |
| 380 */ | |
| 381 handleAcceptLanguagesPrefChange_: function(e) { | |
| 382 var languageCodesInCsv = e.value.value; | |
| 383 var languageCodes = this.filterBadLanguageCodes_( | |
| 384 languageCodesInCsv.split(',')); | |
| 385 this.load_(languageCodes); | |
| 386 }, | |
| 387 | |
| 388 /** | |
| 389 * Loads given language list. | |
| 390 * @param {Array} languageCodes List of language codes. | |
| 391 * @private | |
| 392 */ | |
| 393 load_: function(languageCodes) { | |
| 394 // Preserve the original selected index. See comments below. | |
| 395 var originalSelectedIndex = (this.selectionModel ? | |
| 396 this.selectionModel.selectedIndex : -1); | |
| 397 this.dataModel = new ArrayDataModel(languageCodes); | |
| 398 if (originalSelectedIndex >= 0 && | |
| 399 originalSelectedIndex < this.dataModel.length) { | |
| 400 // Restore the original selected index if the selected index is | |
| 401 // valid after the data model is loaded. This is neeeded to keep | |
| 402 // the selected language after the languge is added or removed. | |
| 403 this.selectionModel.selectedIndex = originalSelectedIndex; | |
| 404 // The lead index should be updated too. | |
| 405 this.selectionModel.leadIndex = originalSelectedIndex; | |
| 406 } else if (this.dataModel.length > 0){ | |
| 407 // Otherwise, select the first item if it's not empty. | |
| 408 // Note that ListSingleSelectionModel won't select an item | |
| 409 // automatically, hence we manually select the first item here. | |
| 410 this.selectionModel.selectedIndex = 0; | |
| 411 } | |
| 412 }, | |
| 413 | |
| 414 /** | |
| 415 * Saves the preference. | |
| 416 */ | |
| 417 savePreference_: function() { | |
| 418 // Encode the language codes into a CSV string. | |
| 419 if (cr.isChromeOS) | |
| 420 Preferences.setStringPref(this.preferredLanguagesPref, | |
| 421 this.dataModel.slice().join(',')); | |
| 422 // Save the same language list as accept languages preference as | |
| 423 // well, but we need to expand the language list, to make it more | |
| 424 // acceptable. For instance, some web sites don't understand 'en-US' | |
| 425 // but 'en'. See crosbug.com/9884. | |
| 426 var acceptLanguages = this.expandLanguageCodes(this.dataModel.slice()); | |
| 427 Preferences.setStringPref(this.acceptLanguagesPref, | |
| 428 acceptLanguages.join(',')); | |
| 429 cr.dispatchSimpleEvent(this, 'save'); | |
| 430 }, | |
| 431 | |
| 432 /** | |
| 433 * Expands language codes to make these more suitable for Accept-Language. | |
| 434 * Example: ['en-US', 'ja', 'en-CA'] => ['en-US', 'en', 'ja', 'en-CA']. | |
| 435 * 'en' won't appear twice as this function eliminates duplicates. | |
| 436 * @param {Array} languageCodes List of language codes. | |
| 437 * @private | |
| 438 */ | |
| 439 expandLanguageCodes: function(languageCodes) { | |
| 440 var expandedLanguageCodes = []; | |
| 441 var seen = {}; // Used to eliminiate duplicates. | |
| 442 for (var i = 0; i < languageCodes.length; i++) { | |
| 443 var languageCode = languageCodes[i]; | |
| 444 if (!(languageCode in seen)) { | |
| 445 expandedLanguageCodes.push(languageCode); | |
| 446 seen[languageCode] = true; | |
| 447 } | |
| 448 var parts = languageCode.split('-'); | |
| 449 if (!(parts[0] in seen)) { | |
| 450 expandedLanguageCodes.push(parts[0]); | |
| 451 seen[parts[0]] = true; | |
| 452 } | |
| 453 } | |
| 454 return expandedLanguageCodes; | |
| 455 }, | |
| 456 | |
| 457 /** | |
| 458 * Filters bad language codes in case bad language codes are | |
| 459 * stored in the preference. Removes duplicates as well. | |
| 460 * @param {Array} languageCodes List of language codes. | |
| 461 * @private | |
| 462 */ | |
| 463 filterBadLanguageCodes_: function(languageCodes) { | |
| 464 var filteredLanguageCodes = []; | |
| 465 var seen = {}; | |
| 466 for (var i = 0; i < languageCodes.length; i++) { | |
| 467 // Check if the the language code is valid, and not | |
| 468 // duplicate. Otherwise, skip it. | |
| 469 if (LanguageList.isValidLanguageCode(languageCodes[i]) && | |
| 470 !(languageCodes[i] in seen)) { | |
| 471 filteredLanguageCodes.push(languageCodes[i]); | |
| 472 seen[languageCodes[i]] = true; | |
| 473 } | |
| 474 } | |
| 475 return filteredLanguageCodes; | |
| 476 }, | |
| 477 }; | |
| 478 | |
| 479 return { | |
| 480 LanguageList: LanguageList, | |
| 481 LanguageListItem: LanguageListItem | |
| 482 }; | |
| 483 }); | |
| OLD | NEW |