Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / options / language_list.js
blob1030a0ac70e449e6cd59f36bf21855a1f61d738e
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.
5 cr.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;
13   /**
14    * Creates a new Language list item.
15    * @param {Object} languageInfo The information of the language.
16    * @constructor
17    * @extends {options.DeletableItem}
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   };
27   LanguageListItem.prototype = {
28     __proto__: DeletableItem.prototype,
30     /**
31      * The language code of this language.
32      * @type {?string}
33      * @private
34      */
35     languageCode_: null,
37     /** @override */
38     decorate: function() {
39       DeletableItem.prototype.decorate.call(this);
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   };
55   /**
56    * Creates a new language list.
57    * @param {Object=} opt_propertyBag Optional properties.
58    * @constructor
59    * @extends {options.DeletableItemList}
60    */
61   var LanguageList = cr.ui.define('list');
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     }
78     return this.languageCodeToLanguageInfo_[languageCode];
79   };
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   };
94   LanguageList.prototype = {
95     __proto__: DeletableItemList.prototype,
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',
110     /** @override */
111     decorate: function() {
112       DeletableItemList.prototype.decorate.call(this);
113       this.selectionModel = new ListSingleSelectionModel;
115       // HACK(arv): http://crbug.com/40902
116       window.addEventListener('resize', this.redraw.bind(this));
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       }
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     },
135     /**
136      * @override
137      * @param {string} languageCode
138      */
139     createItem: function(languageCode) {
140       var languageInfo =
141           LanguageList.getLanguageInfoFromLanguageCode(languageCode);
142       return new LanguageListItem(languageInfo);
143     },
145     /*
146      * For each item, determines whether it's deletable.
147      */
148     updateDeletable: function() {
149       var items = this.items;
150       for (var i = 0; i < items.length; ++i) {
151         var item = items[i];
152         var languageCode = item.languageCode;
153         var languageOptions = options.LanguageOptions.getInstance();
154         item.deletable = languageOptions.languageIsDeletable(languageCode);
155       }
156     },
158     /**
159      * Adds a language to the language list.
160      * @param {string} languageCode language code (ex. "fr").
161      */
162     addLanguage: function(languageCode) {
163       // It shouldn't happen but ignore the language code if it's
164       // null/undefined, or already present.
165       if (!languageCode || this.dataModel.indexOf(languageCode) >= 0) {
166         return;
167       }
168       this.dataModel.push(languageCode);
169       // Select the last item, which is the language added.
170       this.selectionModel.selectedIndex = this.dataModel.length - 1;
172       this.savePreference_();
173     },
175     /**
176      * Gets the language codes of the currently listed languages.
177      */
178     getLanguageCodes: function() {
179       return this.dataModel.slice();
180     },
182     /**
183      * Clears the selection
184      */
185     clearSelection: function() {
186       this.selectionModel.unselectAll();
187     },
189     /**
190      * Gets the language code of the selected language.
191      */
192     getSelectedLanguageCode: function() {
193       return this.selectedItem;
194     },
196     /**
197      * Selects the language by the given language code.
198      * @return {boolean} True if the operation is successful.
199      */
200     selectLanguageByCode: function(languageCode) {
201       var index = this.dataModel.indexOf(languageCode);
202       if (index >= 0) {
203         this.selectionModel.selectedIndex = index;
204         return true;
205       }
206       return false;
207     },
209     /** @override */
210     deleteItemAtIndex: function(index) {
211       if (index >= 0) {
212         this.dataModel.splice(index, 1);
213         // Once the selected item is removed, there will be no selected item.
214         // Select the item pointed by the lead index.
215         index = this.selectionModel.leadIndex;
216         this.savePreference_();
217       }
218       return index;
219     },
221     /**
222      * Computes the target item of drop event.
223      * @param {Event} e The drop or dragover event.
224      * @private
225      */
226     getTargetFromDropEvent_: function(e) {
227       var target = e.target;
228       // e.target may be an inner element of the list item
229       while (target != null && !(target instanceof ListItem)) {
230         target = target.parentNode;
231       }
232       return target;
233     },
235     /**
236      * Handles the dragstart event.
237      * @param {Event} e The dragstart event.
238      * @private
239      */
240     handleDragStart_: function(e) {
241       var target = e.target;
242       // ListItem should be the only draggable element type in the page,
243       // but just in case.
244       if (target instanceof ListItem) {
245         this.draggedItem = target;
246         e.dataTransfer.effectAllowed = 'move';
247         // We need to put some kind of data in the drag or it will be
248         // ignored.  Use the display name in case the user drags to a text
249         // field or the desktop.
250         e.dataTransfer.setData('text/plain', target.title);
251       }
252     },
254     /**
255      * Handles the dragenter event.
256      * @param {Event} e The dragenter event.
257      * @private
258      */
259     handleDragEnter_: function(e) {
260       e.preventDefault();
261     },
263     /**
264      * Handles the dragover event.
265      * @param {Event} e The dragover event.
266      * @private
267      */
268     handleDragOver_: function(e) {
269       var dropTarget = this.getTargetFromDropEvent_(e);
270       // Determines whether the drop target is to accept the drop.
271       // The drop is only successful on another ListItem.
272       if (!(dropTarget instanceof ListItem) ||
273           dropTarget == this.draggedItem) {
274         this.hideDropMarker_();
275         return;
276       }
277       // Compute the drop postion. Should we move the dragged item to
278       // below or above the drop target?
279       var rect = dropTarget.getBoundingClientRect();
280       var dy = e.clientY - rect.top;
281       var yRatio = dy / rect.height;
282       var dropPos = yRatio <= .5 ? 'above' : 'below';
283       this.dropPos = dropPos;
284       this.showDropMarker_(dropTarget, dropPos);
285       e.preventDefault();
286     },
288     /**
289      * Handles the drop event.
290      * @param {Event} e The drop event.
291      * @private
292      */
293     handleDrop_: function(e) {
294       var dropTarget = this.getTargetFromDropEvent_(e);
295       this.hideDropMarker_();
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     },
312     /**
313      * Handles the dragleave event.
314      * @param {Event} e The dragleave event
315      * @private
316      */
317     handleDragLeave_: function(e) {
318       this.hideDropMarker_();
319     },
321     /**
322      * Shows and positions the marker to indicate the drop target.
323      * @param {HTMLElement} target The current target list item of drop
324      * @param {string} pos 'below' or 'above'
325      * @private
326      */
327     showDropMarker_: function(target, pos) {
328       window.clearTimeout(this.hideDropMarkerTimer_);
329       var marker = $('language-options-list-dropmarker');
330       var rect = target.getBoundingClientRect();
331       var markerHeight = 8;
332       if (pos == 'above') {
333         marker.style.top = (rect.top - markerHeight / 2) + 'px';
334       } else {
335         marker.style.top = (rect.bottom - markerHeight / 2) + 'px';
336       }
337       marker.style.width = rect.width + 'px';
338       marker.style.left = rect.left + 'px';
339       marker.style.display = 'block';
340     },
342     /**
343      * Hides the drop marker.
344      * @private
345      */
346     hideDropMarker_: function() {
347       // Hide the marker in a timeout to reduce flickering as we move between
348       // valid drop targets.
349       window.clearTimeout(this.hideDropMarkerTimer_);
350       this.hideDropMarkerTimer_ = window.setTimeout(function() {
351         $('language-options-list-dropmarker').style.display = '';
352       }, 100);
353     },
355     /**
356      * Handles preferred languages pref change.
357      * @param {Event} e The change event object.
358      * @private
359      */
360     handlePreferredLanguagesPrefChange_: function(e) {
361       var languageCodesInCsv = e.value.value;
362       var languageCodes = languageCodesInCsv.split(',');
364       // Add the UI language to the initial list of languages.  This is to avoid
365       // a bug where the UI language would be removed from the preferred
366       // language list by sync on first login.
367       // See: crosbug.com/14283
368       languageCodes.push(navigator.language);
369       languageCodes = this.filterBadLanguageCodes_(languageCodes);
370       this.load_(languageCodes);
371     },
373     /**
374      * Handles accept languages pref change.
375      * @param {Event} e The change event object.
376      * @private
377      */
378     handleAcceptLanguagesPrefChange_: function(e) {
379       var languageCodesInCsv = e.value.value;
380       var languageCodes = this.filterBadLanguageCodes_(
381           languageCodesInCsv.split(','));
382       this.load_(languageCodes);
383     },
385     /**
386      * Loads given language list.
387      * @param {!Array} languageCodes List of language codes.
388      * @private
389      */
390     load_: function(languageCodes) {
391       // Preserve the original selected index. See comments below.
392       var originalSelectedIndex = (this.selectionModel ?
393                                    this.selectionModel.selectedIndex : -1);
394       this.dataModel = new ArrayDataModel(languageCodes);
395       if (originalSelectedIndex >= 0 &&
396           originalSelectedIndex < this.dataModel.length) {
397         // Restore the original selected index if the selected index is
398         // valid after the data model is loaded. This is neeeded to keep
399         // the selected language after the languge is added or removed.
400         this.selectionModel.selectedIndex = originalSelectedIndex;
401         // The lead index should be updated too.
402         this.selectionModel.leadIndex = originalSelectedIndex;
403       } else if (this.dataModel.length > 0) {
404         // Otherwise, select the first item if it's not empty.
405         // Note that ListSingleSelectionModel won't select an item
406         // automatically, hence we manually select the first item here.
407         this.selectionModel.selectedIndex = 0;
408       }
409     },
411     /**
412      * Saves the preference.
413      */
414     savePreference_: function() {
415       chrome.send('updateLanguageList', [this.dataModel.slice()]);
416       cr.dispatchSimpleEvent(this, 'save');
417     },
419     /**
420      * Filters bad language codes in case bad language codes are
421      * stored in the preference. Removes duplicates as well.
422      * @param {Array} languageCodes List of language codes.
423      * @private
424      */
425     filterBadLanguageCodes_: function(languageCodes) {
426       var filteredLanguageCodes = [];
427       var seen = {};
428       for (var i = 0; i < languageCodes.length; i++) {
429         // Check if the the language code is valid, and not
430         // duplicate. Otherwise, skip it.
431         if (LanguageList.isValidLanguageCode(languageCodes[i]) &&
432             !(languageCodes[i] in seen)) {
433           filteredLanguageCodes.push(languageCodes[i]);
434           seen[languageCodes[i]] = true;
435         }
436       }
437       return filteredLanguageCodes;
438     },
439   };
441   return {
442     LanguageList: LanguageList,
443     LanguageListItem: LanguageListItem
444   };