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