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