blob: 6d5817a60532ef44bf772aa5ea66edab718139a0 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
cr.define('options', function() {
/** @const */ var ArrayDataModel = cr.ui.ArrayDataModel;
/** @const */ var DeletableItem = options.DeletableItem;
/** @const */ var DeletableItemList = options.DeletableItemList;
/** @const */ var List = cr.ui.List;
/** @const */ var ListItem = cr.ui.ListItem;
/** @const */ var ListSingleSelectionModel = cr.ui.ListSingleSelectionModel;
/**
* Creates a new Language list item.
* @param {Object} languageInfo The information of the language.
* @constructor
* @extends {DeletableItem.ListItem}
*/
function LanguageListItem(languageInfo) {
var el = cr.doc.createElement('li');
el.__proto__ = LanguageListItem.prototype;
el.language_ = languageInfo;
el.decorate();
return el;
};
LanguageListItem.prototype = {
__proto__: DeletableItem.prototype,
/**
* The language code of this language.
* @type {string}
* @private
*/
languageCode_: null,
/** @override */
decorate: function() {
DeletableItem.prototype.decorate.call(this);
var languageCode = this.language_.code;
var languageOptions = options.LanguageOptions.getInstance();
this.deletable = languageOptions.languageIsDeletable(languageCode);
this.languageCode = languageCode;
this.languageName = cr.doc.createElement('div');
this.languageName.className = 'language-name';
this.languageName.dir = this.language_.textDirection;
this.languageName.textContent = this.language_.displayName;
this.contentElement.appendChild(this.languageName);
this.title = this.language_.nativeDisplayName;
this.draggable = true;
},
};
/**
* Creates a new language list.
* @param {Object=} opt_propertyBag Optional properties.
* @constructor
* @extends {cr.ui.List}
*/
var LanguageList = cr.ui.define('list');
/**
* Gets information of a language from the given language code.
* @param {string} languageCode Language code (ex. "fr").
*/
LanguageList.getLanguageInfoFromLanguageCode = function(languageCode) {
// Build the language code to language info dictionary at first time.
if (!this.languageCodeToLanguageInfo_) {
this.languageCodeToLanguageInfo_ = {};
var languageList = loadTimeData.getValue('languageList');
for (var i = 0; i < languageList.length; i++) {
var languageInfo = languageList[i];
this.languageCodeToLanguageInfo_[languageInfo.code] = languageInfo;
}
}
return this.languageCodeToLanguageInfo_[languageCode];
}
/**
* Returns true if the given language code is valid.
* @param {string} languageCode Language code (ex. "fr").
*/
LanguageList.isValidLanguageCode = function(languageCode) {
// Having the display name for the language code means that the
// language code is valid.
if (LanguageList.getLanguageInfoFromLanguageCode(languageCode)) {
return true;
}
return false;
}
LanguageList.prototype = {
__proto__: DeletableItemList.prototype,
// The list item being dragged.
draggedItem: null,
// The drop position information: "below" or "above".
dropPos: null,
// The preference is a CSV string that describes preferred languages
// in Chrome OS. The language list is used for showing the language
// list in "Language and Input" options page.
preferredLanguagesPref: 'settings.language.preferred_languages',
// The preference is a CSV string that describes accept languages used
// for content negotiation. To be more precise, the list will be used
// in "Accept-Language" header in HTTP requests.
acceptLanguagesPref: 'intl.accept_languages',
/** @override */
decorate: function() {
DeletableItemList.prototype.decorate.call(this);
this.selectionModel = new ListSingleSelectionModel;
// HACK(arv): http://crbug.com/40902
window.addEventListener('resize', this.redraw.bind(this));
// Listen to pref change.
if (cr.isChromeOS) {
Preferences.getInstance().addEventListener(this.preferredLanguagesPref,
this.handlePreferredLanguagesPrefChange_.bind(this));
} else {
Preferences.getInstance().addEventListener(this.acceptLanguagesPref,
this.handleAcceptLanguagesPrefChange_.bind(this));
}
// Listen to drag and drop events.
this.addEventListener('dragstart', this.handleDragStart_.bind(this));
this.addEventListener('dragenter', this.handleDragEnter_.bind(this));
this.addEventListener('dragover', this.handleDragOver_.bind(this));
this.addEventListener('drop', this.handleDrop_.bind(this));
this.addEventListener('dragleave', this.handleDragLeave_.bind(this));
},
createItem: function(languageCode) {
languageInfo = LanguageList.getLanguageInfoFromLanguageCode(languageCode);
return new LanguageListItem(languageInfo);
},
/*
* For each item, determines whether it's deletable.
*/
updateDeletable: function() {
var items = this.items;
for (var i = 0; i < items.length; ++i) {
var item = items[i];
var languageCode = item.languageCode;
var languageOptions = options.LanguageOptions.getInstance();
item.deletable = languageOptions.languageIsDeletable(languageCode);
}
},
/*
* Adds a language to the language list.
* @param {string} languageCode language code (ex. "fr").
*/
addLanguage: function(languageCode) {
// It shouldn't happen but ignore the language code if it's
// null/undefined, or already present.
if (!languageCode || this.dataModel.indexOf(languageCode) >= 0) {
return;
}
this.dataModel.push(languageCode);
// Select the last item, which is the language added.
this.selectionModel.selectedIndex = this.dataModel.length - 1;
this.savePreference_();
},
/*
* Gets the language codes of the currently listed languages.
*/
getLanguageCodes: function() {
return this.dataModel.slice();
},
/*
* Clears the selection
*/
clearSelection: function() {
this.selectionModel.unselectAll();
},
/*
* Gets the language code of the selected language.
*/
getSelectedLanguageCode: function() {
return this.selectedItem;
},
/*
* Selects the language by the given language code.
* @returns {boolean} True if the operation is successful.
*/
selectLanguageByCode: function(languageCode) {
var index = this.dataModel.indexOf(languageCode);
if (index >= 0) {
this.selectionModel.selectedIndex = index;
return true;
}
return false;
},
/** @override */
deleteItemAtIndex: function(index) {
if (index >= 0) {
this.dataModel.splice(index, 1);
// Once the selected item is removed, there will be no selected item.
// Select the item pointed by the lead index.
index = this.selectionModel.leadIndex;
this.savePreference_();
}
return index;
},
/*
* Computes the target item of drop event.
* @param {Event} e The drop or dragover event.
* @private
*/
getTargetFromDropEvent_: function(e) {
var target = e.target;
// e.target may be an inner element of the list item
while (target != null && !(target instanceof ListItem)) {
target = target.parentNode;
}
return target;
},
/*
* Handles the dragstart event.
* @param {Event} e The dragstart event.
* @private
*/
handleDragStart_: function(e) {
var target = e.target;
// ListItem should be the only draggable element type in the page,
// but just in case.
if (target instanceof ListItem) {
this.draggedItem = target;
e.dataTransfer.effectAllowed = 'move';
// We need to put some kind of data in the drag or it will be
// ignored. Use the display name in case the user drags to a text
// field or the desktop.
e.dataTransfer.setData('text/plain', target.title);
}
},
/*
* Handles the dragenter event.
* @param {Event} e The dragenter event.
* @private
*/
handleDragEnter_: function(e) {
e.preventDefault();
},
/*
* Handles the dragover event.
* @param {Event} e The dragover event.
* @private
*/
handleDragOver_: function(e) {
var dropTarget = this.getTargetFromDropEvent_(e);
// Determines whether the drop target is to accept the drop.
// The drop is only successful on another ListItem.
if (!(dropTarget instanceof ListItem) ||
dropTarget == this.draggedItem) {
this.hideDropMarker_();
return;
}
// Compute the drop postion. Should we move the dragged item to
// below or above the drop target?
var rect = dropTarget.getBoundingClientRect();
var dy = e.clientY - rect.top;
var yRatio = dy / rect.height;
var dropPos = yRatio <= .5 ? 'above' : 'below';
this.dropPos = dropPos;
this.showDropMarker_(dropTarget, dropPos);
e.preventDefault();
},
/*
* Handles the drop event.
* @param {Event} e The drop event.
* @private
*/
handleDrop_: function(e) {
var dropTarget = this.getTargetFromDropEvent_(e);
this.hideDropMarker_();
// Delete the language from the original position.
var languageCode = this.draggedItem.languageCode;
var originalIndex = this.dataModel.indexOf(languageCode);
this.dataModel.splice(originalIndex, 1);
// Insert the language to the new position.
var newIndex = this.dataModel.indexOf(dropTarget.languageCode);
if (this.dropPos == 'below')
newIndex += 1;
this.dataModel.splice(newIndex, 0, languageCode);
// The cursor should move to the moved item.
this.selectionModel.selectedIndex = newIndex;
// Save the preference.
this.savePreference_();
},
/*
* Handles the dragleave event.
* @param {Event} e The dragleave event
* @private
*/
handleDragLeave_: function(e) {
this.hideDropMarker_();
},
/*
* Shows and positions the marker to indicate the drop target.
* @param {HTMLElement} target The current target list item of drop
* @param {string} pos 'below' or 'above'
* @private
*/
showDropMarker_: function(target, pos) {
window.clearTimeout(this.hideDropMarkerTimer_);
var marker = $('language-options-list-dropmarker');
var rect = target.getBoundingClientRect();
var markerHeight = 8;
if (pos == 'above') {
marker.style.top = (rect.top - markerHeight / 2) + 'px';
} else {
marker.style.top = (rect.bottom - markerHeight / 2) + 'px';
}
marker.style.width = rect.width + 'px';
marker.style.left = rect.left + 'px';
marker.style.display = 'block';
},
/*
* Hides the drop marker.
* @private
*/
hideDropMarker_: function() {
// Hide the marker in a timeout to reduce flickering as we move between
// valid drop targets.
window.clearTimeout(this.hideDropMarkerTimer_);
this.hideDropMarkerTimer_ = window.setTimeout(function() {
$('language-options-list-dropmarker').style.display = '';
}, 100);
},
/**
* Handles preferred languages pref change.
* @param {Event} e The change event object.
* @private
*/
handlePreferredLanguagesPrefChange_: function(e) {
var languageCodesInCsv = e.value.value;
var languageCodes = languageCodesInCsv.split(',');
// Add the UI language to the initial list of languages. This is to avoid
// a bug where the UI language would be removed from the preferred
// language list by sync on first login.
// See: crosbug.com/14283
languageCodes.push(navigator.language);
languageCodes = this.filterBadLanguageCodes_(languageCodes);
this.load_(languageCodes);
},
/**
* Handles accept languages pref change.
* @param {Event} e The change event object.
* @private
*/
handleAcceptLanguagesPrefChange_: function(e) {
var languageCodesInCsv = e.value.value;
var languageCodes = this.filterBadLanguageCodes_(
languageCodesInCsv.split(','));
this.load_(languageCodes);
},
/**
* Loads given language list.
* @param {Array} languageCodes List of language codes.
* @private
*/
load_: function(languageCodes) {
// Preserve the original selected index. See comments below.
var originalSelectedIndex = (this.selectionModel ?
this.selectionModel.selectedIndex : -1);
this.dataModel = new ArrayDataModel(languageCodes);
if (originalSelectedIndex >= 0 &&
originalSelectedIndex < this.dataModel.length) {
// Restore the original selected index if the selected index is
// valid after the data model is loaded. This is neeeded to keep
// the selected language after the languge is added or removed.
this.selectionModel.selectedIndex = originalSelectedIndex;
// The lead index should be updated too.
this.selectionModel.leadIndex = originalSelectedIndex;
} else if (this.dataModel.length > 0) {
// Otherwise, select the first item if it's not empty.
// Note that ListSingleSelectionModel won't select an item
// automatically, hence we manually select the first item here.
this.selectionModel.selectedIndex = 0;
}
},
/**
* Saves the preference.
*/
savePreference_: function() {
// Encode the language codes into a CSV string.
if (cr.isChromeOS)
Preferences.setStringPref(this.preferredLanguagesPref,
this.dataModel.slice().join(','), true);
// Save the same language list as accept languages preference as
// well, but we need to expand the language list, to make it more
// acceptable. For instance, some web sites don't understand 'en-US'
// but 'en'. See crosbug.com/9884.
var acceptLanguages = this.expandLanguageCodes(this.dataModel.slice());
Preferences.setStringPref(this.acceptLanguagesPref,
acceptLanguages.join(','), true);
cr.dispatchSimpleEvent(this, 'save');
},
/**
* Expands language codes to make these more suitable for Accept-Language.
* Example: ['en-US', 'ja', 'en-CA'] => ['en-US', 'en', 'ja', 'en-CA'].
* 'en' won't appear twice as this function eliminates duplicates.
* @param {Array} languageCodes List of language codes.
* @private
*/
expandLanguageCodes: function(languageCodes) {
var expandedLanguageCodes = [];
var seen = {}; // Used to eliminiate duplicates.
for (var i = 0; i < languageCodes.length; i++) {
var languageCode = languageCodes[i];
if (!(languageCode in seen)) {
expandedLanguageCodes.push(languageCode);
seen[languageCode] = true;
}
var parts = languageCode.split('-');
if (!(parts[0] in seen)) {
expandedLanguageCodes.push(parts[0]);
seen[parts[0]] = true;
}
}
return expandedLanguageCodes;
},
/**
* Filters bad language codes in case bad language codes are
* stored in the preference. Removes duplicates as well.
* @param {Array} languageCodes List of language codes.
* @private
*/
filterBadLanguageCodes_: function(languageCodes) {
var filteredLanguageCodes = [];
var seen = {};
for (var i = 0; i < languageCodes.length; i++) {
// Check if the the language code is valid, and not
// duplicate. Otherwise, skip it.
if (LanguageList.isValidLanguageCode(languageCodes[i]) &&
!(languageCodes[i] in seen)) {
filteredLanguageCodes.push(languageCodes[i]);
seen[languageCodes[i]] = true;
}
}
return filteredLanguageCodes;
},
};
return {
LanguageList: LanguageList,
LanguageListItem: LanguageListItem
};
});