- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / options / language_list.js
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
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;
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 });