gecko-dev/browser/components/preferences/in-content/search.js

688 строки
22 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 extensionControlled.js */
/* import-globals-from preferences.js */
ChromeUtils.defineModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
ChromeUtils.defineModuleGetter(this, "ExtensionSettingsStore",
"resource://gre/modules/ExtensionSettingsStore.jsm");
Preferences.addAll([
{ id: "browser.search.suggest.enabled", type: "bool" },
{ id: "browser.urlbar.suggest.searches", type: "bool" },
{ id: "browser.search.hiddenOneOffs", type: "unichar" },
{ id: "browser.search.widget.inNavBar", type: "bool" },
{ id: "browser.urlbar.matchBuckets", type: "string" },
]);
const ENGINE_FLAVOR = "text/x-moz-search-engine";
const SEARCH_TYPE = "default_search";
const SEARCH_KEY = "defaultSearch";
var gEngineView = null;
var gSearchPane = {
/**
* Initialize autocomplete to ensure prefs are in sync.
*/
_initAutocomplete() {
Cc["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"]
.getService(Ci.mozIPlacesAutoComplete);
},
init() {
gEngineView = new EngineView(new EngineStore());
document.getElementById("engineList").view = gEngineView;
this.buildDefaultEngineDropDown();
if (Services.policies &&
!Services.policies.isAllowed("installSearchEngine")) {
document.getElementById("addEnginesBox").hidden = true;
} else {
let addEnginesLink = document.getElementById("addEngines");
let searchEnginesURL = Services.wm.getMostRecentWindow("navigator:browser")
.BrowserSearch.searchEnginesURL;
addEnginesLink.setAttribute("href", searchEnginesURL);
}
window.addEventListener("click", this);
window.addEventListener("command", this);
window.addEventListener("dragstart", this);
window.addEventListener("keypress", this);
window.addEventListener("select", this);
window.addEventListener("blur", this, true);
Services.obs.addObserver(this, "browser-search-engine-modified");
window.addEventListener("unload", () => {
Services.obs.removeObserver(this, "browser-search-engine-modified");
});
this._initAutocomplete();
let suggestsPref = Preferences.get("browser.search.suggest.enabled");
let urlbarSuggestsPref = Preferences.get("browser.urlbar.suggest.searches");
let updateSuggestionCheckboxes = this._updateSuggestionCheckboxes.bind(this);
suggestsPref.on("change", updateSuggestionCheckboxes);
urlbarSuggestsPref.on("change", updateSuggestionCheckboxes);
let urlbarSuggests = document.getElementById("urlBarSuggestion");
urlbarSuggests.addEventListener("command", () => {
urlbarSuggestsPref.value = urlbarSuggests.checked;
});
this._initShowSearchSuggestionsFirst();
this._updateSuggestionCheckboxes();
},
_initShowSearchSuggestionsFirst() {
this._urlbarSuggestionsPosPref = Preferences.get("browser.urlbar.matchBuckets");
let checkbox =
document.getElementById("showSearchSuggestionsFirstCheckbox");
this._urlbarSuggestionsPosPref.on("change", () => {
this._syncFromShowSearchSuggestionsFirstPref(checkbox);
});
this._syncFromShowSearchSuggestionsFirstPref(checkbox);
checkbox.addEventListener("command", () => {
this._syncToShowSearchSuggestionsFirstPref(checkbox.checked);
});
},
_syncFromShowSearchSuggestionsFirstPref(checkbox) {
if (!this._urlbarSuggestionsPosPref.value) {
// The pref is cleared, meaning search suggestions are shown first.
checkbox.checked = true;
return;
}
// The pref has a value. If the first bucket in the pref is search
// suggestions, then check the checkbox.
let buckets = PlacesUtils.convertMatchBucketsStringToArray(this._urlbarSuggestionsPosPref.value);
checkbox.checked = buckets[0] && buckets[0][0] == "suggestion";
},
_syncToShowSearchSuggestionsFirstPref(checked) {
if (checked) {
// Show search suggestions first, so clear the pref since that's the
// default.
this._urlbarSuggestionsPosPref.reset();
return;
}
// Show history first.
this._urlbarSuggestionsPosPref.value = "general:5,suggestion:Infinity";
},
_updateSuggestionCheckboxes() {
let suggestsPref = Preferences.get("browser.search.suggest.enabled");
let permanentPB =
Services.prefs.getBoolPref("browser.privatebrowsing.autostart");
let urlbarSuggests = document.getElementById("urlBarSuggestion");
let positionCheckbox =
document.getElementById("showSearchSuggestionsFirstCheckbox");
urlbarSuggests.disabled = !suggestsPref.value || permanentPB;
let urlbarSuggestsPref = Preferences.get("browser.urlbar.suggest.searches");
urlbarSuggests.checked = urlbarSuggestsPref.value;
if (urlbarSuggests.disabled) {
urlbarSuggests.checked = false;
}
if (urlbarSuggests.checked) {
positionCheckbox.disabled = false;
this._syncFromShowSearchSuggestionsFirstPref(positionCheckbox);
} else {
positionCheckbox.disabled = true;
positionCheckbox.checked = false;
}
let permanentPBLabel =
document.getElementById("urlBarSuggestionPermanentPBLabel");
permanentPBLabel.hidden = urlbarSuggests.hidden || !permanentPB;
},
buildDefaultEngineDropDown() {
// This is called each time something affects the list of engines.
let list = document.getElementById("defaultEngine");
// Set selection to the current default engine.
let currentEngine = Services.search.defaultEngine.name;
// If the current engine isn't in the list any more, select the first item.
let engines = gEngineView._engineStore._engines;
if (!engines.some(e => e.name == currentEngine))
currentEngine = engines[0].name;
// Now clean-up and rebuild the list.
list.removeAllItems();
gEngineView._engineStore._engines.forEach(e => {
let item = list.appendItem(e.name);
item.setAttribute("class", "menuitem-iconic searchengine-menuitem menuitem-with-favicon");
if (e.iconURI) {
item.setAttribute("image", e.iconURI.spec);
}
item.engine = e;
if (e.name == currentEngine)
list.selectedItem = item;
});
handleControllingExtension(SEARCH_TYPE, SEARCH_KEY);
let searchEngineListener = {
observe(subject, topic, data) {
handleControllingExtension(SEARCH_TYPE, SEARCH_KEY);
},
};
Services.obs.addObserver(searchEngineListener, "browser-search-engine-modified");
window.addEventListener("unload", () => {
Services.obs.removeObserver(searchEngineListener, "browser-search-engine-modified");
});
},
handleEvent(aEvent) {
switch (aEvent.type) {
case "click":
if (aEvent.target.id != "engineChildren" &&
!aEvent.target.classList.contains("searchEngineAction")) {
let engineList = document.getElementById("engineList");
// We don't want to toggle off selection while editing keyword
// so proceed only when the input field is hidden.
// We need to check that engineList.view is defined here
// because the "click" event listener is on <window> and the
// view might have been destroyed if the pane has been navigated
// away from.
if (engineList.inputField.hidden && engineList.view) {
let selection = engineList.view.selection;
if (selection.count > 0) {
selection.toggleSelect(selection.currentIndex);
}
engineList.blur();
}
}
break;
case "command":
switch (aEvent.target.id) {
case "":
if (aEvent.target.parentNode &&
aEvent.target.parentNode.parentNode &&
aEvent.target.parentNode.parentNode.id == "defaultEngine") {
gSearchPane.setDefaultEngine();
}
break;
case "restoreDefaultSearchEngines":
gSearchPane.onRestoreDefaults();
break;
case "removeEngineButton":
Services.search.removeEngine(gEngineView.selectedEngine.originalEngine);
break;
}
break;
case "dragstart":
if (aEvent.target.id == "engineChildren") {
onDragEngineStart(aEvent);
}
break;
case "keypress":
if (aEvent.target.id == "engineList") {
gSearchPane.onTreeKeyPress(aEvent);
}
break;
case "select":
if (aEvent.target.id == "engineList") {
gSearchPane.onTreeSelect();
}
break;
case "blur":
if (aEvent.target.id == "engineList" &&
aEvent.target.inputField == document.getBindingParent(aEvent.originalTarget)) {
gSearchPane.onInputBlur();
}
break;
}
},
observe(aEngine, aTopic, aVerb) {
if (aTopic == "browser-search-engine-modified") {
aEngine.QueryInterface(Ci.nsISearchEngine);
switch (aVerb) {
case "engine-added":
gEngineView._engineStore.addEngine(aEngine);
gEngineView.rowCountChanged(gEngineView.lastIndex, 1);
gSearchPane.buildDefaultEngineDropDown();
break;
case "engine-changed":
gEngineView._engineStore.reloadIcons();
gEngineView.invalidate();
break;
case "engine-removed":
gSearchPane.remove(aEngine);
break;
case "engine-current":
// If the user is going through the drop down using up/down keys, the
// dropdown may still be open (eg. on Windows) when engine-current is
// fired, so rebuilding the list unconditionally would get in the way.
let selectedEngine =
document.getElementById("defaultEngine").selectedItem.engine;
if (selectedEngine.name != aEngine.name)
gSearchPane.buildDefaultEngineDropDown();
break;
case "engine-default":
// Not relevant
break;
}
}
},
onInputBlur(aEvent) {
let tree = document.getElementById("engineList");
if (!tree.hasAttribute("editing"))
return;
// Accept input unless discarded.
let accept = aEvent.charCode != KeyEvent.DOM_VK_ESCAPE;
tree.stopEditing(accept);
},
onTreeSelect() {
document.getElementById("removeEngineButton").disabled =
!gEngineView.isEngineSelectedAndRemovable();
},
onTreeKeyPress(aEvent) {
let index = gEngineView.selectedIndex;
let tree = document.getElementById("engineList");
if (tree.hasAttribute("editing"))
return;
if (aEvent.charCode == KeyEvent.DOM_VK_SPACE) {
// Space toggles the checkbox.
let newValue = !gEngineView._engineStore.engines[index].shown;
gEngineView.setCellValue(index, tree.columns.getFirstColumn(),
newValue.toString());
// Prevent page from scrolling on the space key.
aEvent.preventDefault();
} else {
let isMac = Services.appinfo.OS == "Darwin";
if ((isMac && aEvent.keyCode == KeyEvent.DOM_VK_RETURN) ||
(!isMac && aEvent.keyCode == KeyEvent.DOM_VK_F2)) {
tree.startEditing(index, tree.columns.getLastColumn());
} else if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE ||
(isMac && aEvent.shiftKey &&
aEvent.keyCode == KeyEvent.DOM_VK_BACK_SPACE &&
gEngineView.isEngineSelectedAndRemovable())) {
// Delete and Shift+Backspace (Mac) removes selected engine.
Services.search.removeEngine(gEngineView.selectedEngine.originalEngine);
}
}
},
onRestoreDefaults() {
let num = gEngineView._engineStore.restoreDefaultEngines();
gEngineView.rowCountChanged(0, num);
gEngineView.invalidate();
},
showRestoreDefaults(aEnable) {
document.getElementById("restoreDefaultSearchEngines").disabled = !aEnable;
},
remove(aEngine) {
let index = gEngineView._engineStore.removeEngine(aEngine);
gEngineView.rowCountChanged(index, -1);
gEngineView.invalidate();
gEngineView.selection.select(Math.min(index, gEngineView.lastIndex));
gEngineView.ensureRowIsVisible(gEngineView.currentIndex);
document.getElementById("engineList").focus();
},
async editKeyword(aEngine, aNewKeyword) {
let keyword = aNewKeyword.trim();
if (keyword) {
let eduplicate = false;
let dupName = "";
// Check for duplicates in Places keywords.
let bduplicate = !!(await PlacesUtils.keywords.fetch(keyword));
// Check for duplicates in changes we haven't committed yet
let engines = gEngineView._engineStore.engines;
let lc_keyword = keyword.toLocaleLowerCase();
for (let engine of engines) {
if (engine.alias &&
engine.alias.toLocaleLowerCase() == lc_keyword &&
engine.name != aEngine.name) {
eduplicate = true;
dupName = engine.name;
break;
}
}
// Notify the user if they have chosen an existing engine/bookmark keyword
if (eduplicate || bduplicate) {
let msgids = [{id: "search-keyword-warning-title"}];
if (eduplicate) {
msgids.push({id: "search-keyword-warning-engine", args: { name: dupName }});
} else {
msgids.push({id: "search-keyword-warning-bookmark"});
}
let [dtitle, msg] = await document.l10n.formatValues(msgids);
Services.prompt.alert(window, dtitle, msg);
return false;
}
}
gEngineView._engineStore.changeEngine(aEngine, "alias", keyword);
gEngineView.invalidate();
return true;
},
saveOneClickEnginesList() {
let hiddenList = [];
for (let engine of gEngineView._engineStore.engines) {
if (!engine.shown)
hiddenList.push(engine.name);
}
Preferences.get("browser.search.hiddenOneOffs").value =
hiddenList.join(",");
},
setDefaultEngine() {
Services.search.defaultEngine =
document.getElementById("defaultEngine").selectedItem.engine;
ExtensionSettingsStore.setByUser(SEARCH_TYPE, SEARCH_KEY);
},
};
function onDragEngineStart(event) {
var selectedIndex = gEngineView.selectedIndex;
var tree = document.getElementById("engineList");
let cell = tree.getCellAt(event.clientX, event.clientY);
if (selectedIndex >= 0 && !gEngineView.isCheckBox(cell.row, cell.col)) {
event.dataTransfer.setData(ENGINE_FLAVOR, selectedIndex.toString());
event.dataTransfer.effectAllowed = "move";
}
}
function EngineStore() {
let pref = Preferences.get("browser.search.hiddenOneOffs").value;
this.hiddenList = pref ? pref.split(",") : [];
this._engines = Services.search.getVisibleEngines().map(this._cloneEngine, this);
this._defaultEngines = Services.search.getDefaultEngines().map(this._cloneEngine, this);
// check if we need to disable the restore defaults button
var someHidden = this._defaultEngines.some(e => e.hidden);
gSearchPane.showRestoreDefaults(someHidden);
}
EngineStore.prototype = {
_engines: null,
_defaultEngines: null,
get engines() {
return this._engines;
},
set engines(val) {
this._engines = val;
return val;
},
_getIndexForEngine(aEngine) {
return this._engines.indexOf(aEngine);
},
_getEngineByName(aName) {
return this._engines.find(engine => engine.name == aName);
},
_cloneEngine(aEngine) {
var clonedObj = {};
for (var i in aEngine)
clonedObj[i] = aEngine[i];
clonedObj.originalEngine = aEngine;
clonedObj.shown = !this.hiddenList.includes(clonedObj.name);
return clonedObj;
},
// Callback for Array's some(). A thisObj must be passed to some()
_isSameEngine(aEngineClone) {
return aEngineClone.originalEngine == this.originalEngine;
},
addEngine(aEngine) {
this._engines.push(this._cloneEngine(aEngine));
},
moveEngine(aEngine, aNewIndex) {
if (aNewIndex < 0 || aNewIndex > this._engines.length - 1)
throw new Error("ES_moveEngine: invalid aNewIndex!");
var index = this._getIndexForEngine(aEngine);
if (index == -1)
throw new Error("ES_moveEngine: invalid engine?");
if (index == aNewIndex)
return; // nothing to do
// Move the engine in our internal store
var removedEngine = this._engines.splice(index, 1)[0];
this._engines.splice(aNewIndex, 0, removedEngine);
Services.search.moveEngine(aEngine.originalEngine, aNewIndex);
},
removeEngine(aEngine) {
if (this._engines.length == 1) {
throw new Error("Cannot remove last engine!");
}
let engineName = aEngine.name;
let index = this._engines.findIndex(element => element.name == engineName);
if (index == -1)
throw new Error("invalid engine?");
let removedEngine = this._engines.splice(index, 1)[0];
if (this._defaultEngines.some(this._isSameEngine, removedEngine))
gSearchPane.showRestoreDefaults(true);
gSearchPane.buildDefaultEngineDropDown();
return index;
},
restoreDefaultEngines() {
var added = 0;
for (var i = 0; i < this._defaultEngines.length; ++i) {
var e = this._defaultEngines[i];
// If the engine is already in the list, just move it.
if (this._engines.some(this._isSameEngine, e)) {
this.moveEngine(this._getEngineByName(e.name), i);
} else {
// Otherwise, add it back to our internal store
// The search service removes the alias when an engine is hidden,
// so clear any alias we may have cached before unhiding the engine.
e.alias = "";
this._engines.splice(i, 0, e);
let engine = e.originalEngine;
engine.hidden = false;
Services.search.moveEngine(engine, i);
added++;
}
}
Services.search.resetToOriginalDefaultEngine();
gSearchPane.showRestoreDefaults(false);
gSearchPane.buildDefaultEngineDropDown();
return added;
},
changeEngine(aEngine, aProp, aNewValue) {
var index = this._getIndexForEngine(aEngine);
if (index == -1)
throw new Error("invalid engine?");
this._engines[index][aProp] = aNewValue;
aEngine.originalEngine[aProp] = aNewValue;
},
reloadIcons() {
this._engines.forEach(function(e) {
e.uri = e.originalEngine.uri;
});
},
};
function EngineView(aEngineStore) {
this._engineStore = aEngineStore;
}
EngineView.prototype = {
_engineStore: null,
tree: null,
get lastIndex() {
return this.rowCount - 1;
},
get selectedIndex() {
var seln = this.selection;
if (seln.getRangeCount() > 0) {
var min = {};
seln.getRangeAt(0, min, {});
return min.value;
}
return -1;
},
get selectedEngine() {
return this._engineStore.engines[this.selectedIndex];
},
// Helpers
rowCountChanged(index, count) {
this.tree.rowCountChanged(index, count);
},
invalidate() {
this.tree.invalidate();
},
ensureRowIsVisible(index) {
this.tree.ensureRowIsVisible(index);
},
getSourceIndexFromDrag(dataTransfer) {
return parseInt(dataTransfer.getData(ENGINE_FLAVOR));
},
isCheckBox(index, column) {
return column.id == "engineShown";
},
isEngineSelectedAndRemovable() {
return this.selectedIndex != -1 && this.lastIndex != 0;
},
// nsITreeView
get rowCount() {
return this._engineStore.engines.length;
},
getImageSrc(index, column) {
if (column.id == "engineName") {
if (this._engineStore.engines[index].iconURI)
return this._engineStore.engines[index].iconURI.spec;
if (window.devicePixelRatio > 1)
return "chrome://browser/skin/search-engine-placeholder@2x.png";
return "chrome://browser/skin/search-engine-placeholder.png";
}
return "";
},
getCellText(index, column) {
if (column.id == "engineName")
return this._engineStore.engines[index].name;
else if (column.id == "engineKeyword")
return this._engineStore.engines[index].alias;
return "";
},
setTree(tree) {
this.tree = tree;
},
canDrop(targetIndex, orientation, dataTransfer) {
var sourceIndex = this.getSourceIndexFromDrag(dataTransfer);
return (sourceIndex != -1 &&
sourceIndex != targetIndex &&
sourceIndex != targetIndex + orientation);
},
drop(dropIndex, orientation, dataTransfer) {
var sourceIndex = this.getSourceIndexFromDrag(dataTransfer);
var sourceEngine = this._engineStore.engines[sourceIndex];
const nsITreeView = Ci.nsITreeView;
if (dropIndex > sourceIndex) {
if (orientation == nsITreeView.DROP_BEFORE)
dropIndex--;
} else if (orientation == nsITreeView.DROP_AFTER) {
dropIndex++;
}
this._engineStore.moveEngine(sourceEngine, dropIndex);
gSearchPane.showRestoreDefaults(true);
gSearchPane.buildDefaultEngineDropDown();
// Redraw, and adjust selection
this.invalidate();
this.selection.select(dropIndex);
},
selection: null,
getRowProperties(index) { return ""; },
getCellProperties(index, column) { return ""; },
getColumnProperties(column) { return ""; },
isContainer(index) { return false; },
isContainerOpen(index) { return false; },
isContainerEmpty(index) { return false; },
isSeparator(index) { return false; },
isSorted(index) { return false; },
getParentIndex(index) { return -1; },
hasNextSibling(parentIndex, index) { return false; },
getLevel(index) { return 0; },
getCellValue(index, column) {
if (column.id == "engineShown")
return this._engineStore.engines[index].shown;
return undefined;
},
toggleOpenState(index) { },
cycleHeader(column) { },
selectionChanged() { },
cycleCell(row, column) { },
isEditable(index, column) { return column.id != "engineName"; },
setCellValue(index, column, value) {
if (column.id == "engineShown") {
this._engineStore.engines[index].shown = value == "true";
gEngineView.invalidate();
gSearchPane.saveOneClickEnginesList();
}
},
setCellText(index, column, value) {
if (column.id == "engineKeyword") {
gSearchPane.editKeyword(this._engineStore.engines[index], value)
.then(valid => {
if (!valid)
document.getElementById("engineList").startEditing(index, column);
});
}
},
performAction(action) { },
performActionOnRow(action, index) { },
performActionOnCell(action, index, column) { },
};