• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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