language_list.js revision 72a454cd3513ac24fbdd0e0cb9ad70b86a99b801
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
5cr.define('options', function() {
6  const ArrayDataModel = cr.ui.ArrayDataModel;
7  const LanguageOptions = options.LanguageOptions;
8  const List = cr.ui.List;
9  const ListItem = cr.ui.ListItem;
10  const ListSingleSelectionModel = cr.ui.ListSingleSelectionModel;
11
12  /**
13   * Creates a new language list.
14   * @param {Object=} opt_propertyBag Optional properties.
15   * @constructor
16   * @extends {cr.ui.List}
17   */
18  var LanguageList = cr.ui.define('list');
19
20  /**
21   * Gets display name from the given language code.
22   * @param {string} languageCode Language code (ex. "fr").
23   */
24  LanguageList.getDisplayNameFromLanguageCode = function(languageCode) {
25    // Build the language code to display name dictionary at first time.
26    if (!this.languageCodeToDisplayName_) {
27      this.languageCodeToDisplayName_ = {};
28      var languageList = templateData.languageList;
29      for (var i = 0; i < languageList.length; i++) {
30        var language = languageList[i];
31        this.languageCodeToDisplayName_[language.code] = language.displayName;
32      }
33    }
34
35    return this.languageCodeToDisplayName_[languageCode];
36  }
37
38  /**
39   * Gets native display name from the given language code.
40   * @param {string} languageCode Language code (ex. "fr").
41   */
42  LanguageList.getNativeDisplayNameFromLanguageCode = function(languageCode) {
43    // Build the language code to display name dictionary at first time.
44    if (!this.languageCodeToNativeDisplayName_) {
45      this.languageCodeToNativeDisplayName_ = {};
46      var languageList = templateData.languageList;
47      for (var i = 0; i < languageList.length; i++) {
48        var language = languageList[i];
49        this.languageCodeToNativeDisplayName_[language.code] =
50            language.nativeDisplayName;
51      }
52    }
53
54    return this.languageCodeToNativeDisplayName_[languageCode];
55  }
56
57  /**
58   * Returns true if the given language code is valid.
59   * @param {string} languageCode Language code (ex. "fr").
60   */
61  LanguageList.isValidLanguageCode = function(languageCode) {
62    // Having the display name for the language code means that the
63    // language code is valid.
64    if (LanguageList.getDisplayNameFromLanguageCode(languageCode)) {
65      return true;
66    }
67    return false;
68  }
69
70  LanguageList.prototype = {
71    __proto__: List.prototype,
72
73    // The list item being dragged.
74    draggedItem: null,
75    // The drop position information: "below" or "above".
76    dropPos: null,
77    // The preference is a CSV string that describes preferred languages
78    // in Chrome OS. The language list is used for showing the language
79    // list in "Language and Input" options page.
80    preferredLanguagesPref: 'settings.language.preferred_languages',
81    // The preference is a CSV string that describes accept languages used
82    // for content negotiation. To be more precise, the list will be used
83    // in "Accept-Language" header in HTTP requests.
84    acceptLanguagesPref: 'intl.accept_languages',
85
86    /** @inheritDoc */
87    decorate: function() {
88      List.prototype.decorate.call(this);
89      this.selectionModel = new ListSingleSelectionModel;
90
91      // HACK(arv): http://crbug.com/40902
92      window.addEventListener('resize', this.redraw.bind(this));
93
94      // Listen to pref change.
95      if (cr.isChromeOS) {
96        Preferences.getInstance().addEventListener(this.preferredLanguagesPref,
97            this.handlePreferredLanguagesPrefChange_.bind(this));
98      } else {
99        Preferences.getInstance().addEventListener(this.acceptLanguagesPref,
100            this.handleAcceptLanguagesPrefChange_.bind(this));
101      }
102
103      // Listen to drag and drop events.
104      this.addEventListener('dragstart', this.handleDragStart_.bind(this));
105      this.addEventListener('dragenter', this.handleDragEnter_.bind(this));
106      this.addEventListener('dragover', this.handleDragOver_.bind(this));
107      this.addEventListener('drop', this.handleDrop_.bind(this));
108    },
109
110    createItem: function(languageCode) {
111      var languageDisplayName =
112          LanguageList.getDisplayNameFromLanguageCode(languageCode);
113      var languageNativeDisplayName =
114          LanguageList.getNativeDisplayNameFromLanguageCode(languageCode);
115      return new ListItem({
116        label: languageDisplayName,
117        draggable: true,
118        languageCode: languageCode,
119        title: languageNativeDisplayName  // Show native name as tooltip.
120      });
121    },
122
123    /*
124     * Adds a language to the language list.
125     * @param {string} languageCode language code (ex. "fr").
126     */
127    addLanguage: function(languageCode) {
128      // It shouldn't happen but ignore the language code if it's
129      // null/undefined, or already present.
130      if (!languageCode || this.dataModel.indexOf(languageCode) >= 0) {
131        return;
132      }
133      this.dataModel.push(languageCode);
134      // Select the last item, which is the language added.
135      this.selectionModel.selectedIndex = this.dataModel.length - 1;
136
137      this.savePreference_();
138    },
139
140    /*
141     * Gets the language codes of the currently listed languages.
142     */
143    getLanguageCodes: function() {
144      return this.dataModel.slice();
145    },
146
147    /*
148     * Gets the language code of the selected language.
149     */
150    getSelectedLanguageCode: function() {
151      return this.selectedItem;
152    },
153
154    /*
155     * Selects the language by the given language code.
156     * @returns {boolean} True if the operation is successful.
157     */
158    selectLanguageByCode: function(languageCode) {
159      var index = this.dataModel.indexOf(languageCode);
160      if (index >= 0) {
161        this.selectionModel.selectedIndex = index;
162        return true;
163      }
164      return false;
165    },
166
167    /*
168     * Removes the currently selected language.
169     */
170    removeSelectedLanguage: function() {
171      if (this.selectionModel.selectedIndex >= 0) {
172        this.dataModel.splice(this.selectionModel.selectedIndex, 1);
173        // Once the selected item is removed, there will be no selected item.
174        // Select the item pointed by the lead index.
175        this.selectionModel.selectedIndex = this.selectionModel.leadIndex;
176        this.savePreference_();
177      }
178    },
179
180    /*
181     * Handles the dragstart event.
182     * @param {Event} e The dragstart event.
183     * @private
184     */
185    handleDragStart_: function(e) {
186      var target = e.target;
187      // ListItem should be the only draggable element type in the page,
188      // but just in case.
189      if (target instanceof ListItem) {
190        this.draggedItem = target;
191        e.dataTransfer.effectAllowed = 'move';
192        // We need to put some kind of data in the drag or it will be
193        // ignored.  Use the display name in case the user drags to a text
194        // field or the desktop.
195        e.dataTransfer.setData('text/plain', target.title);
196      }
197    },
198
199    /*
200     * Handles the dragenter event.
201     * @param {Event} e The dragenter event.
202     * @private
203     */
204    handleDragEnter_: function(e) {
205      e.preventDefault();
206    },
207
208    /*
209     * Handles the dragover event.
210     * @param {Event} e The dragover event.
211     * @private
212     */
213    handleDragOver_: function(e) {
214      var dropTarget = e.target;
215      // Determins whether the drop target is to accept the drop.
216      // The drop is only successful on another ListItem.
217      if (!(dropTarget instanceof ListItem) ||
218          dropTarget == this.draggedItem) {
219        return;
220      }
221      // Compute the drop postion. Should we move the dragged item to
222      // below or above the drop target?
223      var rect = dropTarget.getBoundingClientRect();
224      var dy = e.clientY - rect.top;
225      var yRatio = dy / rect.height;
226      var dropPos = yRatio <= .5 ? 'above' : 'below';
227      this.dropPos = dropPos;
228      e.preventDefault();
229      // TODO(satorux): Show the drop marker just like the bookmark manager.
230    },
231
232    /*
233     * Handles the drop event.
234     * @param {Event} e The drop event.
235     * @private
236     */
237    handleDrop_: function(e) {
238      var dropTarget = e.target;
239
240      // Delete the language from the original position.
241      var languageCode = this.draggedItem.languageCode;
242      var originalIndex = this.dataModel.indexOf(languageCode);
243      this.dataModel.splice(originalIndex, 1);
244      // Insert the language to the new position.
245      var newIndex = this.dataModel.indexOf(dropTarget.languageCode);
246      if (this.dropPos == 'below')
247        newIndex += 1;
248      this.dataModel.splice(newIndex, 0, languageCode);
249      // The cursor should move to the moved item.
250      this.selectionModel.selectedIndex = newIndex;
251      // Save the preference.
252      this.savePreference_();
253    },
254
255    /**
256     * Handles preferred languages pref change.
257     * @param {Event} e The change event object.
258     * @private
259     */
260    handlePreferredLanguagesPrefChange_: function(e) {
261      var languageCodesInCsv = e.value.value;
262      var languageCodes = this.filterBadLanguageCodes_(
263          languageCodesInCsv.split(','));
264      this.load_(languageCodes);
265    },
266
267    /**
268     * Handles accept languages pref change.
269     * @param {Event} e The change event object.
270     * @private
271     */
272    handleAcceptLanguagesPrefChange_: function(e) {
273      var languageCodesInCsv = e.value.value;
274      var languageCodes = this.filterBadLanguageCodes_(
275          languageCodesInCsv.split(','));
276      this.load_(languageCodes);
277    },
278
279    /**
280     * Loads given language list.
281     * @param {Array} languageCodes List of language codes.
282     * @private
283     */
284    load_: function(languageCodes) {
285      // Preserve the original selected index. See comments below.
286      var originalSelectedIndex = (this.selectionModel ?
287                                   this.selectionModel.selectedIndex : -1);
288      this.dataModel = new ArrayDataModel(languageCodes);
289      if (originalSelectedIndex >= 0 &&
290          originalSelectedIndex < this.dataModel.length) {
291        // Restore the original selected index if the selected index is
292        // valid after the data model is loaded. This is neeeded to keep
293        // the selected language after the languge is added or removed.
294        this.selectionModel.selectedIndex = originalSelectedIndex;
295        // The lead index should be updated too.
296        this.selectionModel.leadIndex = originalSelectedIndex;
297      } else if (this.dataModel.length > 0){
298        // Otherwise, select the first item if it's not empty.
299        // Note that ListSingleSelectionModel won't select an item
300        // automatically, hence we manually select the first item here.
301        this.selectionModel.selectedIndex = 0;
302      }
303    },
304
305    /**
306     * Saves the preference.
307     */
308    savePreference_: function() {
309      // Encode the language codes into a CSV string.
310      if (cr.isChromeOS)
311        Preferences.setStringPref(this.preferredLanguagesPref,
312                                  this.dataModel.slice().join(','));
313      // Save the same language list as accept languages preference as
314      // well, but we need to expand the language list, to make it more
315      // acceptable. For instance, some web sites don't understand 'en-US'
316      // but 'en'. See crosbug.com/9884.
317      var acceptLanguages = this.expandLanguageCodes(this.dataModel.slice());
318      Preferences.setStringPref(this.acceptLanguagesPref,
319                                acceptLanguages.join(','));
320      cr.dispatchSimpleEvent(this, 'save');
321    },
322
323    /**
324     * Expands language codes to make these more suitable for Accept-Language.
325     * Example: ['en-US', 'ja', 'en-CA'] => ['en-US', 'en', 'ja', 'en-CA'].
326     * 'en' won't appear twice as this function eliminates duplicates.
327     * @param {Array} languageCodes List of language codes.
328     * @private
329     */
330    expandLanguageCodes: function(languageCodes) {
331      var expandedLanguageCodes = [];
332      var seen = {};  // Used to eliminiate duplicates.
333      for (var i = 0; i < languageCodes.length; i++) {
334        var languageCode = languageCodes[i];
335        if (!(languageCode in seen)) {
336          expandedLanguageCodes.push(languageCode);
337          seen[languageCode] = true;
338        }
339        var parts = languageCode.split('-');
340        if (!(parts[0] in seen)) {
341          expandedLanguageCodes.push(parts[0]);
342          seen[parts[0]] = true;
343        }
344      }
345      return expandedLanguageCodes;
346    },
347
348    /**
349     * Filters bad language codes in case bad language codes are
350     * stored in the preference. Removes duplicates as well.
351     * @param {Array} languageCodes List of language codes.
352     * @private
353     */
354    filterBadLanguageCodes_: function(languageCodes) {
355      var filteredLanguageCodes = [];
356      var seen = {};
357      for (var i = 0; i < languageCodes.length; i++) {
358        // Check if the the language code is valid, and not
359        // duplicate. Otherwise, skip it.
360        if (LanguageList.isValidLanguageCode(languageCodes[i]) &&
361            !(languageCodes[i] in seen)) {
362          filteredLanguageCodes.push(languageCodes[i]);
363          seen[languageCodes[i]] = true;
364        }
365      }
366      return filteredLanguageCodes;
367    },
368  };
369
370  return {
371    LanguageList: LanguageList
372  };
373});
374