305 lines
9.1 KiB
JavaScript
305 lines
9.1 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/. */
|
|
|
|
/* globals AdjustableTitle */
|
|
|
|
// This is the dialog that is displayed when adding or editing a search engine
|
|
// in about:preferences, or when adding a search engine via the context menu of
|
|
// an HTML form. Depending on the scenario where it is used, different arguments
|
|
// must be supplied in an object in `window.arguments[0]`:
|
|
// - `mode` [required] - The type of dialog: NEW, EDIT or FORM.
|
|
// - `title` [optional] - To display a title in the window element.
|
|
// - all arguments required by the constructor of the dialog class
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs",
|
|
});
|
|
|
|
// Set the appropriate l10n id before the dialog's connectedCallback.
|
|
let mode = window.arguments[0].mode == "EDIT" ? "edit" : "add";
|
|
document.l10n.setAttributes(
|
|
document.querySelector("dialog"),
|
|
mode + "-engine-dialog"
|
|
);
|
|
document.l10n.setAttributes(
|
|
document.querySelector("window"),
|
|
mode + "-engine-window"
|
|
);
|
|
|
|
/**
|
|
* The abstract base class for all types of user search engine dialogs.
|
|
* All subclasses must implement the abstract method `onAddEngine`.
|
|
*/
|
|
class EngineDialog {
|
|
constructor() {
|
|
this._dialog = document.querySelector("dialog");
|
|
this._form = document.getElementById("addEngineForm");
|
|
this._name = document.getElementById("engineName");
|
|
this._alias = document.getElementById("engineAlias");
|
|
this._url = document.getElementById("engineUrl");
|
|
this._suggestUrl = document.getElementById("suggestUrl");
|
|
|
|
this._name.addEventListener("input", this.onNameInput.bind(this));
|
|
this._alias.addEventListener("input", this.onAliasInput.bind(this));
|
|
this._url.addEventListener("input", this.onFormInput.bind(this));
|
|
document.addEventListener("dialogaccept", this.onAddEngine.bind(this));
|
|
}
|
|
|
|
onAddEngine() {
|
|
throw new Error("abstract");
|
|
}
|
|
|
|
isNameValid(name) {
|
|
if (!name) {
|
|
return false;
|
|
}
|
|
return !Services.search.getEngineByName(name);
|
|
}
|
|
|
|
onNameInput() {
|
|
let name = this._name.value.trim();
|
|
let validity = this.isNameValid(name)
|
|
? ""
|
|
: document.getElementById("engineNameExists").textContent;
|
|
this._name.setCustomValidity(validity);
|
|
this.onFormInput();
|
|
}
|
|
|
|
async isAliasValid(alias) {
|
|
if (!alias) {
|
|
return true;
|
|
}
|
|
return !(await Services.search.getEngineByAlias(alias));
|
|
}
|
|
|
|
async onAliasInput() {
|
|
let alias = this._alias.value.trim();
|
|
let validity = (await this.isAliasValid(alias))
|
|
? ""
|
|
: document.getElementById("engineAliasExists").textContent;
|
|
this._alias.setCustomValidity(validity);
|
|
this.onFormInput();
|
|
}
|
|
|
|
// This function is not set as a listener but called directly because it
|
|
// depends on the output of `isAliasValid`, but `isAliasValid` contains an
|
|
// await, so it would finish after this function.
|
|
onFormInput() {
|
|
this._dialog.setAttribute(
|
|
"buttondisabledaccept",
|
|
!this._form.checkValidity()
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This dialog is opened when adding a new search engine in preferences.
|
|
*/
|
|
class NewEngineDialog extends EngineDialog {
|
|
onAddEngine() {
|
|
Services.search.addUserEngine({
|
|
name: this._name.value.trim(),
|
|
url: this._url.value.trim().replace(/%s/, "{searchTerms}"),
|
|
suggestUrl: this._suggestUrl.value.trim().replace(/%s/, "{searchTerms}"),
|
|
alias: this._alias.value.trim(),
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This dialog is opened when editing a user search engine in preferences.
|
|
*/
|
|
class EditEngineDialog extends EngineDialog {
|
|
#engine;
|
|
/**
|
|
* Initializes the dialog with information from a user search engine.
|
|
*
|
|
* @param {object} args
|
|
* The arguments.
|
|
* @param {nsISearchEngine} args.engine
|
|
* The search engine to edit. Must be a UserSearchEngine.
|
|
*/
|
|
constructor({ engine }) {
|
|
super();
|
|
this._postData = document.getElementById("enginePostData");
|
|
|
|
this.#engine = engine;
|
|
this._name.value = engine.name;
|
|
this._alias.value = engine.alias ?? "";
|
|
|
|
let [url, postData] = this.getSubmissionTemplate(
|
|
lazy.SearchUtils.URL_TYPE.SEARCH
|
|
);
|
|
this._url.value = url;
|
|
if (postData) {
|
|
document.getElementById("enginePostDataRow").hidden = false;
|
|
this._postData.value = postData;
|
|
}
|
|
|
|
let [suggestUrl] = this.getSubmissionTemplate(
|
|
lazy.SearchUtils.URL_TYPE.SUGGEST_JSON
|
|
);
|
|
this._suggestUrl.value = suggestUrl ?? "";
|
|
|
|
this.onNameInput();
|
|
this.onAliasInput();
|
|
}
|
|
|
|
onAddEngine() {
|
|
this.#engine.wrappedJSObject.rename(this._name.value.trim());
|
|
this.#engine.alias = this._alias.value.trim();
|
|
|
|
let newURL = this._url.value.trim();
|
|
let newPostData = this._postData.value.trim();
|
|
|
|
// UserSearchEngine.changeUrl() does not check whether the URL has actually changed.
|
|
let [prevURL, prevPostData] = this.getSubmissionTemplate(
|
|
lazy.SearchUtils.URL_TYPE.SEARCH
|
|
);
|
|
if (newURL != prevURL || (prevPostData && prevPostData != newPostData)) {
|
|
this.#engine.wrappedJSObject.changeUrl(
|
|
lazy.SearchUtils.URL_TYPE.SEARCH,
|
|
newURL.replace(/%s/, "{searchTerms}"),
|
|
prevPostData ? newPostData.replace(/%s/, "{searchTerms}") : null
|
|
);
|
|
}
|
|
|
|
let newSuggestURL = this._suggestUrl.value.trim() || null;
|
|
let [prevSuggestUrl] = this.getSubmissionTemplate(
|
|
lazy.SearchUtils.URL_TYPE.SUGGEST_JSON
|
|
);
|
|
if (newSuggestURL != prevSuggestUrl) {
|
|
this.#engine.wrappedJSObject.changeUrl(
|
|
lazy.SearchUtils.URL_TYPE.SUGGEST_JSON,
|
|
newSuggestURL.replace(/%s/, "{searchTerms}"),
|
|
null
|
|
);
|
|
}
|
|
}
|
|
|
|
isNameValid(name) {
|
|
if (!name) {
|
|
return false;
|
|
}
|
|
let engine = Services.search.getEngineByName(this._name.value);
|
|
return !engine || engine.id == this.#engine.id;
|
|
}
|
|
|
|
async isAliasValid(alias) {
|
|
if (!alias) {
|
|
return true;
|
|
}
|
|
let engine = await Services.search.getEngineByAlias(this._alias.value);
|
|
return !engine || engine.id == this.#engine.id;
|
|
}
|
|
|
|
/**
|
|
* Returns url and post data templates of the requested type.
|
|
* Both contain %s in place of the search terms.
|
|
*
|
|
* If no url of the requested type exists, both are null.
|
|
* If the url is a GET url, the post data is null.
|
|
*
|
|
* @param {string} urlType
|
|
* The `SearchUtils.URL_TYPE`.
|
|
* @returns {[?string, ?string]}
|
|
* Array of the url and post data.
|
|
*/
|
|
getSubmissionTemplate(urlType) {
|
|
let submission = this.#engine.getSubmission("searchTerms", urlType);
|
|
if (!submission) {
|
|
return [null, null];
|
|
}
|
|
let postData = null;
|
|
if (submission.postData) {
|
|
let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
|
|
Ci.nsIBinaryInputStream
|
|
);
|
|
binaryStream.setInputStream(submission.postData.data);
|
|
|
|
postData = binaryStream
|
|
.readBytes(binaryStream.available())
|
|
.replace("searchTerms", "%s");
|
|
}
|
|
let url = submission.uri.spec.replace("searchTerms", "%s");
|
|
return [url, postData];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This dialog is opened via the context menu of an input and lets the
|
|
* user choose a name and an alias for an engine. Unlike the other two
|
|
* dialogs, it does not add or change an engine in the search service,
|
|
* and instead returns the user input to the caller.
|
|
*
|
|
* The chosen name and alias are returned via `window.arguments[0].engineInfo`.
|
|
* If the user chooses to not save the engine, it's undefined.
|
|
*/
|
|
class NewEngineFromFormDialog extends EngineDialog {
|
|
/**
|
|
* Initializes the dialog.
|
|
*
|
|
* @param {object} args
|
|
* The arguments.
|
|
* @param {string} args.nameTemplate
|
|
* The initial value of the name input.
|
|
*/
|
|
constructor({ nameTemplate }) {
|
|
super();
|
|
this._name.value = nameTemplate;
|
|
this.onNameInput();
|
|
this.onAliasInput();
|
|
|
|
document.getElementById("engineUrlRow").remove();
|
|
this._url = null;
|
|
document.getElementById("suggestUrlRow").remove();
|
|
this._suggestUrl = null;
|
|
|
|
let title = { raw: document.title };
|
|
document.documentElement.setAttribute("headertitle", JSON.stringify(title));
|
|
document.documentElement.style.setProperty(
|
|
"--icon-url",
|
|
'url("chrome://browser/skin/preferences/category-search.svg")'
|
|
);
|
|
}
|
|
|
|
onAddEngine() {
|
|
window.arguments[0].engineInfo = {
|
|
name: this._name.value.trim(),
|
|
// Empty string means no alias.
|
|
alias: this._alias.value.trim(),
|
|
};
|
|
}
|
|
}
|
|
|
|
let loadedResolvers = Promise.withResolvers();
|
|
document.mozSubdialogReady = loadedResolvers.promise;
|
|
|
|
let gAddEngineDialog = null;
|
|
window.addEventListener("DOMContentLoaded", () => {
|
|
if (!window.arguments[0].title) {
|
|
AdjustableTitle.hide();
|
|
}
|
|
|
|
try {
|
|
switch (window.arguments[0].mode) {
|
|
case "NEW":
|
|
gAddEngineDialog = new NewEngineDialog();
|
|
break;
|
|
case "EDIT":
|
|
gAddEngineDialog = new EditEngineDialog(window.arguments[0]);
|
|
break;
|
|
case "FORM":
|
|
gAddEngineDialog = new NewEngineFromFormDialog(window.arguments[0]);
|
|
break;
|
|
default:
|
|
throw new Error("Mode not supported for addEngine dialog.");
|
|
}
|
|
} finally {
|
|
loadedResolvers.resolve();
|
|
}
|
|
});
|