Bug 1759879 - Add error messages to add engine dialog. r=search-reviewers,fluent-reviewers,settings-reviewers,desktop-theme-reviewers,urlbar-reviewers,bolsson,mossop,Standard8,dao
Differential Revision: https://phabricator.services.mozilla.com/D245411
This commit is contained in:
committed by
mbeier@mozilla.com
parent
af48d212c2
commit
167482d903
@@ -174,8 +174,8 @@
|
|||||||
search-l10n-ids="
|
search-l10n-ids="
|
||||||
add-engine-button,
|
add-engine-button,
|
||||||
add-engine-name,
|
add-engine-name,
|
||||||
add-engine-keyword,
|
add-engine-keyword2,
|
||||||
add-engine-url,
|
add-engine-url2,
|
||||||
add-engine-dialog.buttonlabelaccept,
|
add-engine-dialog.buttonlabelaccept,
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -81,42 +81,84 @@ add_task(async function test_validation() {
|
|||||||
let name = dialogWin.document.getElementById("engineName");
|
let name = dialogWin.document.getElementById("engineName");
|
||||||
let url = dialogWin.document.getElementById("engineUrl");
|
let url = dialogWin.document.getElementById("engineUrl");
|
||||||
let alias = dialogWin.document.getElementById("engineAlias");
|
let alias = dialogWin.document.getElementById("engineAlias");
|
||||||
|
let suggestUrl = dialogWin.document.getElementById("suggestUrl");
|
||||||
|
|
||||||
Assert.ok(
|
Assert.ok(
|
||||||
name.value == "" && url.value == "" && alias.value == "",
|
name.value == "" && url.value == "" && alias.value == "",
|
||||||
"Everything is empty initially."
|
"Everything is empty initially."
|
||||||
);
|
);
|
||||||
Assert.ok(button.disabled, "Button is disabled initially.");
|
Assert.ok(button.disabled, "Button is disabled initially.");
|
||||||
|
await assertError(name, "add-engine-no-name");
|
||||||
|
await assertError(url, "add-engine-no-url");
|
||||||
|
await assertError(alias, null);
|
||||||
|
await assertError(suggestUrl, null);
|
||||||
|
|
||||||
setName("Example", dialogWin);
|
setName("Example", dialogWin);
|
||||||
setUrl("https://example.com/search?q=%s", dialogWin);
|
setUrl("https://example.com/search?q=%s", dialogWin);
|
||||||
await setAlias("abc", dialogWin);
|
await setAlias("abc", dialogWin);
|
||||||
|
setSuggestUrl("https://example.com/search?q=%s", dialogWin);
|
||||||
Assert.ok(!button.disabled, "Button is enabled when everything is there.");
|
Assert.ok(!button.disabled, "Button is enabled when everything is there.");
|
||||||
|
|
||||||
|
// Check name
|
||||||
|
setName("", dialogWin);
|
||||||
|
await assertError(name, "add-engine-no-name");
|
||||||
|
Assert.ok(button.disabled, "Name is required.");
|
||||||
|
|
||||||
|
setName(existingEngine.name, dialogWin);
|
||||||
|
await assertError(name, "add-engine-name-exists");
|
||||||
|
Assert.ok(button.disabled, "Existing name is not allowed.");
|
||||||
|
|
||||||
|
setName("Example", dialogWin);
|
||||||
|
await assertError(name, null);
|
||||||
|
Assert.ok(!button.disabled, "Good name enables the button.");
|
||||||
|
|
||||||
// Check URL
|
// Check URL
|
||||||
setUrl("", dialogWin);
|
setUrl("", dialogWin);
|
||||||
Assert.ok(button.disabled, "URL is required.");
|
Assert.ok(button.disabled, "URL is required.");
|
||||||
setUrl("javascript://%s", dialogWin);
|
await assertError(url, "add-engine-no-url");
|
||||||
Assert.ok(button.disabled, "Javascript URLs are not allowed.");
|
|
||||||
setUrl("https://example.com/search?q=%s", dialogWin);
|
|
||||||
Assert.ok(!button.disabled, "Good URL enables the button.");
|
|
||||||
|
|
||||||
// Check name
|
setUrl("javascript://%s", dialogWin);
|
||||||
setName("", dialogWin);
|
await assertError(url, "add-engine-invalid-protocol");
|
||||||
Assert.ok(button.disabled, "Name is required.");
|
Assert.ok(button.disabled, "Javascript URLs are not allowed.");
|
||||||
setName(existingEngine.name, dialogWin);
|
|
||||||
Assert.ok(button.disabled, "Existing name is not allowed.");
|
setUrl("not a url", dialogWin);
|
||||||
setName("Example", dialogWin);
|
await assertError(url, "add-engine-invalid-url");
|
||||||
Assert.ok(!button.disabled, "Good name enables the button.");
|
Assert.ok(button.disabled, "Invalid URLs are not allowed.");
|
||||||
|
|
||||||
|
setUrl("https://example.com/search?q=%s", dialogWin);
|
||||||
|
await assertError(url, null);
|
||||||
|
Assert.ok(!button.disabled, "Good URL enables the button.");
|
||||||
|
|
||||||
// Check alias
|
// Check alias
|
||||||
await setAlias("", dialogWin);
|
await setAlias("", dialogWin);
|
||||||
|
await assertError(alias, null);
|
||||||
Assert.ok(!button.disabled, "Alias is not required.");
|
Assert.ok(!button.disabled, "Alias is not required.");
|
||||||
|
|
||||||
await setAlias(existingEngine.alias, dialogWin);
|
await setAlias(existingEngine.alias, dialogWin);
|
||||||
|
await assertError(alias, "add-engine-keyword-exists");
|
||||||
Assert.ok(button.disabled, "Existing alias is not allowed.");
|
Assert.ok(button.disabled, "Existing alias is not allowed.");
|
||||||
|
|
||||||
|
await setAlias(existingEngine.alias.toUpperCase(), dialogWin);
|
||||||
|
await assertError(alias, "add-engine-keyword-exists");
|
||||||
|
Assert.ok(button.disabled, "Alias duplicate test is case insensitive.");
|
||||||
|
|
||||||
await setAlias("abc", dialogWin);
|
await setAlias("abc", dialogWin);
|
||||||
|
await assertError(alias, null);
|
||||||
Assert.ok(!button.disabled, "Good alias enables the button.");
|
Assert.ok(!button.disabled, "Good alias enables the button.");
|
||||||
|
|
||||||
|
// Check suggest URL
|
||||||
|
setSuggestUrl("javascript://%s", dialogWin);
|
||||||
|
await assertError(suggestUrl, "add-engine-invalid-protocol");
|
||||||
|
Assert.ok(button.disabled, "Javascript URLs are not allowed.");
|
||||||
|
|
||||||
|
setSuggestUrl("not a url", dialogWin);
|
||||||
|
await assertError(suggestUrl, "add-engine-invalid-url");
|
||||||
|
Assert.ok(button.disabled, "Invalid URLs are not allowed.");
|
||||||
|
|
||||||
|
setSuggestUrl("https://example.com/search?q=%s", dialogWin);
|
||||||
|
await assertError(suggestUrl, null);
|
||||||
|
Assert.ok(!button.disabled, "Good URL enables the button.");
|
||||||
|
|
||||||
// Clean up.
|
// Clean up.
|
||||||
BrowserTestUtils.removeTab(gBrowser.selectedTab);
|
BrowserTestUtils.removeTab(gBrowser.selectedTab);
|
||||||
await Services.search.removeEngine(existingEngine);
|
await Services.search.removeEngine(existingEngine);
|
||||||
@@ -158,8 +200,12 @@ add_task(async function test_editEngine() {
|
|||||||
Assert.ok(!!userEngineIndex, "User engine is in the table.");
|
Assert.ok(!!userEngineIndex, "User engine is in the table.");
|
||||||
view.selection.select(userEngineIndex);
|
view.selection.select(userEngineIndex);
|
||||||
|
|
||||||
// Open the dialog.
|
// Open the dialog and check values.
|
||||||
let dialogWin = await openDialogWith(doc, () => editButton.click());
|
let dialogWin = await openDialogWith(doc, () => editButton.click());
|
||||||
|
let acceptButton = dialogWin.document
|
||||||
|
.querySelector("dialog")
|
||||||
|
.shadowRoot.querySelector('button[dlgtype="accept"]');
|
||||||
|
|
||||||
Assert.equal(
|
Assert.equal(
|
||||||
dialogWin.document.getElementById("titleContainer").style.display,
|
dialogWin.document.getElementById("titleContainer").style.display,
|
||||||
"none",
|
"none",
|
||||||
@@ -201,7 +247,7 @@ add_task(async function test_editEngine() {
|
|||||||
SearchUtils.TOPIC_ENGINE_MODIFIED,
|
SearchUtils.TOPIC_ENGINE_MODIFIED,
|
||||||
3
|
3
|
||||||
);
|
);
|
||||||
EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin);
|
acceptButton.click();
|
||||||
await promiseChanged;
|
await promiseChanged;
|
||||||
Assert.ok(true, "Got 3 change notifications.");
|
Assert.ok(true, "Got 3 change notifications.");
|
||||||
|
|
||||||
@@ -301,11 +347,11 @@ add_task(async function test_editPostEngine() {
|
|||||||
let promiseChanged = SearchTestUtils.promiseSearchNotification(
|
let promiseChanged = SearchTestUtils.promiseSearchNotification(
|
||||||
SearchUtils.MODIFIED_TYPE.CHANGED,
|
SearchUtils.MODIFIED_TYPE.CHANGED,
|
||||||
SearchUtils.TOPIC_ENGINE_MODIFIED,
|
SearchUtils.TOPIC_ENGINE_MODIFIED,
|
||||||
3
|
4
|
||||||
);
|
);
|
||||||
EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin);
|
EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin);
|
||||||
await promiseChanged;
|
await promiseChanged;
|
||||||
Assert.ok(true, "Got 3 change notifications.");
|
Assert.ok(true, "Got 4 change notifications.");
|
||||||
|
|
||||||
// Open dialog again and check values.
|
// Open dialog again and check values.
|
||||||
dialogWin = await openDialogWith(doc, () => editButton.click());
|
dialogWin = await openDialogWith(doc, () => editButton.click());
|
||||||
@@ -370,6 +416,25 @@ add_task(async function test_editPostEngine() {
|
|||||||
await Services.search.removeEngine(engine);
|
await Services.search.removeEngine(engine);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the error label of an input of the add engine dialog.
|
||||||
|
*
|
||||||
|
* @param {HTMLInputElement} elt
|
||||||
|
* The element whose error should be checked
|
||||||
|
* @param {?string} error
|
||||||
|
* The l10n id of the expected error message or null if no error is expected.
|
||||||
|
*/
|
||||||
|
async function assertError(elt, error = null) {
|
||||||
|
let errorLabel = elt.parentElement.querySelector(".error-label");
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
let msg = await document.l10n.formatValue(error);
|
||||||
|
Assert.equal(errorLabel.textContent, msg);
|
||||||
|
} else {
|
||||||
|
Assert.equal(errorLabel.textContent, "valid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function openDialogWith(doc, fn) {
|
async function openDialogWith(doc, fn) {
|
||||||
let dialogLoaded = TestUtils.topicObserved("subdialog-loaded");
|
let dialogLoaded = TestUtils.topicObserved("subdialog-loaded");
|
||||||
await fn();
|
await fn();
|
||||||
@@ -387,14 +452,14 @@ function setUrl(value, win) {
|
|||||||
fillTextField("engineUrl", value, win);
|
fillTextField("engineUrl", value, win);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPostData(value, win) {
|
|
||||||
fillTextField("enginePostData", value, win);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setSuggestUrl(value, win) {
|
function setSuggestUrl(value, win) {
|
||||||
fillTextField("suggestUrl", value, win);
|
fillTextField("suggestUrl", value, win);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setPostData(value, win) {
|
||||||
|
fillTextField("enginePostData", value, win);
|
||||||
|
}
|
||||||
|
|
||||||
async function setAlias(value, win) {
|
async function setAlias(value, win) {
|
||||||
fillTextField("engineAlias", value, win);
|
fillTextField("engineAlias", value, win);
|
||||||
await TestUtils.waitForTick();
|
await TestUtils.waitForTick();
|
||||||
|
|||||||
@@ -10,21 +10,52 @@ hbox {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialogRow > label {
|
#titleContainer {
|
||||||
/* Align the labels with the inputs */
|
padding-bottom: var(--space-xlarge);
|
||||||
margin-inline-start: 4px;
|
--icon-url: url("chrome://browser/skin/preferences/category-search.svg")
|
||||||
}
|
}
|
||||||
|
|
||||||
#engineUrl,
|
.dialogRow {
|
||||||
#suggestUrl {
|
& > label,
|
||||||
|
& > .error-label {
|
||||||
|
/* Align the labels with the inputs */
|
||||||
|
margin-inline-start: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="url"] {
|
||||||
/* Full URLs should always be displayed as LTR */
|
/* Full URLs should always be displayed as LTR */
|
||||||
direction: ltr;
|
direction: ltr;
|
||||||
text-align: match-parent;
|
text-align: match-parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialogRow:not([hidden]) {
|
.dialogRow:not([hidden]) {
|
||||||
--grid-padding: 16px;
|
|
||||||
margin-block: 0 var(--grid-padding);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.error-label {
|
||||||
|
display: flex;
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
margin-top: var(--space-xxsmall);
|
||||||
|
margin-bottom: var(--space-medium);
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xsmall);
|
||||||
|
color: var(--text-color-error);
|
||||||
|
|
||||||
|
visibility: hidden;
|
||||||
|
input:user-invalid + & {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: url("chrome://global/skin/icons/error.svg");
|
||||||
|
fill: var(--icon-color-critical);
|
||||||
|
-moz-context-properties: fill;
|
||||||
|
height: var(--icon-size-default);
|
||||||
|
width: var(--icon-size-default);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
// an HTML form. Depending on the scenario where it is used, different arguments
|
// an HTML form. Depending on the scenario where it is used, different arguments
|
||||||
// must be supplied in an object in `window.arguments[0]`:
|
// must be supplied in an object in `window.arguments[0]`:
|
||||||
// - `mode` [required] - The type of dialog: NEW, EDIT or FORM.
|
// - `mode` [required] - The type of dialog: NEW, EDIT or FORM.
|
||||||
// - `title` [optional] - To display a title in the window element.
|
// - `title` [optional] - Whether to display a title in the window element.
|
||||||
// - all arguments required by the constructor of the dialog class
|
// - all arguments required by the constructor of the dialog class
|
||||||
|
|
||||||
const lazy = {};
|
const lazy = {};
|
||||||
@@ -29,6 +29,14 @@ document.l10n.setAttributes(
|
|||||||
mode + "-engine-window"
|
mode + "-engine-window"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let loadedResolvers = Promise.withResolvers();
|
||||||
|
document.mozSubdialogReady = loadedResolvers.promise;
|
||||||
|
|
||||||
|
/** @type {?EngineDialog} */
|
||||||
|
let gAddEngineDialog = null;
|
||||||
|
/** @type {?Map<string, string>} */
|
||||||
|
let l10nCache = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The abstract base class for all types of user search engine dialogs.
|
* The abstract base class for all types of user search engine dialogs.
|
||||||
* All subclasses must implement the abstract method `onAddEngine`.
|
* All subclasses must implement the abstract method `onAddEngine`.
|
||||||
@@ -36,70 +44,181 @@ document.l10n.setAttributes(
|
|||||||
class EngineDialog {
|
class EngineDialog {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._dialog = document.querySelector("dialog");
|
this._dialog = document.querySelector("dialog");
|
||||||
|
|
||||||
this._form = document.getElementById("addEngineForm");
|
this._form = document.getElementById("addEngineForm");
|
||||||
this._name = document.getElementById("engineName");
|
this._name = document.getElementById("engineName");
|
||||||
this._alias = document.getElementById("engineAlias");
|
this._alias = document.getElementById("engineAlias");
|
||||||
this._url = document.getElementById("engineUrl");
|
this._url = document.getElementById("engineUrl");
|
||||||
|
this._postData = document.getElementById("enginePostData");
|
||||||
this._suggestUrl = document.getElementById("suggestUrl");
|
this._suggestUrl = document.getElementById("suggestUrl");
|
||||||
|
|
||||||
this._name.addEventListener("input", this.onNameInput.bind(this));
|
this._form.addEventListener("input", e => this.validateInput(e.target));
|
||||||
this._alias.addEventListener("input", this.onAliasInput.bind(this));
|
document.addEventListener("dialogaccept", this.onAccept.bind(this));
|
||||||
this._url.addEventListener("input", this.onFormInput.bind(this));
|
|
||||||
document.addEventListener("dialogaccept", this.onAddEngine.bind(this));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddEngine() {
|
onAccept() {
|
||||||
throw new Error("abstract");
|
throw new Error("abstract");
|
||||||
}
|
}
|
||||||
|
|
||||||
isNameValid(name) {
|
validateName() {
|
||||||
if (!name) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return !Services.search.getEngineByName(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
onNameInput() {
|
|
||||||
let name = this._name.value.trim();
|
let name = this._name.value.trim();
|
||||||
let validity = this.isNameValid(name)
|
if (!name) {
|
||||||
? ""
|
this.setValidity(this._name, "add-engine-no-name");
|
||||||
: document.getElementById("engineNameExists").textContent;
|
return;
|
||||||
this._name.setCustomValidity(validity);
|
}
|
||||||
this.onFormInput();
|
|
||||||
}
|
let existingEngine = Services.search.getEngineByName(name);
|
||||||
|
if (existingEngine && !this.allowedNames.includes(name)) {
|
||||||
async isAliasValid(alias) {
|
this.setValidity(this._name, "add-engine-name-exists");
|
||||||
if (!alias) {
|
} else {
|
||||||
return true;
|
this.setValidity(this._name, null);
|
||||||
}
|
}
|
||||||
return !(await Services.search.getEngineByAlias(alias));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onAliasInput() {
|
async validateAlias() {
|
||||||
let alias = this._alias.value.trim();
|
let alias = this._alias.value.trim();
|
||||||
let validity = (await this.isAliasValid(alias))
|
if (!alias) {
|
||||||
? ""
|
this.setValidity(this._alias, null);
|
||||||
: document.getElementById("engineAliasExists").textContent;
|
return;
|
||||||
this._alias.setCustomValidity(validity);
|
}
|
||||||
this.onFormInput();
|
|
||||||
|
let existingEngine = await Services.search.getEngineByAlias(alias);
|
||||||
|
if (existingEngine && !this.allowedAliases.includes(alias)) {
|
||||||
|
this.setValidity(this._alias, "add-engine-keyword-exists");
|
||||||
|
} else {
|
||||||
|
this.setValidity(this._alias, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function is not set as a listener but called directly because it
|
/**
|
||||||
// depends on the output of `isAliasValid`, but `isAliasValid` contains an
|
* Validates the passed URL input element and updates error messages.
|
||||||
// await, so it would finish after this function.
|
*
|
||||||
onFormInput() {
|
* @param {HTMLInputElement} urlInput
|
||||||
|
* The URL input to validate.
|
||||||
|
* @param {boolean} required
|
||||||
|
* Whether the input is required or optional.
|
||||||
|
*/
|
||||||
|
validateUrlInput(urlInput, required) {
|
||||||
|
let urlString = urlInput.value.trim();
|
||||||
|
if (!urlString) {
|
||||||
|
if (required) {
|
||||||
|
this.setValidity(urlInput, "add-engine-no-url");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setValidity(urlInput, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = URL.parse(urlString);
|
||||||
|
if (!url) {
|
||||||
|
this.setValidity(urlInput, "add-engine-invalid-url");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.protocol == "http:" || url.protocol == "https:") {
|
||||||
|
this.setValidity(urlInput, null);
|
||||||
|
} else {
|
||||||
|
this.setValidity(urlInput, "add-engine-invalid-protocol");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the passed input element and updates error messages.
|
||||||
|
*
|
||||||
|
* @param {HTMLInputElement} input
|
||||||
|
* The input element to validate.
|
||||||
|
*/
|
||||||
|
async validateInput(input) {
|
||||||
|
switch (input.id) {
|
||||||
|
case this._name.id:
|
||||||
|
this.validateName();
|
||||||
|
break;
|
||||||
|
case this._alias.id:
|
||||||
|
await this.validateAlias();
|
||||||
|
break;
|
||||||
|
case this._url.id:
|
||||||
|
this.validateUrlInput(this._url, true);
|
||||||
|
break;
|
||||||
|
case this._suggestUrl.id:
|
||||||
|
this.validateUrlInput(this._suggestUrl, false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateAll() {
|
||||||
|
for (let input of this._form.elements) {
|
||||||
|
await this.validateInput(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the validity of the passed input element to the string belonging
|
||||||
|
* to the passed l10n id. Also updates the input's error label and
|
||||||
|
* the accept button.
|
||||||
|
*
|
||||||
|
* @param {HTMLInputElement} inputElement
|
||||||
|
* @param {string} l10nId
|
||||||
|
* The l10n id of the string to use as validity.
|
||||||
|
* Must be a key of `l10nCache`.
|
||||||
|
*/
|
||||||
|
setValidity(inputElement, l10nId) {
|
||||||
|
if (l10nId) {
|
||||||
|
inputElement.setCustomValidity(l10nCache.get(l10nId));
|
||||||
|
} else {
|
||||||
|
inputElement.setCustomValidity("");
|
||||||
|
}
|
||||||
|
|
||||||
|
let errorLabel = inputElement.parentElement.querySelector(".error-label");
|
||||||
|
let validationMessage = inputElement.validationMessage;
|
||||||
|
|
||||||
|
// If valid, set the error label to "valid" to ensure the layout doesn't shift.
|
||||||
|
// The CSS already hides the error label based on the validity of `inputElement`.
|
||||||
|
errorLabel.textContent = validationMessage || "valid";
|
||||||
|
|
||||||
this._dialog.setAttribute(
|
this._dialog.setAttribute(
|
||||||
"buttondisabledaccept",
|
"buttondisabledaccept",
|
||||||
!this._form.checkValidity()
|
!this._form.checkValidity()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Engine names that always are allowed, even if they are already in use.
|
||||||
|
* This is needed for the edit engine dialog.
|
||||||
|
*
|
||||||
|
* @type {string[]}
|
||||||
|
*/
|
||||||
|
get allowedNames() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Engine aliases that always are allowed, even if they are already in use.
|
||||||
|
* This is needed for the edit engine dialog.
|
||||||
|
*
|
||||||
|
* @type {string[]}
|
||||||
|
*/
|
||||||
|
get allowedAliases() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This dialog is opened when adding a new search engine in preferences.
|
* This dialog is opened when adding a new search engine in preferences.
|
||||||
*/
|
*/
|
||||||
class NewEngineDialog extends EngineDialog {
|
class NewEngineDialog extends EngineDialog {
|
||||||
onAddEngine() {
|
constructor() {
|
||||||
|
super();
|
||||||
|
document.l10n.setAttributes(this._name, "add-engine-name-placeholder");
|
||||||
|
document.l10n.setAttributes(this._url, "add-engine-url-placeholder");
|
||||||
|
document.l10n.setAttributes(this._alias, "add-engine-keyword-placeholder");
|
||||||
|
|
||||||
|
document.getElementById("enginePostDataRow").remove();
|
||||||
|
this._postData = null;
|
||||||
|
|
||||||
|
this.validateAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
onAccept() {
|
||||||
Services.search.addUserEngine({
|
Services.search.addUserEngine({
|
||||||
name: this._name.value.trim(),
|
name: this._name.value.trim(),
|
||||||
url: this._url.value.trim().replace(/%s/, "{searchTerms}"),
|
url: this._url.value.trim().replace(/%s/, "{searchTerms}"),
|
||||||
@@ -124,8 +243,6 @@ class EditEngineDialog extends EngineDialog {
|
|||||||
*/
|
*/
|
||||||
constructor({ engine }) {
|
constructor({ engine }) {
|
||||||
super();
|
super();
|
||||||
this._postData = document.getElementById("enginePostData");
|
|
||||||
|
|
||||||
this.#engine = engine;
|
this.#engine = engine;
|
||||||
this._name.value = engine.name;
|
this._name.value = engine.name;
|
||||||
this._alias.value = engine.alias ?? "";
|
this._alias.value = engine.alias ?? "";
|
||||||
@@ -135,20 +252,22 @@ class EditEngineDialog extends EngineDialog {
|
|||||||
);
|
);
|
||||||
this._url.value = url;
|
this._url.value = url;
|
||||||
if (postData) {
|
if (postData) {
|
||||||
document.getElementById("enginePostDataRow").hidden = false;
|
|
||||||
this._postData.value = postData;
|
this._postData.value = postData;
|
||||||
|
} else {
|
||||||
|
document.getElementById("enginePostDataRow").hidden = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let [suggestUrl] = this.getSubmissionTemplate(
|
let [suggestUrl] = this.getSubmissionTemplate(
|
||||||
lazy.SearchUtils.URL_TYPE.SUGGEST_JSON
|
lazy.SearchUtils.URL_TYPE.SUGGEST_JSON
|
||||||
);
|
);
|
||||||
this._suggestUrl.value = suggestUrl ?? "";
|
if (suggestUrl) {
|
||||||
|
this._suggestUrl.value = suggestUrl;
|
||||||
|
}
|
||||||
|
|
||||||
this.onNameInput();
|
this.validateAll();
|
||||||
this.onAliasInput();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddEngine() {
|
onAccept() {
|
||||||
this.#engine.wrappedJSObject.rename(this._name.value.trim());
|
this.#engine.wrappedJSObject.rename(this._name.value.trim());
|
||||||
this.#engine.alias = this._alias.value.trim();
|
this.#engine.alias = this._alias.value.trim();
|
||||||
|
|
||||||
@@ -180,20 +299,12 @@ class EditEngineDialog extends EngineDialog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isNameValid(name) {
|
get allowedAliases() {
|
||||||
if (!name) {
|
return [this.#engine.alias];
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let engine = Services.search.getEngineByName(this._name.value);
|
|
||||||
return !engine || engine.id == this.#engine.id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async isAliasValid(alias) {
|
get allowedNames() {
|
||||||
if (!alias) {
|
return [this.#engine.name];
|
||||||
return true;
|
|
||||||
}
|
|
||||||
let engine = await Services.search.getEngineByAlias(this._alias.value);
|
|
||||||
return !engine || engine.id == this.#engine.id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -249,24 +360,19 @@ class NewEngineFromFormDialog extends EngineDialog {
|
|||||||
*/
|
*/
|
||||||
constructor({ nameTemplate }) {
|
constructor({ nameTemplate }) {
|
||||||
super();
|
super();
|
||||||
this._name.value = nameTemplate;
|
|
||||||
this.onNameInput();
|
|
||||||
this.onAliasInput();
|
|
||||||
|
|
||||||
document.getElementById("engineUrlRow").remove();
|
document.getElementById("engineUrlRow").remove();
|
||||||
this._url = null;
|
this._url = null;
|
||||||
document.getElementById("suggestUrlRow").remove();
|
document.getElementById("suggestUrlRow").remove();
|
||||||
this._suggestUrl = null;
|
this._suggestUrl = null;
|
||||||
|
document.getElementById("enginePostDataRow").remove();
|
||||||
|
this._postData = null;
|
||||||
|
|
||||||
let title = { raw: document.title };
|
this._name.value = nameTemplate;
|
||||||
document.documentElement.setAttribute("headertitle", JSON.stringify(title));
|
this.validateAll();
|
||||||
document.documentElement.style.setProperty(
|
|
||||||
"--icon-url",
|
|
||||||
'url("chrome://browser/skin/preferences/category-search.svg")'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddEngine() {
|
onAccept() {
|
||||||
|
// Return the input to the caller.
|
||||||
window.arguments[0].engineInfo = {
|
window.arguments[0].engineInfo = {
|
||||||
name: this._name.value.trim(),
|
name: this._name.value.trim(),
|
||||||
// Empty string means no alias.
|
// Empty string means no alias.
|
||||||
@@ -275,16 +381,37 @@ class NewEngineFromFormDialog extends EngineDialog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let loadedResolvers = Promise.withResolvers();
|
async function initL10nCache() {
|
||||||
document.mozSubdialogReady = loadedResolvers.promise;
|
const errorIds = [
|
||||||
|
"add-engine-name-exists",
|
||||||
|
"add-engine-keyword-exists",
|
||||||
|
"add-engine-no-name",
|
||||||
|
"add-engine-no-url",
|
||||||
|
"add-engine-invalid-protocol",
|
||||||
|
"add-engine-invalid-url",
|
||||||
|
];
|
||||||
|
|
||||||
let gAddEngineDialog = null;
|
let msgs = await document.l10n.formatValues(errorIds.map(id => ({ id })));
|
||||||
window.addEventListener("DOMContentLoaded", () => {
|
l10nCache = new Map();
|
||||||
if (!window.arguments[0].title) {
|
|
||||||
AdjustableTitle.hide();
|
for (let i = 0; i < errorIds.length; i++) {
|
||||||
|
l10nCache.set(errorIds[i], msgs[i]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("DOMContentLoaded", async () => {
|
||||||
try {
|
try {
|
||||||
|
if (window.arguments[0].title) {
|
||||||
|
document.documentElement.setAttribute(
|
||||||
|
"headertitle",
|
||||||
|
JSON.stringify({ raw: document.title })
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
AdjustableTitle.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
await initL10nCache();
|
||||||
|
|
||||||
switch (window.arguments[0].mode) {
|
switch (window.arguments[0].mode) {
|
||||||
case "NEW":
|
case "NEW":
|
||||||
gAddEngineDialog = new NewEngineDialog();
|
gAddEngineDialog = new NewEngineDialog();
|
||||||
|
|||||||
@@ -33,39 +33,45 @@
|
|||||||
<script src="chrome://global/content/adjustableTitle.js" />
|
<script src="chrome://global/content/adjustableTitle.js" />
|
||||||
<script src="chrome://browser/content/search/addEngine.js" />
|
<script src="chrome://browser/content/search/addEngine.js" />
|
||||||
|
|
||||||
<separator class="thin" />
|
|
||||||
|
|
||||||
<html:form id="addEngineForm">
|
<html:form id="addEngineForm">
|
||||||
<html:div class="dialogRow">
|
<html:div class="dialogRow">
|
||||||
<html:span
|
|
||||||
id="engineNameExists"
|
|
||||||
hidden="hidden"
|
|
||||||
data-l10n-id="engine-name-exists"
|
|
||||||
/>
|
|
||||||
<html:label
|
<html:label
|
||||||
id="engineNameLabel"
|
id="engineNameLabel"
|
||||||
for="engineName"
|
for="engineName"
|
||||||
data-l10n-id="add-engine-name"
|
data-l10n-id="add-engine-name"
|
||||||
/>
|
/>
|
||||||
<hbox>
|
<html:input id="engineName" type="text" required="required" />
|
||||||
<html:input id="engineName" type="text" required="required" />
|
<html:div class="error-label">valid</html:div>
|
||||||
</hbox>
|
|
||||||
</html:div>
|
</html:div>
|
||||||
|
|
||||||
<html:div class="dialogRow" id="engineUrlRow">
|
<html:div class="dialogRow" id="engineUrlRow">
|
||||||
<html:label
|
<html:label
|
||||||
id="engineUrlLabel"
|
id="engineUrlLabel"
|
||||||
for="engineUrl"
|
for="engineUrl"
|
||||||
data-l10n-id="add-engine-url"
|
data-l10n-id="add-engine-url2"
|
||||||
/>
|
/>
|
||||||
<hbox>
|
<html:input id="engineUrl" type="url" required="required" />
|
||||||
<html:input
|
<html:div class="error-label">valid</html:div>
|
||||||
id="engineUrl"
|
</html:div>
|
||||||
type="url"
|
|
||||||
required="required"
|
<html:div class="dialogRow" id="enginePostDataRow">
|
||||||
pattern="https?:.*"
|
<html:label
|
||||||
/>
|
id="enginePostDataLabel"
|
||||||
</hbox>
|
for="enginePostData"
|
||||||
|
data-l10n-id="add-engine-post-data"
|
||||||
|
/>
|
||||||
|
<html:input id="enginePostData" type="text" />
|
||||||
|
<html:div class="error-label">valid</html:div>
|
||||||
|
</html:div>
|
||||||
|
|
||||||
|
<html:div class="dialogRow">
|
||||||
|
<html:label
|
||||||
|
id="engineAliasLabel"
|
||||||
|
for="engineAlias"
|
||||||
|
data-l10n-id="add-engine-keyword2"
|
||||||
|
/>
|
||||||
|
<html:input id="engineAlias" type="text" />
|
||||||
|
<html:div class="error-label">valid</html:div>
|
||||||
</html:div>
|
</html:div>
|
||||||
|
|
||||||
<html:div class="dialogRow" id="suggestUrlRow">
|
<html:div class="dialogRow" id="suggestUrlRow">
|
||||||
@@ -74,36 +80,8 @@
|
|||||||
for="suggestUrl"
|
for="suggestUrl"
|
||||||
data-l10n-id="add-engine-suggest-url"
|
data-l10n-id="add-engine-suggest-url"
|
||||||
/>
|
/>
|
||||||
<hbox>
|
<html:input id="suggestUrl" type="url" />
|
||||||
<html:input id="suggestUrl" type="url" pattern="https?:.*" />
|
<html:div class="error-label">valid</html:div>
|
||||||
</hbox>
|
|
||||||
</html:div>
|
|
||||||
|
|
||||||
<html:div class="dialogRow" id="enginePostDataRow" hidden="hidden">
|
|
||||||
<html:label
|
|
||||||
id="enginePostDataLabel"
|
|
||||||
for="enginePostData"
|
|
||||||
data-l10n-id="add-engine-post-data"
|
|
||||||
/>
|
|
||||||
<hbox>
|
|
||||||
<html:input id="enginePostData" type="text" />
|
|
||||||
</hbox>
|
|
||||||
</html:div>
|
|
||||||
|
|
||||||
<html:div class="dialogRow">
|
|
||||||
<html:span
|
|
||||||
id="engineAliasExists"
|
|
||||||
hidden="hidden"
|
|
||||||
data-l10n-id="engine-keyword-exists"
|
|
||||||
/>
|
|
||||||
<html:label
|
|
||||||
id="engineAliasLabel"
|
|
||||||
for="engineAlias"
|
|
||||||
data-l10n-id="add-engine-keyword"
|
|
||||||
/>
|
|
||||||
<hbox>
|
|
||||||
<html:input id="engineAlias" type="text" />
|
|
||||||
</hbox>
|
|
||||||
</html:div>
|
</html:div>
|
||||||
</html:form>
|
</html:form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|||||||
@@ -315,7 +315,13 @@ urlbar-search-mode-actions-en = Actions
|
|||||||
## Add search engine dialog
|
## Add search engine dialog
|
||||||
## These strings will be moved to search.ftl once the dialog is finished.
|
## These strings will be moved to search.ftl once the dialog is finished.
|
||||||
|
|
||||||
add-engine-suggest-url = Search suggestion URL, use %s in place of the search term
|
add-engine-url2 = URL with %s in place of search term
|
||||||
|
|
||||||
|
add-engine-keyword2 = Keyword (optional)
|
||||||
|
|
||||||
|
add-engine-post-data = Post data with %s in place of search term
|
||||||
|
|
||||||
|
add-engine-suggest-url = Suggestions URL with %s in place of search term (optional)
|
||||||
|
|
||||||
# Variables:
|
# Variables:
|
||||||
# $name (string) - Name of a search engine.
|
# $name (string) - Name of a search engine.
|
||||||
@@ -329,4 +335,22 @@ edit-engine-window =
|
|||||||
.title = Edit Search Engine
|
.title = Edit Search Engine
|
||||||
.style = min-width: 32em;
|
.style = min-width: 32em;
|
||||||
|
|
||||||
add-engine-post-data = Post data
|
## The following placeholders are shown when adding a new engine.
|
||||||
|
|
||||||
|
add-engine-name-placeholder =
|
||||||
|
.placeholder = e.g., Mozilla Developer Network
|
||||||
|
|
||||||
|
add-engine-url-placeholder =
|
||||||
|
.placeholder = e.g., https://developer.mozilla.com/search?q=%s
|
||||||
|
|
||||||
|
add-engine-keyword-placeholder =
|
||||||
|
.placeholder = e.g., @mdn
|
||||||
|
|
||||||
|
## The following strings are used as error labels.
|
||||||
|
|
||||||
|
add-engine-keyword-exists = That keyword is already being used. Try a different one.
|
||||||
|
add-engine-name-exists = That name is already being used. Please choose a different one.
|
||||||
|
add-engine-no-name = Please add a name.
|
||||||
|
add-engine-no-url = Please enter a URL.
|
||||||
|
add-engine-invalid-url = That URL doesn’t look right. Please check it and try again.
|
||||||
|
add-engine-invalid-protocol = That URL doesn’t look right. Use a URL that starts with http or https.
|
||||||
|
|||||||
@@ -63,13 +63,6 @@ add-engine-button = Add Custom Engine
|
|||||||
|
|
||||||
add-engine-name = Search engine name
|
add-engine-name = Search engine name
|
||||||
|
|
||||||
add-engine-keyword = Keyword
|
|
||||||
|
|
||||||
add-engine-url = Engine URL, use %s in place of the search term
|
|
||||||
|
|
||||||
add-engine-dialog =
|
add-engine-dialog =
|
||||||
.buttonlabelaccept = Add Engine
|
.buttonlabelaccept = Add Engine
|
||||||
.buttonaccesskeyaccept = A
|
.buttonaccesskeyaccept = A
|
||||||
|
|
||||||
engine-name-exists = An engine with that name already exists
|
|
||||||
engine-keyword-exists = An engine with that keyword already exists
|
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export class UserSearchEngine extends SearchEngine {
|
|||||||
*
|
*
|
||||||
* @param {string} newName
|
* @param {string} newName
|
||||||
* The new name.
|
* The new name.
|
||||||
* @returns {bool}
|
* @returns {boolean}
|
||||||
* Whether the name was changed successfully.
|
* Whether the name was changed successfully.
|
||||||
*/
|
*/
|
||||||
rename(newName) {
|
rename(newName) {
|
||||||
|
|||||||
@@ -47,12 +47,3 @@ html|button {
|
|||||||
/* XUL button min-width */
|
/* XUL button min-width */
|
||||||
min-width: 79px;
|
min-width: 79px;
|
||||||
}
|
}
|
||||||
|
|
||||||
html|input[type="email"],
|
|
||||||
html|input[type="tel"],
|
|
||||||
html|input[type="text"],
|
|
||||||
html|input[type="password"],
|
|
||||||
html|input[type="number"],
|
|
||||||
html|textarea {
|
|
||||||
margin: 4px;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -200,6 +200,8 @@ CO01:
|
|||||||
# toolkit/neterror/certError.ftl
|
# toolkit/neterror/certError.ftl
|
||||||
- cert-error-mitm-mozilla
|
- cert-error-mitm-mozilla
|
||||||
- cert-error-mitm-connection
|
- cert-error-mitm-connection
|
||||||
|
# browser/components/urlbar/content/enUS-searchFeatures.ftl
|
||||||
|
- add-engine-name-placeholder
|
||||||
files:
|
files:
|
||||||
- browser/locales/en-US/browser/profile/default-bookmarks.ftl
|
- browser/locales/en-US/browser/profile/default-bookmarks.ftl
|
||||||
- toolkit/locales/en-US/toolkit/about/aboutMozilla.ftl
|
- toolkit/locales/en-US/toolkit/about/aboutMozilla.ftl
|
||||||
|
|||||||
Reference in New Issue
Block a user