Bug 1496827 - Remove the "notification" binding. r=bgrins

Differential Revision: https://phabricator.services.mozilla.com/D11650
This commit is contained in:
Paolo Amadini
2018-11-13 12:56:42 +00:00
parent 275c8989dc
commit d03e533476
38 changed files with 616 additions and 696 deletions

View File

@@ -215,7 +215,6 @@ var CaptivePortalWatcher = {
// Returning true prevents the notification from closing. // Returning true prevents the notification from closing.
return true; return true;
}, },
isDefault: true,
}, },
]; ];

View File

@@ -482,8 +482,7 @@ var gPluginHandler = {
let crashurl = formatURL("app.support.baseURL", true); let crashurl = formatURL("app.support.baseURL", true);
crashurl += "plugin-crashed-notificationbar"; crashurl += "plugin-crashed-notificationbar";
link.href = crashurl; link.href = crashurl;
let description = notification.ownerDocument.getAnonymousElementByAttribute(notification, "anonid", "messageText"); notification.messageText.appendChild(link);
description.appendChild(link);
}, },
}; };

View File

@@ -1058,11 +1058,6 @@ browser[tabmodalPromptShowing] {
-moz-box-align: end; -moz-box-align: end;
} }
/* Translation */
notification[value="translation"] {
-moz-binding: url("chrome://browser/content/translation-infobar.xml#translationbar");
}
/*** Visibility of downloads indicator controls ***/ /*** Visibility of downloads indicator controls ***/
/* Bug 924050: If we've loaded the indicator, for now we hide it in the menu panel, /* Bug 924050: If we've loaded the indicator, for now we hide it in the menu panel,

View File

@@ -242,6 +242,11 @@ XPCOMUtils.defineLazyGetter(this, "Win7Features", function() {
return null; return null;
}); });
customElements.setElementCreationCallback("translation-notification", () => {
Services.scriptloader.loadSubScript(
"chrome://browser/content/translation-notification.js", window);
});
var gBrowser; var gBrowser;
var gLastValidURLStr = ""; var gLastValidURLStr = "";
var gInPrintPreviewMode = false; var gInPrintPreviewMode = false;

View File

@@ -47,10 +47,10 @@ async function test_decoder_doctor_notification(data, notificationMessage,
} }
ok(notification, "Got decoder-doctor-notification notification"); ok(notification, "Got decoder-doctor-notification notification");
is(notification.getAttribute("label"), notificationMessage, is(notification.messageText.textContent, notificationMessage,
"notification message should match expectation"); "notification message should match expectation");
let button = notification.children[0]; let button = notification.querySelector("button");
if (!label) { if (!label) {
ok(!button, "There should not be button"); ok(!button, "There should not be button");
return; return;

View File

@@ -100,7 +100,7 @@ async function testRealRefresh(refreshPage, delay) {
let notification = notificationBox.currentNotification; let notification = notificationBox.currentNotification;
ok(notification, "Notification should be visible"); ok(notification, "Notification should be visible");
is(notification.value, "refresh-blocked", is(notification.getAttribute("value"), "refresh-blocked",
"Should be showing the right notification"); "Should be showing the right notification");
// Then click the button to allow the refresh. // Then click the button to allow the refresh.

View File

@@ -45,7 +45,7 @@ add_task(async function() {
let notification = box.currentNotification; let notification = box.currentNotification;
ok(notification, "Notification should be visible"); ok(notification, "Notification should be visible");
is(notification.value, "drmContentDisabled", is(notification.getAttribute("value"), "drmContentDisabled",
"Should be showing the right notification"); "Should be showing the right notification");
// Verify the "Enable DRM" button is there. // Verify the "Enable DRM" button is there.

View File

@@ -27,7 +27,7 @@ add_task(async function() {
ok(notification, "Infobar was shown."); ok(notification, "Infobar was shown.");
is(notification.priority, notificationBox.PRIORITY_WARNING_MEDIUM, is(notification.priority, notificationBox.PRIORITY_WARNING_MEDIUM,
"Correct priority."); "Correct priority.");
is(notification.getAttribute("label"), is(notification.messageText.textContent,
"The GlobalTestPlugin plugin has crashed.", "The GlobalTestPlugin plugin has crashed.",
"Correct message."); "Correct message.");
}); });

View File

@@ -14,19 +14,8 @@ function promiseNotification(aBrowser, value, expected, input) {
let notificationBox = aBrowser.getNotificationBox(aBrowser.selectedBrowser); let notificationBox = aBrowser.getNotificationBox(aBrowser.selectedBrowser);
if (expected) { if (expected) {
info("Waiting for " + value + " notification"); info("Waiting for " + value + " notification");
let checkForNotification = function() { resolve(BrowserTestUtils.waitForNotificationInNotificationBox(
if (notificationBox.getNotificationWithValue(value)) { notificationBox, value));
info("Saw the notification");
notificationObserver.disconnect();
notificationObserver = null;
resolve();
}
};
if (notificationObserver) {
notificationObserver.disconnect();
}
notificationObserver = new MutationObserver(checkForNotification);
notificationObserver.observe(notificationBox.stack, {childList: true});
} else { } else {
setTimeout(() => { setTimeout(() => {
is(notificationBox.getNotificationWithValue(value), null, is(notificationBox.getNotificationWithValue(value), null,
@@ -152,7 +141,7 @@ function get_test_function_for_localhost_with_hostname(hostName, isPrivate) {
let notificationBox = browser.getNotificationBox(tab.linkedBrowser); let notificationBox = browser.getNotificationBox(tab.linkedBrowser);
let notification = notificationBox.getNotificationWithValue("keyword-uri-fixup"); let notification = notificationBox.getNotificationWithValue("keyword-uri-fixup");
let docLoadPromise = waitForDocLoadAndStopIt("http://" + hostName + "/", tab.linkedBrowser); let docLoadPromise = waitForDocLoadAndStopIt("http://" + hostName + "/", tab.linkedBrowser);
notification.querySelector(".notification-button-default").click(); notification.querySelector("button").click();
// check pref value // check pref value
let prefValue = Services.prefs.getBoolPref(pref); let prefValue = Services.prefs.getBoolPref(pref);

View File

@@ -49,7 +49,7 @@ add_task(async function test_slow_content_script() {
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/"); let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
let notification = await BrowserTestUtils.waitForGlobalNotificationBar(window, "process-hang"); let notification = await BrowserTestUtils.waitForGlobalNotificationBar(window, "process-hang");
let text = document.getAnonymousElementByAttribute(notification, "anonid", "messageText").textContent; let text = notification.messageText.textContent;
ok(text.includes("\u201cSlow Script Extension\u201d"), ok(text.includes("\u201cSlow Script Extension\u201d"),
"Label is correct"); "Label is correct");

View File

@@ -22,13 +22,12 @@ add_task(async function() {
finish(); finish();
return; return;
} }
is(notification.type, "info", "We expect this notification to have the type of 'info'."); is(notification.getAttribute("type"), "info",
isnot(notification.image, null, "We expect this notification to have an icon."); "We expect this notification to have the type of 'info'.");
ok(notification.messageImage.getAttribute("src"),
"We expect this notification to have an icon.");
let buttons = notification.getElementsByClassName("notification-button-default"); let buttons = notification.getElementsByClassName("notification-button");
is(buttons.length, 1, "We expect see one default button.");
buttons = notification.getElementsByClassName("notification-button");
is(buttons.length, 1, "We expect see one button."); is(buttons.length, 1, "We expect see one button.");
let button = buttons[0]; let button = buttons[0];

View File

@@ -333,7 +333,8 @@ function testShowNotification() {
ok(updateBox, "Update notification box should have been displayed"); ok(updateBox, "Update notification box should have been displayed");
if (updateBox) { if (updateBox) {
if (testCase.notificationText) { if (testCase.notificationText) {
is(updateBox.label, testCase.notificationText, "Update notification box " + is(updateBox.messageText.textContent, testCase.notificationText,
"Update notification box " +
"should have the label provided by the update"); "should have the label provided by the update");
} }
if (testCase.notificationButtonLabel) { if (testCase.notificationButtonLabel) {

View File

@@ -228,7 +228,8 @@ TranslationUI.prototype = {
showTranslationInfoBar() { showTranslationInfoBar() {
let notificationBox = this.notificationBox; let notificationBox = this.notificationBox;
let notif = notificationBox.appendNotification("", "translation", null, let notif = notificationBox.appendNotification("", "translation", null,
notificationBox.PRIORITY_INFO_HIGH); notificationBox.PRIORITY_INFO_HIGH, null, null,
"translation-notification");
notif.init(this); notif.init(this);
return notif; return notif;
}, },

View File

@@ -0,0 +1,11 @@
"use strict";
module.exports = {
"env": {
"mozilla/browser-window": true,
},
"plugins": [
"mozilla",
]
};

View File

@@ -2,5 +2,5 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this # 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/. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
browser.jar: browser.jar:
content/browser/translation-infobar.xml content/browser/translation-notification.js
content/browser/microsoft-translator-attribution.png content/browser/microsoft-translator-attribution.png

View File

@@ -0,0 +1,7 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
JAR_MANIFESTS += ['jar.mn']

View File

@@ -0,0 +1,349 @@
/* 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/. */
"use strict";
class MozTranslationNotification extends MozElements.Notification {
connectedCallback() {
this.appendChild(MozXULElement.parseXULToFragment(`
<hbox anonid="details" align="center" flex="1">
<image class="translate-infobar-element messageImage"/>
<panel anonid="welcomePanel" class="translation-welcome-panel" type="arrow" align="start">
<image class="translation-welcome-logo"/>
<vbox flex="1" class="translation-welcome-content">
<description class="translation-welcome-headline" anonid="welcomeHeadline"/>
<description class="translation-welcome-body" anonid="welcomeBody"/>
<hbox align="center">
<label anonid="learnMore" class="plain text-link" onclick="openTrustedLinkIn('https://support.mozilla.org/kb/automatic-translation', 'tab'); this.parentNode.parentNode.parentNode.hidePopup();"/>
<spacer flex="1"/>
<button class="translate-infobar-element" anonid="thanksButton" onclick="this.parentNode.parentNode.parentNode.hidePopup();"/>
</hbox>
</vbox>
</panel>
<deck anonid="translationStates" selectedIndex="0">
<hbox class="translate-offer-box" align="center">
<label class="translate-infobar-element" value="&translation.thisPageIsIn.label;"/>
<menulist class="translate-infobar-element" anonid="detectedLanguage">
<menupopup/>
</menulist>
<label class="translate-infobar-element" value="&translation.translateThisPage.label;"/>
<button class="translate-infobar-element" label="&translation.translate.button;" anonid="translate" oncommand="this.closest('notification').translate();"/>
<button class="translate-infobar-element" label="&translation.notNow.button;" anonid="notNow" oncommand="this.closest('notification').closeCommand();"/>
</hbox>
<vbox class="translating-box" pack="center">
<label class="translate-infobar-element" value="&translation.translatingContent.label;"/>
</vbox>
<hbox class="translated-box" align="center">
<label class="translate-infobar-element" value="&translation.translatedFrom.label;"/>
<menulist class="translate-infobar-element" anonid="fromLanguage" oncommand="this.closest('notification').translate();">
<menupopup/>
</menulist>
<label class="translate-infobar-element" value="&translation.translatedTo.label;"/>
<menulist class="translate-infobar-element" anonid="toLanguage" oncommand="this.closest('notification').translate();">
<menupopup/>
</menulist>
<label class="translate-infobar-element" value="&translation.translatedToSuffix.label;"/>
<button anonid="showOriginal" class="translate-infobar-element" label="&translation.showOriginal.button;" oncommand="this.closest('notification').showOriginal();"/>
<button anonid="showTranslation" class="translate-infobar-element" label="&translation.showTranslation.button;" oncommand="this.closest('notification').showTranslation();"/>
</hbox>
<hbox class="translation-error" align="center">
<label class="translate-infobar-element" value="&translation.errorTranslating.label;"/>
<button class="translate-infobar-element" label="&translation.tryAgain.button;" anonid="tryAgain" oncommand="this.closest('notification').translate();"/>
</hbox>
<vbox class="translation-unavailable" pack="center">
<label class="translate-infobar-element" value="&translation.serviceUnavailable.label;"/>
</vbox>
</deck>
<spacer flex="1"/>
<button type="menu" class="translate-infobar-element options-menu-button" anonid="options" label="&translation.options.menu;">
<menupopup class="translation-menupopup cui-widget-panel cui-widget-panelview
cui-widget-panelWithFooter PanelUI-subView" onpopupshowing="this.closest('notification').optionsShowing();">
<menuitem anonid="neverForLanguage" oncommand="this.closest('notification').neverForLanguage();"/>
<menuitem anonid="neverForSite" oncommand="this.closest('notification').neverForSite();" label="&translation.options.neverForSite.label;" accesskey="&translation.options.neverForSite.accesskey;"/>
<menuseparator/>
<menuitem oncommand="openPreferences('paneGeneral', {origin:'translationInfobar'});" label="&translation.options.preferences.label;" accesskey="&translation.options.preferences.accesskey;"/>
<menuitem class="subviewbutton panel-subview-footer" oncommand="this.closest('notification').openProviderAttribution();">
<deck anonid="translationEngine" selectedIndex="0">
<hbox class="translation-attribution">
<label/>
<image src="chrome://browser/content/microsoft-translator-attribution.png" aria-label="Microsoft Translator"/>
<label/>
</hbox>
<label class="translation-attribution"/>
</deck>
</menuitem>
</menupopup>
</button>
</hbox>
<toolbarbutton anonid="closeButton" ondblclick="event.stopPropagation();"
class="messageCloseButton close-icon tabbable"
tooltiptext="&closeNotification.tooltip;"
oncommand="this.parentNode.closeCommand();"/>
`, [
"chrome://global/locale/notification.dtd",
"chrome://browser/locale/translation.dtd",
]));
for (let [propertyName, selector] of [
["details", "[anonid=details]"],
["messageImage", ".messageImage"],
["spacer", "[anonid=spacer]"],
]) {
this[propertyName] = this.querySelector(selector);
}
}
set state(val) {
let deck = this._getAnonElt("translationStates");
let activeElt = document.activeElement;
if (activeElt && deck.contains(activeElt))
activeElt.blur();
let stateName;
for (let name of ["OFFER", "TRANSLATING", "TRANSLATED", "ERROR"]) {
if (Translation["STATE_" + name] == val) {
stateName = name.toLowerCase();
break;
}
}
this.setAttribute("state", stateName);
if (val == Translation.STATE_TRANSLATED)
this._handleButtonHiding();
deck.selectedIndex = val;
}
get state() {
return this._getAnonElt("translationStates").selectedIndex;
}
init(aTranslation) {
this.translation = aTranslation;
let sortByLocalizedName = function(aList) {
let names = Services.intl.getLanguageDisplayNames(undefined, aList);
return aList.map((code, i) => [code, names[i]])
.sort((a, b) => a[1].localeCompare(b[1]));
};
// Fill the lists of supported source languages.
let detectedLanguage = this._getAnonElt("detectedLanguage");
let fromLanguage = this._getAnonElt("fromLanguage");
let sourceLanguages =
sortByLocalizedName(Translation.supportedSourceLanguages);
for (let [code, name] of sourceLanguages) {
detectedLanguage.appendItem(name, code);
fromLanguage.appendItem(name, code);
}
detectedLanguage.value = this.translation.detectedLanguage;
// translatedFrom is only set if we have already translated this page.
if (aTranslation.translatedFrom)
fromLanguage.value = aTranslation.translatedFrom;
// Fill the list of supported target languages.
let toLanguage = this._getAnonElt("toLanguage");
let targetLanguages =
sortByLocalizedName(Translation.supportedTargetLanguages);
for (let [code, name] of targetLanguages)
toLanguage.appendItem(name, code);
if (aTranslation.translatedTo)
toLanguage.value = aTranslation.translatedTo;
if (aTranslation.state)
this.state = aTranslation.state;
// Show attribution for the preferred translator.
let engineIndex = Object.keys(Translation.supportedEngines)
.indexOf(Translation.translationEngine);
// We currently only have attribution for the Bing and Yandex engines.
if (engineIndex >= 0) {
--engineIndex;
}
let attributionNode = this._getAnonElt("translationEngine");
if (engineIndex != -1) {
attributionNode.selectedIndex = engineIndex;
} else {
// Hide the attribution menuitem
let footer = attributionNode.parentNode;
footer.hidden = true;
// Make the 'Translation preferences' item the new footer.
footer = footer.previousSibling;
footer.setAttribute("class", "subviewbutton panel-subview-footer");
// And hide the menuseparator.
footer.previousSibling.hidden = true;
}
const kWelcomePref = "browser.translation.ui.welcomeMessageShown";
if (Services.prefs.prefHasUserValue(kWelcomePref) ||
this.translation.browser != gBrowser.selectedBrowser)
return;
this.addEventListener("transitionend", function() {
// These strings are hardcoded because they need to reach beta
// without riding the trains.
let localizedStrings = {
en: ["Hey look! It's something new!",
"Now the Web is even more accessible with our new in-page translation feature. Click the translate button to try it!",
"Learn more.",
"Thanks",
],
"es-AR": ["\xA1Mir\xE1! \xA1Hay algo nuevo!",
"Ahora la web es a\xFAn m\xE1s accesible con nuestra nueva funcionalidad de traducci\xF3n integrada. \xA1Hac\xE9 clic en el bot\xF3n traducir para probarla!",
"Conoc\xE9 m\xE1s.",
"Gracias",
],
"es-ES": ["\xA1Mira! \xA1Hay algo nuevo!",
"Con la nueva funcionalidad de traducci\xF3n integrada, ahora la Web es a\xFAn m\xE1s accesible. \xA1Pulsa el bot\xF3n Traducir y pru\xE9bala!",
"M\xE1s informaci\xF3n.",
"Gracias",
],
pl: ["Sp\xF3jrz tutaj! To co\u015B nowego!",
"Sie\u0107 sta\u0142a si\u0119 w\u0142a\u015Bnie jeszcze bardziej dost\u0119pna dzi\u0119ki opcji bezpo\u015Bredniego t\u0142umaczenia stron. Kliknij przycisk t\u0142umaczenia, aby spr\xF3bowa\u0107!",
"Dowiedz si\u0119 wi\u0119cej",
"Dzi\u0119kuj\u0119",
],
tr: ["Bak\u0131n, burada yeni bir \u015Fey var!",
"Yeni sayfa i\xE7i \xE7eviri \xF6zelli\u011Fimiz sayesinde Web art\u0131k \xE7ok daha anla\u015F\u0131l\u0131r olacak. Denemek i\xE7in \xC7evir d\xFC\u011Fmesine t\u0131klay\u0131n!",
"Daha fazla bilgi al\u0131n.",
"Te\u015Fekk\xFCrler",
],
vi: ["Nh\xECn n\xE0y! \u0110\u1ED3 m\u1EDBi!",
"Gi\u1EDD \u0111\xE2y ch\xFAng ta c\xF3 th\u1EC3 ti\u1EBFp c\u1EADn web d\u1EC5 d\xE0ng h\u01A1n n\u1EEFa v\u1EDBi t\xEDnh n\u0103ng d\u1ECBch ngay trong trang. Hay nh\u1EA5n n\xFAt d\u1ECBch \u0111\u1EC3 th\u1EED!",
"T\xECm hi\u1EC3u th\xEAm.",
"C\u1EA3m \u01A1n",
],
};
let locale = Services.locale.appLocaleAsLangTag;
if (!(locale in localizedStrings))
locale = "en";
let strings = localizedStrings[locale];
this._getAnonElt("welcomeHeadline").setAttribute("value", strings[0]);
this._getAnonElt("welcomeBody").textContent = strings[1];
this._getAnonElt("learnMore").setAttribute("value", strings[2]);
this._getAnonElt("thanksButton").setAttribute("label", strings[3]);
let panel = this._getAnonElt("welcomePanel");
panel.openPopup(this._getAnonElt("messageImage"),
"bottomcenter topleft");
Services.prefs.setBoolPref(kWelcomePref, true);
}, { once: true });
}
_getAnonElt(aAnonId) {
return this.querySelector("[anonid=" + aAnonId + "]");
}
translate() {
if (this.state == Translation.STATE_OFFER) {
this._getAnonElt("fromLanguage").value =
this._getAnonElt("detectedLanguage").value;
this._getAnonElt("toLanguage").value =
Translation.defaultTargetLanguage;
}
this.translation.translate(this._getAnonElt("fromLanguage").value,
this._getAnonElt("toLanguage").value);
}
/**
* To be called when the infobar should be closed per user's wish (e.g.
* by clicking the notification's close button
*/
closeCommand() {
this.close();
this.translation.infobarClosed();
}
_handleButtonHiding() {
let originalShown = this.translation.originalShown;
this._getAnonElt("showOriginal").hidden = originalShown;
this._getAnonElt("showTranslation").hidden = !originalShown;
}
showOriginal() {
this.translation.showOriginalContent();
this._handleButtonHiding();
}
showTranslation() {
this.translation.showTranslatedContent();
this._handleButtonHiding();
}
optionsShowing() {
// Get the source language name.
let lang;
if (this.state == Translation.STATE_OFFER)
lang = this._getAnonElt("detectedLanguage").value;
else {
lang = this._getAnonElt("fromLanguage").value;
// If we have never attempted to translate the page before the
// service became unavailable, "fromLanguage" isn't set.
if (!lang && this.state == Translation.STATE_UNAVAILABLE)
lang = this.translation.detectedLanguage;
}
let langName = Services.intl.getLanguageDisplayNames(undefined, [lang])[0];
// Set the label and accesskey on the menuitem.
let bundle =
Services.strings.createBundle("chrome://browser/locale/translation.properties");
let item = this._getAnonElt("neverForLanguage");
const kStrId = "translation.options.neverForLanguage";
item.setAttribute("label",
bundle.formatStringFromName(kStrId + ".label", [langName], 1));
item.setAttribute("accesskey",
bundle.GetStringFromName(kStrId + ".accesskey"));
item.langCode = lang;
// We may need to disable the menuitems if they have already been used.
// Check if translation is already disabled for this language:
let neverForLangs =
Services.prefs.getCharPref("browser.translation.neverForLanguages");
item.disabled = neverForLangs.split(",").includes(lang);
// Check if translation is disabled for the domain:
let uri = this.translation.browser.currentURI;
let perms = Services.perms;
item = this._getAnonElt("neverForSite");
item.disabled =
perms.testExactPermission(uri, "translate") == perms.DENY_ACTION;
}
neverForLanguage() {
const kPrefName = "browser.translation.neverForLanguages";
let val = Services.prefs.getCharPref(kPrefName);
if (val)
val += ",";
val += this._getAnonElt("neverForLanguage").langCode;
Services.prefs.setCharPref(kPrefName, val);
this.closeCommand();
}
neverForSite() {
let uri = this.translation.browser.currentURI;
let perms = Services.perms;
perms.add(uri, "translate", perms.DENY_ACTION);
this.closeCommand();
}
openProviderAttribution() {
Translation.openProviderAttribution();
}
}
customElements.define("translation-notification", MozTranslationNotification,
{ extends: "notification" });

View File

@@ -2,6 +2,10 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this # 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/. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
DIRS += [
'content',
]
with Files("**"): with Files("**"):
BUG_COMPONENT = ("Firefox", "Translation") BUG_COMPONENT = ("Firefox", "Translation")
@@ -17,8 +21,6 @@ EXTRA_JS_MODULES.translation = [
'YandexTranslator.jsm' 'YandexTranslator.jsm'
] ]
JAR_MANIFESTS += ['jar.mn']
BROWSER_CHROME_MANIFESTS += [ BROWSER_CHROME_MANIFESTS += [
'test/browser.ini' 'test/browser.ini'
] ]

View File

@@ -1,439 +0,0 @@
<?xml version="1.0"?>
<!-- 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/. -->
<!DOCTYPE bindings [
<!ENTITY % notificationDTD SYSTEM "chrome://global/locale/notification.dtd">
%notificationDTD;
<!ENTITY % translationDTD SYSTEM "chrome://browser/locale/translation.dtd" >
%translationDTD;
]>
<bindings id="translationBindings"
xmlns="http://www.mozilla.org/xbl"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:xbl="http://www.mozilla.org/xbl">
<binding id="translationbar" extends="chrome://global/content/bindings/notification.xml#notification">
<content>
<xul:hbox anonid="details" align="center" flex="1">
<xul:image class="translate-infobar-element messageImage"
anonid="messageImage"/>
<xul:panel anonid="welcomePanel" class="translation-welcome-panel"
type="arrow" align="start">
<xul:image class="translation-welcome-logo"/>
<xul:vbox flex="1" class="translation-welcome-content">
<xul:description class="translation-welcome-headline"
anonid="welcomeHeadline"/>
<xul:description class="translation-welcome-body" anonid="welcomeBody"/>
<xul:hbox align="center">
<xul:label anonid="learnMore" class="plain text-link"
onclick="openTrustedLinkIn('https://support.mozilla.org/kb/automatic-translation', 'tab'); this.parentNode.parentNode.parentNode.hidePopup();"/>
<xul:spacer flex="1"/>
<xul:button class="translate-infobar-element" anonid="thanksButton"
onclick="this.parentNode.parentNode.parentNode.hidePopup();"/>
</xul:hbox>
</xul:vbox>
</xul:panel>
<xul:deck anonid="translationStates" selectedIndex="0">
<!-- offer to translate -->
<xul:hbox class="translate-offer-box" align="center">
<xul:label class="translate-infobar-element" value="&translation.thisPageIsIn.label;"/>
<xul:menulist class="translate-infobar-element" anonid="detectedLanguage">
<xul:menupopup/>
</xul:menulist>
<xul:label class="translate-infobar-element" value="&translation.translateThisPage.label;"/>
<xul:button class="translate-infobar-element"
label="&translation.translate.button;"
anonid="translate"
oncommand="document.getBindingParent(this).translate();"/>
<xul:button class="translate-infobar-element"
label="&translation.notNow.button;" anonid="notNow"
oncommand="document.getBindingParent(this).closeCommand();"/>
</xul:hbox>
<!-- translating -->
<xul:vbox class="translating-box" pack="center">
<xul:label class="translate-infobar-element"
value="&translation.translatingContent.label;"/>
</xul:vbox>
<!-- translated -->
<xul:hbox class="translated-box" align="center">
<xul:label class="translate-infobar-element"
value="&translation.translatedFrom.label;"/>
<xul:menulist class="translate-infobar-element"
anonid="fromLanguage"
oncommand="document.getBindingParent(this).translate()">
<xul:menupopup/>
</xul:menulist>
<xul:label class="translate-infobar-element"
value="&translation.translatedTo.label;"/>
<xul:menulist class="translate-infobar-element"
anonid="toLanguage"
oncommand="document.getBindingParent(this).translate()">
<xul:menupopup/>
</xul:menulist>
<xul:label class="translate-infobar-element"
value="&translation.translatedToSuffix.label;"/>
<xul:button anonid="showOriginal"
class="translate-infobar-element"
label="&translation.showOriginal.button;"
oncommand="document.getBindingParent(this).showOriginal();"/>
<xul:button anonid="showTranslation"
class="translate-infobar-element"
label="&translation.showTranslation.button;"
oncommand="document.getBindingParent(this).showTranslation();"/>
</xul:hbox>
<!-- error -->
<xul:hbox class="translation-error" align="center">
<xul:label class="translate-infobar-element"
value="&translation.errorTranslating.label;"/>
<xul:button class="translate-infobar-element"
label="&translation.tryAgain.button;"
anonid="tryAgain"
oncommand="document.getBindingParent(this).translate();"/>
</xul:hbox>
<!-- unavailable -->
<xul:vbox class="translation-unavailable" pack="center">
<xul:label class="translate-infobar-element"
value="&translation.serviceUnavailable.label;"/>
</xul:vbox>
</xul:deck>
<xul:spacer flex="1"/>
<xul:button type="menu"
class="translate-infobar-element options-menu-button"
anonid="options"
label="&translation.options.menu;">
<xul:menupopup class="translation-menupopup cui-widget-panel cui-widget-panelview
cui-widget-panelWithFooter PanelUI-subView"
onpopupshowing="document.getBindingParent(this).optionsShowing();">
<xul:menuitem anonid="neverForLanguage"
oncommand="document.getBindingParent(this).neverForLanguage();"/>
<xul:menuitem anonid="neverForSite"
oncommand="document.getBindingParent(this).neverForSite();"
label="&translation.options.neverForSite.label;"
accesskey="&translation.options.neverForSite.accesskey;"/>
<xul:menuseparator/>
<xul:menuitem oncommand="openPreferences('paneGeneral', {origin:'translationInfobar'});"
label="&translation.options.preferences.label;"
accesskey="&translation.options.preferences.accesskey;"/>
<xul:menuitem class="subviewbutton panel-subview-footer"
oncommand="document.getBindingParent(this).openProviderAttribution();">
<xul:deck anonid="translationEngine" selectedIndex="0">
<xul:hbox class="translation-attribution">
<xul:label>&translation.options.attribution.beforeLogo;</xul:label>
<xul:image src="chrome://browser/content/microsoft-translator-attribution.png"
aria-label="Microsoft Translator"/>
<xul:label>&translation.options.attribution.afterLogo;</xul:label>
</xul:hbox>
<xul:label class="translation-attribution">&translation.options.attribution.yandexTranslate;</xul:label>
</xul:deck>
</xul:menuitem>
</xul:menupopup>
</xul:button>
</xul:hbox>
<xul:toolbarbutton ondblclick="event.stopPropagation();"
anonid="closeButton"
class="messageCloseButton close-icon tabbable"
xbl:inherits="hidden=hideclose"
tooltiptext="&closeNotification.tooltip;"
oncommand="document.getBindingParent(this).closeCommand();"/>
</content>
<implementation>
<property name="state"
onget="return this._getAnonElt('translationStates').selectedIndex;">
<setter>
<![CDATA[
let deck = this._getAnonElt("translationStates");
let activeElt = document.activeElement;
if (activeElt && deck.contains(activeElt))
activeElt.blur();
let stateName;
for (let name of ["OFFER", "TRANSLATING", "TRANSLATED", "ERROR"]) {
if (Translation["STATE_" + name] == val) {
stateName = name.toLowerCase();
break;
}
}
this.setAttribute("state", stateName);
if (val == Translation.STATE_TRANSLATED)
this._handleButtonHiding();
deck.selectedIndex = val;
]]>
</setter>
</property>
<method name="init">
<parameter name="aTranslation"/>
<body>
<![CDATA[
this.translation = aTranslation;
let sortByLocalizedName = function(aList) {
let names = Services.intl.getLanguageDisplayNames(undefined, aList);
return aList.map((code, i) => [code, names[i]])
.sort((a, b) => a[1].localeCompare(b[1]));
};
// Fill the lists of supported source languages.
let detectedLanguage = this._getAnonElt("detectedLanguage");
let fromLanguage = this._getAnonElt("fromLanguage");
let sourceLanguages =
sortByLocalizedName(Translation.supportedSourceLanguages);
for (let [code, name] of sourceLanguages) {
detectedLanguage.appendItem(name, code);
fromLanguage.appendItem(name, code);
}
detectedLanguage.value = this.translation.detectedLanguage;
// translatedFrom is only set if we have already translated this page.
if (aTranslation.translatedFrom)
fromLanguage.value = aTranslation.translatedFrom;
// Fill the list of supported target languages.
let toLanguage = this._getAnonElt("toLanguage");
let targetLanguages =
sortByLocalizedName(Translation.supportedTargetLanguages);
for (let [code, name] of targetLanguages)
toLanguage.appendItem(name, code);
if (aTranslation.translatedTo)
toLanguage.value = aTranslation.translatedTo;
if (aTranslation.state)
this.state = aTranslation.state;
// Show attribution for the preferred translator.
let engineIndex = Object.keys(Translation.supportedEngines)
.indexOf(Translation.translationEngine);
// We currently only have attribution for the Bing and Yandex engines.
if (engineIndex >= 0) {
--engineIndex;
}
let attributionNode = this._getAnonElt("translationEngine");
if (engineIndex != -1) {
attributionNode.selectedIndex = engineIndex;
} else {
// Hide the attribution menuitem
let footer = attributionNode.parentNode;
footer.hidden = true;
// Make the 'Translation preferences' item the new footer.
footer = footer.previousSibling;
footer.setAttribute("class", "subviewbutton panel-subview-footer");
// And hide the menuseparator.
footer.previousSibling.hidden = true;
}
const kWelcomePref = "browser.translation.ui.welcomeMessageShown";
if (Services.prefs.prefHasUserValue(kWelcomePref) ||
this.translation.browser != gBrowser.selectedBrowser)
return;
this.addEventListener("transitionend", function() {
// These strings are hardcoded because they need to reach beta
// without riding the trains.
let localizedStrings = {
en: ["Hey look! It's something new!",
"Now the Web is even more accessible with our new in-page translation feature. Click the translate button to try it!",
"Learn more.",
"Thanks"],
"es-AR": ["\xA1Mir\xE1! \xA1Hay algo nuevo!",
"Ahora la web es a\xFAn m\xE1s accesible con nuestra nueva funcionalidad de traducci\xF3n integrada. \xA1Hac\xE9 clic en el bot\xF3n traducir para probarla!",
"Conoc\xE9 m\xE1s.",
"Gracias"],
"es-ES": ["\xA1Mira! \xA1Hay algo nuevo!",
"Con la nueva funcionalidad de traducci\xF3n integrada, ahora la Web es a\xFAn m\xE1s accesible. \xA1Pulsa el bot\xF3n Traducir y pru\xE9bala!",
"M\xE1s informaci\xF3n.",
"Gracias"],
pl: ["Sp\xF3jrz tutaj! To co\u015B nowego!",
"Sie\u0107 sta\u0142a si\u0119 w\u0142a\u015Bnie jeszcze bardziej dost\u0119pna dzi\u0119ki opcji bezpo\u015Bredniego t\u0142umaczenia stron. Kliknij przycisk t\u0142umaczenia, aby spr\xF3bowa\u0107!",
"Dowiedz si\u0119 wi\u0119cej",
"Dzi\u0119kuj\u0119"],
tr: ["Bak\u0131n, burada yeni bir \u015Fey var!",
"Yeni sayfa i\xE7i \xE7eviri \xF6zelli\u011Fimiz sayesinde Web art\u0131k \xE7ok daha anla\u015F\u0131l\u0131r olacak. Denemek i\xE7in \xC7evir d\xFC\u011Fmesine t\u0131klay\u0131n!",
"Daha fazla bilgi al\u0131n.",
"Te\u015Fekk\xFCrler"],
vi: ["Nh\xECn n\xE0y! \u0110\u1ED3 m\u1EDBi!",
"Gi\u1EDD \u0111\xE2y ch\xFAng ta c\xF3 th\u1EC3 ti\u1EBFp c\u1EADn web d\u1EC5 d\xE0ng h\u01A1n n\u1EEFa v\u1EDBi t\xEDnh n\u0103ng d\u1ECBch ngay trong trang. Hay nh\u1EA5n n\xFAt d\u1ECBch \u0111\u1EC3 th\u1EED!",
"T\xECm hi\u1EC3u th\xEAm.",
"C\u1EA3m \u01A1n"],
};
let locale = Services.locale.appLocaleAsLangTag;
if (!(locale in localizedStrings))
locale = "en";
let strings = localizedStrings[locale];
this._getAnonElt("welcomeHeadline").setAttribute("value", strings[0]);
this._getAnonElt("welcomeBody").textContent = strings[1];
this._getAnonElt("learnMore").setAttribute("value", strings[2]);
this._getAnonElt("thanksButton").setAttribute("label", strings[3]);
let panel = this._getAnonElt("welcomePanel");
panel.openPopup(this._getAnonElt("messageImage"),
"bottomcenter topleft");
Services.prefs.setBoolPref(kWelcomePref, true);
}, {once: true});
]]>
</body>
</method>
<method name="_getAnonElt">
<parameter name="aAnonId"/>
<body>
return document.getAnonymousElementByAttribute(this, "anonid", aAnonId);
</body>
</method>
<method name="translate">
<body>
<![CDATA[
if (this.state == Translation.STATE_OFFER) {
this._getAnonElt("fromLanguage").value =
this._getAnonElt("detectedLanguage").value;
this._getAnonElt("toLanguage").value =
Translation.defaultTargetLanguage;
}
this.translation.translate(this._getAnonElt("fromLanguage").value,
this._getAnonElt("toLanguage").value);
]]>
</body>
</method>
<!-- To be called when the infobar should be closed per user's wish (e.g.
by clicking the notification's close button -->
<method name="closeCommand">
<body>
<![CDATA[
this.close();
this.translation.infobarClosed();
]]>
</body>
</method>
<method name="_handleButtonHiding">
<body>
<![CDATA[
let originalShown = this.translation.originalShown;
this._getAnonElt("showOriginal").hidden = originalShown;
this._getAnonElt("showTranslation").hidden = !originalShown;
]]>
</body>
</method>
<method name="showOriginal">
<body>
<![CDATA[
this.translation.showOriginalContent();
this._handleButtonHiding();
]]>
</body>
</method>
<method name="showTranslation">
<body>
<![CDATA[
this.translation.showTranslatedContent();
this._handleButtonHiding();
]]>
</body>
</method>
<method name="optionsShowing">
<body>
<![CDATA[
// Get the source language name.
let lang;
if (this.state == Translation.STATE_OFFER)
lang = this._getAnonElt("detectedLanguage").value;
else {
lang = this._getAnonElt("fromLanguage").value;
// If we have never attempted to translate the page before the
// service became unavailable, "fromLanguage" isn't set.
if (!lang && this.state == Translation.STATE_UNAVAILABLE)
lang = this.translation.detectedLanguage;
}
let langName = Services.intl.getLanguageDisplayNames(undefined, [lang])[0];
// Set the label and accesskey on the menuitem.
let bundle =
Services.strings.createBundle("chrome://browser/locale/translation.properties");
let item = this._getAnonElt("neverForLanguage");
const kStrId = "translation.options.neverForLanguage";
item.setAttribute("label",
bundle.formatStringFromName(kStrId + ".label",
[langName], 1));
item.setAttribute("accesskey",
bundle.GetStringFromName(kStrId + ".accesskey"));
item.langCode = lang;
// We may need to disable the menuitems if they have already been used.
// Check if translation is already disabled for this language:
let neverForLangs =
Services.prefs.getCharPref("browser.translation.neverForLanguages");
item.disabled = neverForLangs.split(",").includes(lang);
// Check if translation is disabled for the domain:
let uri = this.translation.browser.currentURI;
let perms = Services.perms;
item = this._getAnonElt("neverForSite");
item.disabled =
perms.testExactPermission(uri, "translate") == perms.DENY_ACTION;
]]>
</body>
</method>
<method name="neverForLanguage">
<body>
<![CDATA[
const kPrefName = "browser.translation.neverForLanguages";
let val = Services.prefs.getCharPref(kPrefName);
if (val)
val += ",";
val += this._getAnonElt("neverForLanguage").langCode;
Services.prefs.setCharPref(kPrefName, val);
this.closeCommand();
]]>
</body>
</method>
<method name="neverForSite">
<body>
<![CDATA[
let uri = this.translation.browser.currentURI;
let perms = Services.perms;
perms.add(uri, "translate", perms.DENY_ACTION);
this.closeCommand();
]]>
</body>
</method>
<method name="openProviderAttribution">
<body>
<![CDATA[
Translation.openProviderAttribution();
]]>
</body>
</method>
</implementation>
</binding>
</bindings>

View File

@@ -279,9 +279,7 @@ add_task(async function test_other_ignored() {
Assert.ok(notification, "There should be a notification"); Assert.ok(notification, "There should be a notification");
// Dismiss notification, creating the .dmp.ignore file // Dismiss notification, creating the .dmp.ignore file
let closeButton = notification.querySelector(".messageCloseButton").click();
document.getAnonymousElementByAttribute(notification, "anonid", "close-button");
closeButton.click();
gNotificationBox.removeNotification(notification, true); gNotificationBox.removeNotification(notification, true);
await waitForIgnoredReports(toIgnore); await waitForIgnoredReports(toIgnore);
@@ -471,9 +469,7 @@ add_task(async function test_can_ignore() {
Assert.ok(notification, "There should be a notification"); Assert.ok(notification, "There should be a notification");
// Dismiss the notification by clicking on the "X" button. // Dismiss the notification by clicking on the "X" button.
let closeButton = notification.querySelector(".messageCloseButton").click();
document.getAnonymousElementByAttribute(notification, "anonid", "close-button");
closeButton.click();
// We'll not wait for the notification to finish its transition - // We'll not wait for the notification to finish its transition -
// we'll just remove it right away. // we'll just remove it right away.
gNotificationBox.removeNotification(notification, true); gNotificationBox.removeNotification(notification, true);
@@ -545,9 +541,7 @@ add_task(async function test_shutdown_while_not_showing() {
Assert.ok(notification, "There should be a notification"); Assert.ok(notification, "There should be a notification");
// Dismiss the notification by clicking on the "X" button. // Dismiss the notification by clicking on the "X" button.
let closeButton = notification.querySelector(".messageCloseButton").click();
document.getAnonymousElementByAttribute(notification, "anonid", "close-button");
closeButton.click();
// We'll not wait for the notification to finish its transition - // We'll not wait for the notification to finish its transition -
// we'll just remove it right away. // we'll just remove it right away.
gNotificationBox.removeNotification(notification, true); gNotificationBox.removeNotification(notification, true);

View File

@@ -4,25 +4,25 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
%endif %endif
notification[value="translation"] .messageImage { notification[value="translation"] .messageImage {
list-style-image: url(chrome://browser/skin/translation-16.png); list-style-image: url(chrome://browser/skin/translation-16.png) !important;
-moz-image-region: rect(0, 32px, 16px, 16px); -moz-image-region: rect(0, 32px, 16px, 16px);
} }
@media (min-resolution: 1.25dppx) { @media (min-resolution: 1.25dppx) {
notification[value="translation"] .messageImage { notification[value="translation"] .messageImage {
list-style-image: url(chrome://browser/skin/translation-16@2x.png); list-style-image: url(chrome://browser/skin/translation-16@2x.png) !important;
-moz-image-region: rect(0, 64px, 32px, 32px); -moz-image-region: rect(0, 64px, 32px, 32px);
} }
} }
notification[value="translation"][state="translating"] .messageImage { notification[value="translation"][state="translating"] .messageImage {
list-style-image: url(chrome://browser/skin/translating-16.png); list-style-image: url(chrome://browser/skin/translating-16.png) !important;
-moz-image-region: auto; -moz-image-region: auto;
} }
@media (min-resolution: 1.25dppx) { @media (min-resolution: 1.25dppx) {
notification[value="translation"][state="translating"] .messageImage { notification[value="translation"][state="translating"] .messageImage {
list-style-image: url(chrome://browser/skin/translating-16@2x.png); list-style-image: url(chrome://browser/skin/translating-16@2x.png) !important;
} }
} }

View File

@@ -82,8 +82,10 @@ function testOpenTestFile() {
is(nb.allNotifications.length, 1, "There is just one notification"); is(nb.allNotifications.length, 1, "There is just one notification");
const cn = nb.currentNotification; const cn = nb.currentNotification;
is(cn.priority, nb.PRIORITY_WARNING_HIGH, "notification priority is correct"); is(cn.priority, nb.PRIORITY_WARNING_HIGH, "notification priority is correct");
is(cn.value, "file-import-convert-failed", "notification value is corrent"); is(cn.getAttribute("value"), "file-import-convert-failed",
is(cn.type, "warning", "notification type is correct"); "notification value is corrent");
is(cn.getAttribute("type"), "warning",
"notification type is correct");
done(); done();
}); });
ok(true, "importFromFile does not cause exception"); ok(true, "importFromFile does not cause exception");

View File

@@ -152,7 +152,7 @@ function testOpenDeletedFile() {
"The missing file was successfully removed from the menu."); "The missing file was successfully removed from the menu.");
ok(gScratchpad.notificationBox.currentNotification, ok(gScratchpad.notificationBox.currentNotification,
"The notification was successfully displayed."); "The notification was successfully displayed.");
is(gScratchpad.notificationBox.currentNotification.label, is(gScratchpad.notificationBox.currentNotification.messageText.textContent,
gScratchpad.strings.GetStringFromName("fileNoLongerExists.notification"), gScratchpad.strings.GetStringFromName("fileNoLongerExists.notification"),
"The notification label is correct."); "The notification label is correct.");

View File

@@ -63,7 +63,7 @@ const CSSUsageFront = protocol.FrontClassWithSpec(cssUsageSpec, {
} }
} else { } else {
if (notification) { if (notification) {
notification.remove(); notification.close();
notification = undefined; notification = undefined;
} }

View File

@@ -146,8 +146,7 @@ class TestSafeBrowsingNotificationBar(PuppeteerMixin, MarionetteTestCase):
# TODO: update to use safe browsing notification bar class when bug 1139544 lands # TODO: update to use safe browsing notification bar class when bug 1139544 lands
button = (self.marionette.find_element(By.ID, 'tabbrowser-tabbox') button = (self.marionette.find_element(By.ID, 'tabbrowser-tabbox')
.find_element(By.CSS_SELECTOR, 'notification[value=blocked-badware-page]') .find_element(By.CSS_SELECTOR, 'notification[value=blocked-badware-page]')
.find_element('anon attribute', .find_element(By.CSS_SELECTOR, '.messageCloseButton'))
{'class': 'messageCloseButton close-icon tabbable'}))
button.click() button.click()
Wait(self.marionette, timeout=self.marionette.timeout.page_load).until( Wait(self.marionette, timeout=self.marionette.timeout.page_load).until(

View File

@@ -1591,7 +1591,7 @@ var BrowserTestUtils = {
waitForNotificationInNotificationBox(notificationBox, notificationValue) { waitForNotificationInNotificationBox(notificationBox, notificationValue) {
return new Promise((resolve) => { return new Promise((resolve) => {
let check = (event) => { let check = (event) => {
return event.target.value == notificationValue; return event.target.getAttribute("value") == notificationValue;
}; };
BrowserTestUtils.waitForEvent(notificationBox.stack, "AlertActive", BrowserTestUtils.waitForEvent(notificationBox.stack, "AlertActive",

View File

@@ -173,8 +173,8 @@ var Heartbeat = class {
// Build the heartbeat stars // Build the heartbeat stars
if (!this.options.engagementButtonLabel) { if (!this.options.engagementButtonLabel) {
const numStars = this.options.engagementButtonLabel ? 0 : 5; const numStars = this.options.engagementButtonLabel ? 0 : 5;
const ratingContainer = this.chromeWindow.document.createXULElement("hbox"); this.ratingContainer = this.chromeWindow.document.createXULElement("hbox");
ratingContainer.id = "star-rating-container"; this.ratingContainer.id = "star-rating-container";
for (let i = 0; i < numStars; i++) { for (let i = 0; i < numStars; i++) {
// create a star rating element // create a star rating element
@@ -193,43 +193,38 @@ var Heartbeat = class {
this.userEngaged({type: "stars", score: rating, flowId: this.options.flowId}); this.userEngaged({type: "stars", score: rating, flowId: this.options.flowId});
}); });
ratingContainer.appendChild(ratingElement); this.ratingContainer.appendChild(ratingElement);
} }
frag.appendChild(ratingContainer); frag.appendChild(this.ratingContainer);
} }
const details = this.chromeWindow.document.getAnonymousElementByAttribute(this.notice, "anonid", "details"); this.notice.messageDetails.style.overflow = "hidden";
details.style.overflow = "hidden"; this.notice.messageImage.classList.add("heartbeat", "pulse-onshow");
this.notice.messageText.classList.add("heartbeat");
this.messageImage = this.chromeWindow.document.getAnonymousElementByAttribute(this.notice, "anonid", "messageImage");
this.messageImage.classList.add("heartbeat", "pulse-onshow");
this.messageText = this.chromeWindow.document.getAnonymousElementByAttribute(this.notice, "anonid", "messageText");
this.messageText.classList.add("heartbeat");
// Make sure the stars are not pushed to the right by the spacer. // Make sure the stars are not pushed to the right by the spacer.
const rightSpacer = this.chromeWindow.document.createXULElement("spacer"); this.rightSpacer = this.chromeWindow.document.createXULElement("spacer");
rightSpacer.flex = 20; this.rightSpacer.flex = 20;
frag.appendChild(rightSpacer); frag.appendChild(this.rightSpacer);
// collapse the space before the stars // collapse the space before the stars
this.messageText.flex = 0; this.notice.messageText.flex = 0;
const leftSpacer = this.messageText.nextSibling; this.notice.spacer.flex = 0;
leftSpacer.flex = 0;
// Add Learn More Link // Add Learn More Link
if (this.options.learnMoreMessage && this.options.learnMoreUrl) { if (this.options.learnMoreMessage && this.options.learnMoreUrl) {
const learnMore = this.chromeWindow.document.createXULElement("label"); this.learnMore = this.chromeWindow.document.createXULElement("label");
learnMore.className = "text-link"; this.learnMore.className = "text-link";
learnMore.href = this.options.learnMoreUrl.toString(); this.learnMore.href = this.options.learnMoreUrl.toString();
learnMore.setAttribute("value", this.options.learnMoreMessage); this.learnMore.setAttribute("value", this.options.learnMoreMessage);
learnMore.addEventListener("click", () => this.maybeNotifyHeartbeat("LearnMore")); this.learnMore.addEventListener("click",
frag.appendChild(learnMore); () => this.maybeNotifyHeartbeat("LearnMore"));
frag.appendChild(this.learnMore);
} }
// Append the fragment and apply the styling // Append the fragment and apply the styling
this.notice.appendChild(frag); this.notice.messageDetails.appendChild(frag);
this.notice.classList.add("heartbeat"); this.notice.classList.add("heartbeat");
// Let the consumer know the notification was shown. // Let the consumer know the notification was shown.
@@ -330,12 +325,17 @@ var Heartbeat = class {
userEngaged(engagementParams) { userEngaged(engagementParams) {
// Make the heartbeat icon pulse twice // Make the heartbeat icon pulse twice
this.notice.label = this.options.thanksMessage; this.notice.label = this.options.thanksMessage;
this.messageImage.classList.remove("pulse-onshow"); this.notice.messageImage.classList.remove("pulse-onshow");
this.messageImage.classList.add("pulse-twice"); this.notice.messageImage.classList.add("pulse-twice");
// Remove all the children of the notice (rating container, and the flex) // Remove the custom contents of the notice and the buttons
while (this.notice.firstChild) { if (this.ratingContainer) {
this.notice.firstChild.remove(); this.ratingContainer.remove();
}
this.rightSpacer.remove();
this.learnMore.remove();
for (let button of this.notice.querySelectorAll("button")) {
button.remove();
} }
// Open the engagement tab if we have a valid engagement URL. // Open the engagement tab if we have a valid engagement URL.
@@ -381,8 +381,10 @@ var Heartbeat = class {
// remove references for garbage collection // remove references for garbage collection
this.chromeWindow = null; this.chromeWindow = null;
this.notificationBox = null; this.notificationBox = null;
this.notification = null;
this.notice = null; this.notice = null;
this.ratingContainer = null;
this.rightSpacer = null;
this.learnMore = null;
this.eventEmitter = null; this.eventEmitter = null;
this.sandboxManager = null; this.sandboxManager = null;
// Ensure we don't re-enter and release the CleanupManager's reference to us: // Ensure we don't re-enter and release the CleanupManager's reference to us:

View File

@@ -77,43 +77,43 @@ notification.heartbeat {
} }
/* Learn More link styles */ /* Learn More link styles */
.heartbeat > .text-link { .heartbeat > hbox > .text-link {
color: #0095DD !important; color: #0095DD !important;
margin-inline-start: 0 !important; margin-inline-start: 0 !important;
} }
.heartbeat > .text-link:hover { .heartbeat > hbox > .text-link:hover {
color: #008ACB !important; color: #008ACB !important;
text-decoration: none !important; text-decoration: none !important;
} }
.heartbeat > .text-link:hover:active { .heartbeat > hbox > .text-link:hover:active {
color: #006B9D !important; color: #006B9D !important;
} }
/* Heartbeat UI Rating Star Classes */ /* Heartbeat UI Rating Star Classes */
.heartbeat > #star-rating-container { #star-rating-container {
display: -moz-box; display: -moz-box;
margin-bottom: 4px; margin-bottom: 4px;
} }
.heartbeat > #star-rating-container > #star5 { #star-rating-container > #star5 {
-moz-box-ordinal-group: 5; -moz-box-ordinal-group: 5;
} }
.heartbeat > #star-rating-container > #star4 { #star-rating-container > #star4 {
-moz-box-ordinal-group: 4; -moz-box-ordinal-group: 4;
} }
.heartbeat > #star-rating-container > #star3 { #star-rating-container > #star3 {
-moz-box-ordinal-group: 3; -moz-box-ordinal-group: 3;
} }
.heartbeat > #star-rating-container > #star2 { #star-rating-container > #star2 {
-moz-box-ordinal-group: 2; -moz-box-ordinal-group: 2;
} }
.heartbeat > #star-rating-container > .star-x { #star-rating-container > .star-x {
background: url("resource://normandy/skin/shared/heartbeat-star-off.svg"); background: url("resource://normandy/skin/shared/heartbeat-star-off.svg");
cursor: pointer; cursor: pointer;
height: 16px; height: 16px;
@@ -121,7 +121,7 @@ notification.heartbeat {
width: 16px; width: 16px;
} }
.heartbeat > #star-rating-container > .star-x:hover, #star-rating-container > .star-x:hover,
.heartbeat > #star-rating-container > .star-x:hover ~ .star-x { #star-rating-container > .star-x:hover ~ .star-x {
background: url("resource://normandy/skin/shared/heartbeat-star-lit.svg"); background: url("resource://normandy/skin/shared/heartbeat-star-lit.svg");
} }

View File

@@ -93,13 +93,12 @@ add_task(async function() {
// Check UI // Check UI
const learnMoreEl = hb.notice.querySelector(".text-link"); const learnMoreEl = hb.notice.querySelector(".text-link");
const messageEl = targetWindow.document.getAnonymousElementByAttribute(hb.notice, "anonid", "messageText");
Assert.equal(notificationBox.allNotifications.length, preCount + 1, "Correct number of notifications open"); Assert.equal(notificationBox.allNotifications.length, preCount + 1, "Correct number of notifications open");
Assert.equal(hb.notice.querySelectorAll(".star-x").length, 5, "Correct number of stars"); Assert.equal(hb.notice.querySelectorAll(".star-x").length, 5, "Correct number of stars");
Assert.equal(hb.notice.querySelectorAll(".notification-button").length, 0, "Engagement button not shown"); Assert.equal(hb.notice.querySelectorAll(".notification-button").length, 0, "Engagement button not shown");
Assert.equal(learnMoreEl.href, "https://example.org/learnmore", "Learn more url correct"); Assert.equal(learnMoreEl.href, "https://example.org/learnmore", "Learn more url correct");
Assert.equal(learnMoreEl.value, "Learn More", "Learn more label correct"); Assert.equal(learnMoreEl.value, "Learn More", "Learn more label correct");
Assert.equal(messageEl.textContent, "test", "Message is correct"); Assert.equal(hb.notice.messageText.textContent, "test", "Message is correct");
// Check that when clicking the learn more link, a tab opens with the right URL // Check that when clicking the learn more link, a tab opens with the right URL
let loadedPromise; let loadedPromise;

View File

@@ -301,8 +301,8 @@ var tests =
return ntf; return ntf;
}, },
result: function(nb, ntf) { result: function(nb, ntf) {
SimpleTest.is(nb.currentNotification == ntf ? is(nb.currentNotification, ntf, "appendNotification last notification");
nb.currentNotification.value : null, "10", "appendNotification order"); is(nb.currentNotification.getAttribute("value"), "10", "appendNotification order");
return 1; return 1;
} }
}, },
@@ -333,8 +333,8 @@ var tests =
}, },
result: function(nb, arr) { result: function(nb, arr) {
// arr is [testindex, expectedvalue] // arr is [testindex, expectedvalue]
SimpleTest.is(nb.currentNotification.value, "" + arr[1], "close order " + arr[0]); is(nb.currentNotification.getAttribute("value"), "" + arr[1], "close order " + arr[0]);
SimpleTest.is(nb.allNotifications.length, 10 - arr[0], "close order " + arr[0] + " count"); is(nb.allNotifications.length, 10 - arr[0], "close order " + arr[0] + " count");
if (arr[0] == 6) if (arr[0] == 6)
this.repeat = false; this.repeat = false;
return ++arr[0]; return ++arr[0];
@@ -400,10 +400,10 @@ function testtag_notificationbox_State(nb, testid, expecteditem, expectedcount)
function testtag_notification_State(nb, ntf, testid, label, value, image, priority) function testtag_notification_State(nb, ntf, testid, label, value, image, priority)
{ {
SimpleTest.is(ntf.label, label, testid + " notification.label"); is(ntf.messageText.textContent, label, testid + " notification label");
SimpleTest.is(ntf.value, value, testid + " notification.value"); is(ntf.getAttribute("value"), value, testid + " notification value");
SimpleTest.is(ntf.image, image, testid + " notification.image"); is(ntf.messageImage.getAttribute("src"), image, testid + " notification image");
SimpleTest.is(ntf.priority, priority, testid + " notification.priority"); is(ntf.priority, priority, testid + " notification priority");
var type; var type;
switch (priority) { switch (priority) {
@@ -424,7 +424,7 @@ function testtag_notification_State(nb, ntf, testid, label, value, image, priori
break; break;
} }
SimpleTest.is(ntf.type, type, testid + " notification.type"); is(ntf.getAttribute("type"), type, testid + " notification type");
} }
function checkPopupTest(nb, ntf) function checkPopupTest(nb, ntf)

View File

@@ -15,99 +15,6 @@
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html = "http://www.w3.org/1999/xhtml"> xmlns:html = "http://www.w3.org/1999/xhtml">
<binding id="notification">
<content>
<xul:hbox anonid="details" align="center" flex="1"
oncommand="this.parentNode._doButtonCommand(event);">
<xul:image anonid="messageImage" class="messageImage" xbl:inherits="src=image,type,value"/>
<xul:description anonid="messageText" class="messageText" flex="1" xbl:inherits="xbl:text=label"/>
<xul:spacer flex="1"/>
<children/>
</xul:hbox>
<xul:toolbarbutton ondblclick="event.stopPropagation();"
class="messageCloseButton close-icon tabbable"
anonid="close-button"
xbl:inherits="hidden=hideclose"
tooltiptext="&closeNotification.tooltip;"
oncommand="document.getBindingParent(this).dismiss();"/>
</content>
<implementation>
<property name="label" onset="this.setAttribute('label', val); return val;"
onget="return this.getAttribute('label');"/>
<property name="value" onset="this.setAttribute('value', val); return val;"
onget="return this.getAttribute('value');"/>
<property name="image" onset="this.setAttribute('image', val); return val;"
onget="return this.getAttribute('image');"/>
<property name="type" onset="this.setAttribute('type', val); return val;"
onget="return this.getAttribute('type');"/>
<property name="priority" onget="return parseInt(this.getAttribute('priority')) || 0;"
onset="this.setAttribute('priority', val); return val;"/>
<property name="persistence" onget="return parseInt(this.getAttribute('persistence')) || 0;"
onset="this.setAttribute('persistence', val); return val;"/>
<field name="timeout">0</field>
<property name="control" readonly="true">
<getter>
<![CDATA[
return this.closest(".notificationbox-stack")._notificationBox;
]]>
</getter>
</property>
<!-- This method should only be called when the user has
manually closed the notification. If you want to
programmatically close the notification, you should
call close() instead. -->
<method name="dismiss">
<body>
<![CDATA[
if (this.eventCallback) {
this.eventCallback("dismissed");
}
this.close();
]]>
</body>
</method>
<method name="close">
<body>
<![CDATA[
var control = this.control;
if (control)
control.removeNotification(this);
else
this.hidden = true;
]]>
</body>
</method>
<method name="_doButtonCommand">
<parameter name="aEvent"/>
<body>
<![CDATA[
if (!("buttonInfo" in aEvent.target))
return;
var button = aEvent.target.buttonInfo;
if (button.popup) {
document.getElementById(button.popup).
openPopup(aEvent.originalTarget, "after_start", 0, 0, false, false, aEvent);
aEvent.stopPropagation();
} else {
var callback = button.callback;
if (callback) {
var result = callback(this, button, aEvent.target, aEvent);
if (!result)
this.close();
aEvent.stopPropagation();
}
}
]]>
</body>
</method>
</implementation>
</binding>
<binding id="popup-notification"> <binding id="popup-notification">
<content orient="vertical"> <content orient="vertical">
<xul:hbox class="popup-notification-header-container"> <xul:hbox class="popup-notification-header-container">

View File

@@ -64,7 +64,49 @@ MozElements.NotificationBox = class NotificationBox {
return null; return null;
} }
appendNotification(aLabel, aValue, aImage, aPriority, aButtons, aEventCallback) { /**
* Creates a <notification> element and shows it. The calling code can modify
* the element synchronously to add features to the notification.
*
* @param aLabel
* The main message text, or a DocumentFragment containing elements to
* add as children of the notification's main <description> element.
* @param aValue
* String identifier of the notification.
* @param aImage
* URL of the icon image to display. If not specified, a default icon
* based on the priority will be shown.
* @param aPriority
* One of the PRIORITY_ constants. These determine the appearance of
* the notification based on severity (using the "type" attribute), and
* only the notification with the highest priority is displayed.
* @param aButtons
* Array of objects defining action buttons:
* {
* label:
* Label of the <button> element.
* accessKey:
* Access key character for the <button> element.
* callback:
* When the button is used, this is called with the arguments:
* 1. The <notification> element.
* 2. This button object definition.
* 3. The <button> element.
* 4. The "command" event.
* popup:
* If specified, the button will open the popup element with this
* ID, anchored to the button. This is alternative to "callback".
* }
* @param aEventCallback
* This may be called with the "removed" or "dismissed" parameter.
* @param aNotificationIs
* Defines a Custom Element name to use as the "is" value on creation.
* This allows subclassing the created element.
*
* @return The <notification> element that is shown.
*/
appendNotification(aLabel, aValue, aImage, aPriority, aButtons,
aEventCallback, aNotificationIs) {
if (aPriority < this.PRIORITY_INFO_LOW || if (aPriority < this.PRIORITY_INFO_LOW ||
aPriority > this.PRIORITY_CRITICAL_HIGH) aPriority > this.PRIORITY_CRITICAL_HIGH)
throw "Invalid notification priority " + aPriority; throw "Invalid notification priority " + aPriority;
@@ -79,24 +121,28 @@ MozElements.NotificationBox = class NotificationBox {
insertPos = notifications[n]; insertPos = notifications[n];
} }
var newitem = document.createXULElement("notification"); // Create the Custom Element and connect it to the document immediately.
// Can't use instanceof in case this was created from a different document: var newitem = document.createXULElement("notification",
let labelIsDocFragment = aLabel && typeof aLabel == "object" && aLabel.nodeType && aNotificationIs ? { is: aNotificationIs } : {});
aLabel.nodeType == aLabel.DOCUMENT_FRAGMENT_NODE; this.stack.insertBefore(newitem, insertPos);
if (!labelIsDocFragment)
newitem.setAttribute("label", aLabel); // Custom notification classes may not have the messageText property.
if (newitem.messageText) {
// Can't use instanceof in case this was created from a different document:
if (aLabel && typeof aLabel == "object" &&
aLabel.nodeType && aLabel.nodeType == aLabel.DOCUMENT_FRAGMENT_NODE) {
newitem.messageText.appendChild(aLabel);
} else {
newitem.messageText.textContent = aLabel;
}
}
newitem.setAttribute("value", aValue); newitem.setAttribute("value", aValue);
if (aImage) if (aImage) {
newitem.setAttribute("image", aImage); newitem.messageImage.setAttribute("src", aImage);
}
newitem.eventCallback = aEventCallback; newitem.eventCallback = aEventCallback;
if (aButtons) { if (aButtons) {
// The notification-button-default class is added to the button
// with isDefault set to true. If there is no such button, it is
// added to the first button (unless that button has isDefault
// set to false). There cannot be multiple default buttons.
var defaultElem;
for (var b = 0; b < aButtons.length; b++) { for (var b = 0; b < aButtons.length; b++) {
var button = aButtons[b]; var button = aButtons[b];
var buttonElem = document.createXULElement("button"); var buttonElem = document.createXULElement("button");
@@ -104,20 +150,12 @@ MozElements.NotificationBox = class NotificationBox {
if (typeof button.accessKey == "string") if (typeof button.accessKey == "string")
buttonElem.setAttribute("accesskey", button.accessKey); buttonElem.setAttribute("accesskey", button.accessKey);
buttonElem.classList.add("notification-button"); buttonElem.classList.add("notification-button");
newitem.messageDetails.appendChild(buttonElem);
if (button.isDefault ||
b == 0 && !("isDefault" in button))
defaultElem = buttonElem;
newitem.appendChild(buttonElem);
buttonElem.buttonInfo = button; buttonElem.buttonInfo = button;
} }
if (defaultElem)
defaultElem.classList.add("notification-button-default");
} }
newitem.setAttribute("priority", aPriority); newitem.priority = aPriority;
if (aPriority >= this.PRIORITY_CRITICAL_LOW) if (aPriority >= this.PRIORITY_CRITICAL_LOW)
newitem.setAttribute("type", "critical"); newitem.setAttribute("type", "critical");
else if (aPriority <= this.PRIORITY_INFO_HIGH) else if (aPriority <= this.PRIORITY_INFO_HIGH)
@@ -130,17 +168,8 @@ MozElements.NotificationBox = class NotificationBox {
newitem.style.top = "100%"; newitem.style.top = "100%";
newitem.style.marginTop = "-15px"; newitem.style.marginTop = "-15px";
newitem.style.opacity = "0"; newitem.style.opacity = "0";
}
this.stack.insertBefore(newitem, insertPos);
// Can only insert the document fragment after the item has been created because
// otherwise the XBL structure isn't there yet:
if (labelIsDocFragment) {
document.getAnonymousElementByAttribute(newitem, "anonid", "messageText")
.appendChild(aLabel);
}
if (!insertPos)
this._showNotification(newitem, true); this._showNotification(newitem, true);
}
// Fire event for accessibility APIs // Fire event for accessibility APIs
var event = document.createEvent("Events"); var event = document.createEvent("Events");
@@ -265,3 +294,80 @@ Object.assign(MozElements.NotificationBox.prototype, {
PRIORITY_CRITICAL_MEDIUM: 8, PRIORITY_CRITICAL_MEDIUM: 8,
PRIORITY_CRITICAL_HIGH: 9, PRIORITY_CRITICAL_HIGH: 9,
}); });
MozElements.Notification = class Notification extends MozXULElement {
constructor() {
super();
this.persistence = 0;
this.priority = 0;
this.timeout = 0;
}
connectedCallback() {
this.appendChild(MozXULElement.parseXULToFragment(`
<hbox class="messageDetails" align="center" flex="1"
oncommand="this.parentNode._doButtonCommand(event);">
<image class="messageImage"/>
<description class="messageText" flex="1"/>
<spacer flex="1"/>
</hbox>
<toolbarbutton ondblclick="event.stopPropagation();"
class="messageCloseButton close-icon tabbable"
tooltiptext="&closeNotification.tooltip;"
oncommand="this.parentNode.dismiss();"/>
`, ["chrome://global/locale/notification.dtd"]));
for (let [propertyName, selector] of [
["messageDetails", ".messageDetails"],
["messageImage", ".messageImage"],
["messageText", ".messageText"],
["spacer", "spacer"],
]) {
this[propertyName] = this.querySelector(selector);
}
}
get control() {
return this.closest(".notificationbox-stack")._notificationBox;
}
/**
* This method should only be called when the user has manually closed the
* notification. If you want to programmatically close the notification, you
* should call close() instead.
*/
dismiss() {
if (this.eventCallback) {
this.eventCallback("dismissed");
}
this.close();
}
close() {
this.control.removeNotification(this);
}
_doButtonCommand(event) {
if (!("buttonInfo" in event.target)) {
return;
}
var button = event.target.buttonInfo;
if (button.popup) {
document.getElementById(button.popup).
openPopup(event.originalTarget, "after_start", 0, 0, false, false, event);
event.stopPropagation();
} else {
var callback = button.callback;
if (callback) {
var result = callback(this, button, event.target, event);
if (!result) {
this.close();
}
event.stopPropagation();
}
}
}
};
customElements.define("notification", MozElements.Notification);

View File

@@ -176,12 +176,6 @@ browser {
-moz-binding: url("chrome://global/content/bindings/browser.xml#browser"); -moz-binding: url("chrome://global/content/bindings/browser.xml#browser");
} }
/********** notifications **********/
notification {
-moz-binding: url("chrome://global/content/bindings/notification.xml#notification");
}
/*********** popup notification ************/ /*********** popup notification ************/
popupnotification { popupnotification {
-moz-binding: url("chrome://global/content/bindings/notification.xml#popup-notification"); -moz-binding: url("chrome://global/content/bindings/notification.xml#popup-notification");

View File

@@ -257,7 +257,7 @@ label[disabled="true"] {
outline: 1px dotted; outline: 1px dotted;
} }
notification > button { notification > hbox > button {
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
} }

View File

@@ -218,7 +218,7 @@ label[disabled="true"] {
box-shadow: var(--focus-ring-box-shadow); box-shadow: var(--focus-ring-box-shadow);
} }
notification > button { notification > hbox > button {
margin: 0 3px; margin: 0 3px;
padding: 1px 10px; padding: 1px 10px;
min-width: 60px; min-width: 60px;
@@ -232,21 +232,21 @@ notification > button {
box-shadow: @roundButtonShadow@; box-shadow: @roundButtonShadow@;
} }
notification > button:active:hover { notification > hbox > button:active:hover {
color: @roundButtonColor@; color: @roundButtonColor@;
background: @roundButtonPressedBackground@; background: @roundButtonPressedBackground@;
box-shadow: @roundButtonPressedShadow@; box-shadow: @roundButtonPressedShadow@;
} }
notification > button:-moz-focusring { notification > hbox > button:-moz-focusring {
box-shadow: var(--focus-ring-box-shadow), @roundButtonShadow@; box-shadow: var(--focus-ring-box-shadow), @roundButtonShadow@;
} }
notification > button:active:hover:-moz-focusring { notification > hbox > button:active:hover:-moz-focusring {
box-shadow: var(--focus-ring-box-shadow), @roundButtonPressedShadow@; box-shadow: var(--focus-ring-box-shadow), @roundButtonPressedShadow@;
} }
notification > button > .button-box > .button-text { notification > hbox > button > .button-box > .button-text {
margin: 0 !important; margin: 0 !important;
} }

View File

@@ -63,15 +63,15 @@ notification[type="critical"] {
fill: currentColor; fill: currentColor;
} }
.messageImage[type="info"] { notification[type="info"] > hbox > .messageImage {
list-style-image: url("chrome://global/skin/icons/help.svg"); list-style-image: url("chrome://global/skin/icons/help.svg");
} }
.messageImage[type="warning"] { notification[type="warning"] > hbox > .messageImage {
list-style-image: url("chrome://global/skin/icons/warning.svg"); list-style-image: url("chrome://global/skin/icons/warning.svg");
} }
.messageImage[type="critical"] { notification[type="critical"] > hbox > .messageImage {
list-style-image: url("chrome://global/skin/icons/error.svg"); list-style-image: url("chrome://global/skin/icons/error.svg");
} }

View File

@@ -13,8 +13,7 @@ add_task(async function() {
gBrowser.getNotificationBox().getNotificationWithValue(notificationValue); gBrowser.getNotificationBox().getNotificationWithValue(notificationValue);
await BrowserTestUtils.waitForCondition(getNotification); await BrowserTestUtils.waitForCondition(getNotification);
let notification = getNotification(); let notification = getNotification();
let button = let button = notification.querySelector("button");
notification.getElementsByClassName("notification-button-default")[0];
ok(button, "got registration button"); ok(button, "got registration button");
button.click(); button.click();