forked from mirrors/gecko-dev
The search handler was being called when focusing the menuitem with the keyboard on Windows. This didn't provide a good experience and left the popup open once the search started. Ensure the popup is always shown when using the keyboard and don't trigger the search until the popup is closed. Differential Revision: https://phabricator.services.mozilla.com/D12324 --HG-- extra : moz-landing-system : lando
518 lines
16 KiB
JavaScript
518 lines
16 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
/* import-globals-from ../../../toolkit/content/preferencesBindings.js */
|
|
|
|
ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
|
|
ChromeUtils.defineModuleGetter(this, "AddonManager",
|
|
"resource://gre/modules/AddonManager.jsm");
|
|
ChromeUtils.defineModuleGetter(this, "AddonRepository",
|
|
"resource://gre/modules/addons/AddonRepository.jsm");
|
|
ChromeUtils.defineModuleGetter(this, "RemoteSettings",
|
|
"resource://services-settings/remote-settings.js");
|
|
ChromeUtils.defineModuleGetter(this, "SelectionChangedMenulist",
|
|
"resource:///modules/SelectionChangedMenulist.jsm");
|
|
|
|
async function installFromUrl(url, hash) {
|
|
let install = await AddonManager.getInstallForURL(
|
|
url, "application/x-xpinstall", hash);
|
|
return install.install();
|
|
}
|
|
|
|
async function dictionaryIdsForLocale(locale) {
|
|
let entries = await RemoteSettings("language-dictionaries").get({
|
|
filters: {id: locale},
|
|
});
|
|
if (entries.length > 0) {
|
|
return entries[0].dictionaries;
|
|
}
|
|
return [];
|
|
}
|
|
|
|
class OrderedListBox {
|
|
constructor({richlistbox, upButton, downButton, removeButton, onRemove}) {
|
|
this.richlistbox = richlistbox;
|
|
this.upButton = upButton;
|
|
this.downButton = downButton;
|
|
this.removeButton = removeButton;
|
|
this.onRemove = onRemove;
|
|
|
|
this.items = [];
|
|
|
|
this.richlistbox.addEventListener("select", () => this.setButtonState());
|
|
this.upButton.addEventListener("command", () => this.moveUp());
|
|
this.downButton.addEventListener("command", () => this.moveDown());
|
|
this.removeButton.addEventListener("command", () => this.removeItem());
|
|
}
|
|
|
|
get selectedItem() {
|
|
return this.items[this.richlistbox.selectedIndex];
|
|
}
|
|
|
|
setButtonState() {
|
|
let {upButton, downButton, removeButton} = this;
|
|
let {selectedIndex, itemCount} = this.richlistbox;
|
|
upButton.disabled = selectedIndex == 0;
|
|
downButton.disabled = selectedIndex == itemCount - 1;
|
|
removeButton.disabled = itemCount == 1 || !this.selectedItem.canRemove;
|
|
}
|
|
|
|
moveUp() {
|
|
let {selectedIndex} = this.richlistbox;
|
|
if (selectedIndex == 0) {
|
|
return;
|
|
}
|
|
let {items} = this;
|
|
let selectedItem = items[selectedIndex];
|
|
let prevItem = items[selectedIndex - 1];
|
|
items[selectedIndex - 1] = items[selectedIndex];
|
|
items[selectedIndex] = prevItem;
|
|
let prevEl = document.getElementById(prevItem.id);
|
|
let selectedEl = document.getElementById(selectedItem.id);
|
|
this.richlistbox.insertBefore(selectedEl, prevEl);
|
|
this.richlistbox.ensureElementIsVisible(selectedEl);
|
|
this.setButtonState();
|
|
}
|
|
|
|
moveDown() {
|
|
let {selectedIndex} = this.richlistbox;
|
|
if (selectedIndex == this.items.length - 1) {
|
|
return;
|
|
}
|
|
let {items} = this;
|
|
let selectedItem = items[selectedIndex];
|
|
let nextItem = items[selectedIndex + 1];
|
|
items[selectedIndex + 1] = items[selectedIndex];
|
|
items[selectedIndex] = nextItem;
|
|
let nextEl = document.getElementById(nextItem.id);
|
|
let selectedEl = document.getElementById(selectedItem.id);
|
|
this.richlistbox.insertBefore(nextEl, selectedEl);
|
|
this.richlistbox.ensureElementIsVisible(selectedEl);
|
|
this.setButtonState();
|
|
}
|
|
|
|
removeItem() {
|
|
let {selectedIndex} = this.richlistbox;
|
|
|
|
if (selectedIndex == -1) {
|
|
return;
|
|
}
|
|
|
|
let [item] = this.items.splice(selectedIndex, 1);
|
|
this.richlistbox.selectedItem.remove();
|
|
this.richlistbox.selectedIndex = Math.min(
|
|
selectedIndex, this.richlistbox.itemCount - 1);
|
|
this.richlistbox.ensureElementIsVisible(this.richlistbox.selectedItem);
|
|
this.onRemove(item);
|
|
}
|
|
|
|
setItems(items) {
|
|
this.items = items;
|
|
this.populate();
|
|
this.setButtonState();
|
|
}
|
|
|
|
/**
|
|
* Add an item to the top of the ordered list.
|
|
*
|
|
* @param {object} item The item to insert.
|
|
*/
|
|
addItem(item) {
|
|
this.items.unshift(item);
|
|
this.richlistbox.insertBefore(
|
|
this.createItem(item),
|
|
this.richlistbox.firstElementChild);
|
|
this.richlistbox.selectedIndex = 0;
|
|
this.richlistbox.ensureElementIsVisible(this.richlistbox.selectedItem);
|
|
}
|
|
|
|
populate() {
|
|
this.richlistbox.textContent = "";
|
|
|
|
let frag = document.createDocumentFragment();
|
|
for (let item of this.items) {
|
|
frag.appendChild(this.createItem(item));
|
|
}
|
|
this.richlistbox.appendChild(frag);
|
|
|
|
this.richlistbox.selectedIndex = 0;
|
|
this.richlistbox.ensureElementIsVisible(this.richlistbox.selectedItem);
|
|
}
|
|
|
|
createItem({id, label, value}) {
|
|
let listitem = document.createElement("richlistitem");
|
|
listitem.id = id;
|
|
listitem.setAttribute("value", value);
|
|
|
|
let labelEl = document.createElement("label");
|
|
labelEl.textContent = label;
|
|
listitem.appendChild(labelEl);
|
|
|
|
return listitem;
|
|
}
|
|
}
|
|
|
|
class SortedItemSelectList {
|
|
constructor({menulist, button, onSelect, onChange, compareFn}) {
|
|
this.menulist = menulist;
|
|
this.popup = menulist.firstElementChild;
|
|
this.button = button;
|
|
this.compareFn = compareFn;
|
|
this.items = [];
|
|
|
|
// This will register the "command" listener.
|
|
new SelectionChangedMenulist(this.menulist, () => {
|
|
button.disabled = !menulist.selectedItem;
|
|
if (menulist.selectedItem) {
|
|
onChange(this.items[menulist.selectedIndex]);
|
|
}
|
|
});
|
|
button.addEventListener("command", () => {
|
|
if (!menulist.selectedItem) return;
|
|
|
|
let [item] = this.items.splice(menulist.selectedIndex, 1);
|
|
menulist.selectedItem.remove();
|
|
menulist.setAttribute("label", menulist.getAttribute("placeholder"));
|
|
button.disabled = true;
|
|
menulist.disabled = menulist.itemCount == 0;
|
|
menulist.selectedIndex = -1;
|
|
|
|
onSelect(item);
|
|
});
|
|
}
|
|
|
|
setItems(items) {
|
|
this.items = items.sort(this.compareFn);
|
|
this.populate();
|
|
}
|
|
|
|
populate() {
|
|
let {button, items, menulist, popup} = this;
|
|
popup.textContent = "";
|
|
|
|
let frag = document.createDocumentFragment();
|
|
for (let item of items) {
|
|
frag.appendChild(this.createItem(item));
|
|
}
|
|
popup.appendChild(frag);
|
|
|
|
menulist.setAttribute("label", menulist.getAttribute("placeholder"));
|
|
menulist.disabled = menulist.itemCount == 0;
|
|
menulist.selectedIndex = -1;
|
|
button.disabled = true;
|
|
}
|
|
|
|
/**
|
|
* Add an item to the list sorted by the label.
|
|
*
|
|
* @param {object} item The item to insert.
|
|
*/
|
|
addItem(item) {
|
|
let {compareFn, items, menulist, popup} = this;
|
|
|
|
// Find the index of the item to insert before.
|
|
let i = items.findIndex(el => compareFn(el, item) >= 0);
|
|
items.splice(i, 0, item);
|
|
popup.insertBefore(this.createItem(item), menulist.getItemAtIndex(i));
|
|
|
|
menulist.disabled = menulist.itemCount == 0;
|
|
}
|
|
|
|
createItem({label, value, className, disabled}) {
|
|
let item = document.createElement("menuitem");
|
|
item.value = value;
|
|
item.setAttribute("label", label);
|
|
if (className)
|
|
item.classList.add(className);
|
|
if (disabled)
|
|
item.setAttribute("disabled", "true");
|
|
return item;
|
|
}
|
|
|
|
/**
|
|
* Disable the inputs and set a data-l10n-id on the menulist. This can be
|
|
* reverted with `enableWithMessageId()`.
|
|
*/
|
|
disableWithMessageId(messageId) {
|
|
this.menulist.setAttribute("data-l10n-id", messageId);
|
|
this.menulist.setAttribute("image", "chrome://browser/skin/tabbrowser/tab-connecting.png");
|
|
this.menulist.disabled = true;
|
|
this.button.disabled = true;
|
|
}
|
|
|
|
/**
|
|
* Enable the inputs and set a data-l10n-id on the menulist. This can be
|
|
* reverted with `disableWithMessageId()`.
|
|
*/
|
|
enableWithMessageId(messageId) {
|
|
this.menulist.setAttribute("data-l10n-id", messageId);
|
|
this.menulist.removeAttribute("image");
|
|
this.menulist.disabled = this.menulist.itemCount == 0;
|
|
this.button.disabled = !this.menulist.selectedItem;
|
|
}
|
|
}
|
|
|
|
function getLocaleDisplayInfo(localeCodes) {
|
|
let availableLocales = new Set(Services.locale.availableLocales);
|
|
let packagedLocales = new Set(Services.locale.packagedLocales);
|
|
let localeNames = Services.intl.getLocaleDisplayNames(undefined, localeCodes);
|
|
return localeCodes.map((code, i) => {
|
|
return {
|
|
id: "locale-" + code,
|
|
label: localeNames[i],
|
|
value: code,
|
|
canRemove: !packagedLocales.has(code),
|
|
installed: availableLocales.has(code),
|
|
};
|
|
});
|
|
}
|
|
|
|
function compareItems(a, b) {
|
|
// Sort by installed.
|
|
if (a.installed != b.installed) {
|
|
return a.installed ? -1 : 1;
|
|
|
|
// The search label is always last.
|
|
} else if (a.value == "search") {
|
|
return 1;
|
|
} else if (b.value == "search") {
|
|
return -1;
|
|
|
|
// If both items are locales, sort by label.
|
|
} else if (a.value && b.value) {
|
|
return a.label.localeCompare(b.label);
|
|
|
|
// One of them is a label, put it first.
|
|
} else if (a.value) {
|
|
return 1;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
var gBrowserLanguagesDialog = {
|
|
_availableLocales: null,
|
|
_requestedLocales: null,
|
|
requestedLocales: null,
|
|
|
|
get downloadEnabled() {
|
|
// Downloading langpacks isn't always supported, check the pref.
|
|
return Services.prefs.getBoolPref("intl.multilingual.downloadEnabled");
|
|
},
|
|
|
|
beforeAccept() {
|
|
this.requestedLocales = this.getRequestedLocales();
|
|
return true;
|
|
},
|
|
|
|
async onLoad() {
|
|
// Maintain the previously requested locales even if we cancel out.
|
|
let {requesting, search} = window.arguments[0] || {};
|
|
this.requestedLocales = requesting;
|
|
|
|
let requested = this.requestedLocales || Services.locale.requestedLocales;
|
|
let requestedSet = new Set(requested);
|
|
let available = Services.locale.availableLocales
|
|
.filter(locale => !requestedSet.has(locale));
|
|
|
|
this.initRequestedLocales(requested);
|
|
await this.initAvailableLocales(available, search);
|
|
this.initialized = true;
|
|
},
|
|
|
|
initRequestedLocales(requested) {
|
|
this._requestedLocales = new OrderedListBox({
|
|
richlistbox: document.getElementById("requestedLocales"),
|
|
upButton: document.getElementById("up"),
|
|
downButton: document.getElementById("down"),
|
|
removeButton: document.getElementById("remove"),
|
|
onRemove: (item) => this.requestedLocaleRemoved(item),
|
|
});
|
|
this._requestedLocales.setItems(getLocaleDisplayInfo(requested));
|
|
},
|
|
|
|
async initAvailableLocales(available, search) {
|
|
this._availableLocales = new SortedItemSelectList({
|
|
menulist: document.getElementById("availableLocales"),
|
|
button: document.getElementById("add"),
|
|
compareFn: compareItems,
|
|
onSelect: (item) => this.availableLanguageSelected(item),
|
|
onChange: (item) => {
|
|
this.hideError();
|
|
if (item.value == "search") {
|
|
this.loadLocalesFromAMO();
|
|
}
|
|
},
|
|
});
|
|
|
|
// Populate the list with the installed locales even if the user is
|
|
// searching in case the download fails.
|
|
await this.loadLocalesFromInstalled(available);
|
|
|
|
// If the user opened this from the "Search for more languages" option,
|
|
// search AMO for available locales.
|
|
if (search) {
|
|
return this.loadLocalesFromAMO();
|
|
}
|
|
|
|
return undefined;
|
|
},
|
|
|
|
async loadLocalesFromAMO() {
|
|
if (!this.downloadEnabled) {
|
|
return;
|
|
}
|
|
|
|
// Disable the dropdown while we hit the network.
|
|
this._availableLocales.disableWithMessageId("browser-languages-searching");
|
|
|
|
// Fetch the available langpacks from AMO.
|
|
let availableLangpacks;
|
|
try {
|
|
availableLangpacks = await AddonRepository.getAvailableLangpacks();
|
|
} catch (e) {
|
|
this.showError();
|
|
return;
|
|
}
|
|
|
|
// Store the available langpack info for later use.
|
|
this.availableLangpacks = new Map();
|
|
for (let {target_locale, url, hash} of availableLangpacks) {
|
|
this.availableLangpacks.set(target_locale, {url, hash});
|
|
}
|
|
|
|
// Create a list of installed locales to hide.
|
|
let installedLocales = new Set([
|
|
...Services.locale.requestedLocales,
|
|
...Services.locale.availableLocales,
|
|
]);
|
|
|
|
let availableLocales = availableLangpacks
|
|
.filter(({target_locale}) => !installedLocales.has(target_locale))
|
|
.map(lang => lang.target_locale);
|
|
let availableItems = getLocaleDisplayInfo(availableLocales);
|
|
availableItems.push({
|
|
label: await document.l10n.formatValue("browser-languages-available-label"),
|
|
className: "label-item",
|
|
disabled: true,
|
|
installed: false,
|
|
});
|
|
let items = this._availableLocales.items;
|
|
// Drop the search item.
|
|
items.pop();
|
|
items = items.concat(availableItems);
|
|
|
|
// Update the dropdown and enable it again.
|
|
this._availableLocales.setItems(items);
|
|
this._availableLocales.enableWithMessageId("browser-languages-select-language");
|
|
},
|
|
|
|
async loadLocalesFromInstalled(available) {
|
|
let items;
|
|
if (available.length > 0) {
|
|
items = getLocaleDisplayInfo(available);
|
|
items.push(await this.createInstalledLabel());
|
|
} else {
|
|
items = [];
|
|
}
|
|
if (this.downloadEnabled) {
|
|
items.push({
|
|
label: await document.l10n.formatValue("browser-languages-search"),
|
|
value: "search",
|
|
});
|
|
}
|
|
this._availableLocales.setItems(items);
|
|
},
|
|
|
|
async availableLanguageSelected(item) {
|
|
if (Services.locale.availableLocales.includes(item.value)) {
|
|
this.requestLocalLanguage(item);
|
|
} else if (this.availableLangpacks.has(item.value)) {
|
|
await this.requestRemoteLanguage(item);
|
|
} else {
|
|
this.showError();
|
|
}
|
|
},
|
|
|
|
requestLocalLanguage(item, available) {
|
|
this._requestedLocales.addItem(item);
|
|
let requestedCount = this._requestedLocales.items.length;
|
|
let availableCount = Services.locale.availableLocales.length;
|
|
if (requestedCount == availableCount) {
|
|
// Remove the installed label, they're all installed.
|
|
this._availableLocales.items.shift();
|
|
this._availableLocales.setItems(this._availableLocales.items);
|
|
}
|
|
},
|
|
|
|
async requestRemoteLanguage(item) {
|
|
this._availableLocales.disableWithMessageId(
|
|
"browser-languages-downloading");
|
|
|
|
let {url, hash} = this.availableLangpacks.get(item.value);
|
|
|
|
try {
|
|
await installFromUrl(url, hash);
|
|
} catch (e) {
|
|
this.showError();
|
|
return;
|
|
}
|
|
|
|
item.installed = true;
|
|
this._requestedLocales.addItem(item);
|
|
this._availableLocales.enableWithMessageId(
|
|
"browser-languages-select-language");
|
|
|
|
// This is an async task that will install the recommended dictionaries for
|
|
// this locale. This will fail silently at least until a management UI is
|
|
// added in bug 1493705.
|
|
this.installDictionariesForLanguage(item.value);
|
|
},
|
|
|
|
async installDictionariesForLanguage(locale) {
|
|
try {
|
|
let ids = await dictionaryIdsForLocale(locale);
|
|
let addonInfos = await AddonRepository.getAddonsByIDs(ids);
|
|
await Promise.all(addonInfos.map(
|
|
info => installFromUrl(info.sourceURI.spec)));
|
|
} catch (e) {
|
|
Cu.reportError(e);
|
|
}
|
|
},
|
|
|
|
showError() {
|
|
document.querySelectorAll(".warning-message-separator")
|
|
.forEach(separator => separator.classList.add("thin"));
|
|
document.getElementById("warning-message").hidden = false;
|
|
this._availableLocales.enableWithMessageId("browser-languages-select-language");
|
|
},
|
|
|
|
hideError() {
|
|
document.querySelectorAll(".warning-message-separator")
|
|
.forEach(separator => separator.classList.remove("thin"));
|
|
document.getElementById("warning-message").hidden = true;
|
|
},
|
|
|
|
getRequestedLocales() {
|
|
return this._requestedLocales.items.map(item => item.value);
|
|
},
|
|
|
|
async requestedLocaleRemoved(item) {
|
|
this._availableLocales.addItem(item);
|
|
|
|
// If the item we added is at the top of the list, it needs the label.
|
|
if (this._availableLocales.items[0] == item) {
|
|
this._availableLocales.addItem(await this.createInstalledLabel());
|
|
}
|
|
},
|
|
|
|
async createInstalledLabel() {
|
|
return {
|
|
label: await document.l10n.formatValue("browser-languages-installed-label"),
|
|
className: "label-item",
|
|
disabled: true,
|
|
installed: true,
|
|
};
|
|
},
|
|
};
|