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