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 } else if (this.dataModel.length > 0){ | |
409 // Otherwise, select the first item if it's not empty. | |
410 // Note that ListSingleSelectionModel won't select an item | |
411 // automatically, hence we manually select the first item here. | |
412 this.selectionModel.selectedIndex = 0; | |
413 } | |
414 }, | |
415 | |
416 /** | |
417 * Saves the preference. | |
418 */ | |
419 savePreference_: function() { | |
420 // Encode the language codes into a CSV string. | |
421 if (cr.isChromeOS) | |
422 Preferences.setStringPref(this.preferredLanguagesPref, | |
423 this.dataModel.slice().join(',')); | |
424 // Save the same language list as accept languages preference as | |
425 // well, but we need to expand the language list, to make it more | |
426 // acceptable. For instance, some web sites don't understand 'en-US' | |
427 // but 'en'. See crosbug.com/9884. | |
428 var acceptLanguages = this.expandLanguageCodes(this.dataModel.slice()); | |
429 Preferences.setStringPref(this.acceptLanguagesPref, | |
430 acceptLanguages.join(',')); | |
431 cr.dispatchSimpleEvent(this, 'save'); | |
432 }, | |
433 | |
434 /** | |
435 * Expands language codes to make these more suitable for Accept-Language. | |
436 * Example: ['en-US', 'ja', 'en-CA'] => ['en-US', 'en', 'ja', 'en-CA']. | |
437 * 'en' won't appear twice as this function eliminates duplicates. | |
438 * @param {Array} languageCodes List of language codes. | |
439 * @private | |
440 */ | |
441 expandLanguageCodes: function(languageCodes) { | |
442 var expandedLanguageCodes = []; | |
443 var seen = {}; // Used to eliminiate duplicates. | |
444 for (var i = 0; i < languageCodes.length; i++) { | |
445 var languageCode = languageCodes[i]; | |
446 if (!(languageCode in seen)) { | |
447 expandedLanguageCodes.push(languageCode); | |
448 seen[languageCode] = true; | |
449 } | |
450 var parts = languageCode.split('-'); | |
451 if (!(parts[0] in seen)) { | |
452 expandedLanguageCodes.push(parts[0]); | |
453 seen[parts[0]] = true; | |
454 } | |
455 } | |
456 return expandedLanguageCodes; | |
457 }, | |
458 | |
459 /** | |
460 * Filters bad language codes in case bad language codes are | |
461 * stored in the preference. Removes duplicates as well. | |
462 * @param {Array} languageCodes List of language codes. | |
463 * @private | |
464 */ | |
465 filterBadLanguageCodes_: function(languageCodes) { | |
466 var filteredLanguageCodes = []; | |
467 var seen = {}; | |
468 for (var i = 0; i < languageCodes.length; i++) { | |
469 // Check if the the language code is valid, and not | |
470 // duplicate. Otherwise, skip it. | |
471 if (LanguageList.isValidLanguageCode(languageCodes[i]) && | |
472 !(languageCodes[i] in seen)) { | |
473 filteredLanguageCodes.push(languageCodes[i]); | |
474 seen[languageCodes[i]] = true; | |
475 } | |
476 } | |
477 return filteredLanguageCodes; | |
478 }, | |
479 }; | |
480 | |
481 return { | |
482 LanguageList: LanguageList, | |
483 LanguageListItem: LanguageListItem | |
484 }; | |
485 }); | |
OLD | NEW |