Bug 1943650 - Command-line --help output misformatted after --dbus-service. r=emilio
[gecko.git] / toolkit / components / alerts / alert.js
blob8a097f967babd921ff08f65d8296a7419b023302
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 const { AppConstants } = ChromeUtils.importESModule(
6 "resource://gre/modules/AppConstants.sys.mjs"
7 );
9 // Copied from nsILookAndFeel.h, see comments on eMetric_AlertNotificationOrigin
10 const NS_ALERT_HORIZONTAL = 1;
11 const NS_ALERT_LEFT = 2;
12 const NS_ALERT_TOP = 4;
14 const WINDOW_MARGIN = AppConstants.platform == "win" ? 0 : 10;
15 const BODY_TEXT_LIMIT = 200;
16 const WINDOW_SHADOW_SPREAD = AppConstants.platform == "win" ? 10 : 0;
18 var gOrigin = 0; // Default value: alert from bottom right.
19 var gReplacedWindow = null;
20 var gAlertListener = null;
21 var gAlertTextClickable = false;
22 var gAlertCookie = "";
23 var gIsActive = false;
24 var gIsReplaced = false;
25 var gRequireInteraction = false;
27 function prefillAlertInfo() {
28 // unwrap all the args....
29 // arguments[0] --> the image src url
30 // arguments[1] --> the alert title
31 // arguments[2] --> the alert text
32 // arguments[3] --> is the text clickable?
33 // arguments[4] --> the alert cookie to be passed back to the listener
34 // arguments[5] --> the alert origin reported by the look and feel
35 // arguments[6] --> bidi
36 // arguments[7] --> lang
37 // arguments[8] --> requires interaction
38 // arguments[9] --> replaced alert window (nsIDOMWindow)
39 // arguments[10] --> an optional callback listener (nsIObserver)
40 // arguments[11] -> the nsIURI.hostPort of the origin, optional
41 // arguments[12] -> the alert icon URL, optional
43 switch (window.arguments.length) {
44 default:
45 case 13: {
46 if (window.arguments[12]) {
47 let alertBox = document.getElementById("alertBox");
48 alertBox.setAttribute("hasIcon", true);
50 let icon = document.getElementById("alertIcon");
51 icon.src = window.arguments[12];
54 // fall through
55 case 12: {
56 if (window.arguments[11]) {
57 let alertBox = document.getElementById("alertBox");
58 alertBox.setAttribute("hasOrigin", true);
60 let hostPort = window.arguments[11];
61 const ALERT_BUNDLE = Services.strings.createBundle(
62 "chrome://alerts/locale/alert.properties"
64 const BRAND_BUNDLE = Services.strings.createBundle(
65 "chrome://branding/locale/brand.properties"
67 const BRAND_NAME = BRAND_BUNDLE.GetStringFromName("brandShortName");
68 let label = document.getElementById("alertSourceLabel");
69 label.setAttribute(
70 "value",
71 ALERT_BUNDLE.formatStringFromName("source.label", [hostPort])
73 let doNotDisturbMenuItem = document.getElementById(
74 "doNotDisturbMenuItem"
76 doNotDisturbMenuItem.setAttribute(
77 "label",
78 ALERT_BUNDLE.formatStringFromName("pauseNotifications.label", [
79 BRAND_NAME,
82 let disableForOrigin = document.getElementById(
83 "disableForOriginMenuItem"
85 disableForOrigin.setAttribute(
86 "label",
87 ALERT_BUNDLE.formatStringFromName(
88 "webActions.disableForOrigin.label",
89 [hostPort]
92 let openSettings = document.getElementById("openSettingsMenuItem");
93 openSettings.setAttribute(
94 "label",
95 ALERT_BUNDLE.GetStringFromName("webActions.settings.label")
99 // fall through
100 case 11:
101 gAlertListener = window.arguments[10];
102 // fall through
103 case 10:
104 gReplacedWindow = window.arguments[9];
105 // fall through
106 case 9:
107 gRequireInteraction = window.arguments[8];
108 // fall through
109 case 8:
110 if (window.arguments[7]) {
111 document
112 .getElementById("alertTitleLabel")
113 .setAttribute("lang", window.arguments[7]);
114 document
115 .getElementById("alertTextLabel")
116 .setAttribute("lang", window.arguments[7]);
118 // fall through
119 case 7:
120 if (window.arguments[6]) {
121 document.getElementById("alertNotification").style.direction =
122 window.arguments[6];
124 // fall through
125 case 6:
126 gOrigin = window.arguments[5];
127 // fall through
128 case 5:
129 gAlertCookie = window.arguments[4];
130 // fall through
131 case 4:
132 gAlertTextClickable = window.arguments[3];
133 if (gAlertTextClickable) {
134 document
135 .getElementById("alertNotification")
136 .setAttribute("clickable", true);
137 document
138 .getElementById("alertTextLabel")
139 .setAttribute("clickable", true);
141 // fall through
142 case 3:
143 if (window.arguments[2]) {
144 document.getElementById("alertBox").setAttribute("hasBodyText", true);
145 let bodyText = window.arguments[2];
146 let bodyTextLabel = document.getElementById("alertTextLabel");
148 if (bodyText.length > BODY_TEXT_LIMIT) {
149 bodyTextLabel.setAttribute("tooltiptext", bodyText);
151 let ellipsis = "\u2026";
152 try {
153 ellipsis = Services.prefs.getComplexValue(
154 "intl.ellipsis",
155 Ci.nsIPrefLocalizedString
156 ).data;
157 } catch (e) {}
159 // Copied from nsContextMenu.js' formatSearchContextItem().
160 // If the JS character after our truncation point is a trail surrogate,
161 // include it in the truncated string to avoid splitting a surrogate pair.
162 let truncLength = BODY_TEXT_LIMIT;
163 let truncChar = bodyText[BODY_TEXT_LIMIT].charCodeAt(0);
164 if (truncChar >= 0xdc00 && truncChar <= 0xdfff) {
165 truncLength++;
168 bodyText = bodyText.substring(0, truncLength) + ellipsis;
170 bodyTextLabel.textContent = bodyText;
172 // fall through
173 case 2:
174 document
175 .getElementById("alertTitleLabel")
176 .setAttribute("value", window.arguments[1]);
177 // fall through
178 case 1:
179 if (window.arguments[0]) {
180 document.getElementById("alertBox").setAttribute("hasImage", true);
181 document
182 .getElementById("alertImage")
183 .setAttribute("src", window.arguments[0]);
185 // fall through
186 case 0:
187 break;
191 function onAlertLoad() {
192 const ALERT_DURATION_IMMEDIATE = 20000;
193 let alertTextBox = document.getElementById("alertTextBox");
194 let alertImageBox = document.getElementById("alertImageBox");
195 alertImageBox.style.minHeight = alertTextBox.scrollHeight + "px";
197 window.sizeToContent();
199 if (gReplacedWindow && !gReplacedWindow.closed) {
200 moveWindowToReplace(gReplacedWindow);
201 gReplacedWindow.gIsReplaced = true;
202 gReplacedWindow.close();
203 } else {
204 moveWindowToEnd();
207 window.addEventListener("XULAlertClose", function () {
208 window.close();
211 // If the require interaction flag is set, prevent auto-closing the notification.
212 if (!gRequireInteraction) {
213 if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
214 setTimeout(function () {
215 window.close();
216 }, ALERT_DURATION_IMMEDIATE);
217 } else {
218 let alertBox = document.getElementById("alertBox");
219 alertBox.addEventListener("animationend", function hideAlert(event) {
220 if (
221 event.animationName == "alert-animation" ||
222 event.animationName == "alert-clicked-animation" ||
223 event.animationName == "alert-closing-animation"
225 alertBox.removeEventListener("animationend", hideAlert);
226 window.close();
229 alertBox.setAttribute("animate", true);
233 let alertSettings = document.getElementById("alertSettings");
234 alertSettings.addEventListener("focus", onAlertSettingsFocus);
235 alertSettings.addEventListener("click", onAlertSettingsClick);
237 gIsActive = true;
239 let ev = new CustomEvent("AlertActive", { bubbles: true, cancelable: true });
240 document.documentElement.dispatchEvent(ev);
242 if (gAlertListener) {
243 gAlertListener.observe(null, "alertshow", gAlertCookie);
247 function moveWindowToReplace(aReplacedAlert) {
248 let heightDelta = window.outerHeight - aReplacedAlert.outerHeight;
250 // Move windows that come after the replaced alert if the height is different.
251 if (heightDelta != 0) {
252 for (let alertWindow of Services.wm.getEnumerator("alert:alert")) {
253 if (!alertWindow.gIsActive) {
254 continue;
256 // boolean to determine if the alert window is after the replaced alert.
257 let alertIsAfter =
258 gOrigin & NS_ALERT_TOP
259 ? alertWindow.screenY > aReplacedAlert.screenY
260 : aReplacedAlert.screenY > alertWindow.screenY;
261 if (alertIsAfter) {
262 // The new Y position of the window.
263 let adjustedY =
264 gOrigin & NS_ALERT_TOP
265 ? alertWindow.screenY + heightDelta
266 : alertWindow.screenY - heightDelta;
267 alertWindow.moveTo(alertWindow.screenX, adjustedY);
272 let adjustedY =
273 gOrigin & NS_ALERT_TOP
274 ? aReplacedAlert.screenY
275 : aReplacedAlert.screenY - heightDelta;
276 window.moveTo(aReplacedAlert.screenX, adjustedY);
279 function moveWindowToEnd() {
280 // Determine position
281 let x =
282 gOrigin & NS_ALERT_LEFT
283 ? screen.availLeft
284 : screen.availLeft + screen.availWidth - window.outerWidth;
285 let y =
286 gOrigin & NS_ALERT_TOP
287 ? screen.availTop
288 : screen.availTop + screen.availHeight - window.outerHeight;
290 // Position the window at the end of all alerts.
291 for (let alertWindow of Services.wm.getEnumerator("alert:alert")) {
292 if (alertWindow != window && alertWindow.gIsActive) {
293 if (gOrigin & NS_ALERT_TOP) {
294 y = Math.max(
296 alertWindow.screenY + alertWindow.outerHeight - WINDOW_SHADOW_SPREAD
298 } else {
299 y = Math.min(
301 alertWindow.screenY - window.outerHeight + WINDOW_SHADOW_SPREAD
307 // Offset the alert by WINDOW_MARGIN pixels from the edge of the screen
308 y += gOrigin & NS_ALERT_TOP ? WINDOW_MARGIN : -WINDOW_MARGIN;
309 x += gOrigin & NS_ALERT_LEFT ? WINDOW_MARGIN : -WINDOW_MARGIN;
311 window.moveTo(x, y);
314 function onAlertBeforeUnload() {
315 if (!gIsReplaced) {
316 // Move other alert windows to fill the gap left by closing alert.
317 let heightDelta = window.outerHeight + WINDOW_MARGIN - WINDOW_SHADOW_SPREAD;
318 for (let alertWindow of Services.wm.getEnumerator("alert:alert")) {
319 if (alertWindow != window && alertWindow.gIsActive) {
320 if (gOrigin & NS_ALERT_TOP) {
321 if (alertWindow.screenY > window.screenY) {
322 alertWindow.moveTo(
323 alertWindow.screenX,
324 alertWindow.screenY - heightDelta
327 } else if (window.screenY > alertWindow.screenY) {
328 alertWindow.moveTo(
329 alertWindow.screenX,
330 alertWindow.screenY + heightDelta
337 if (gAlertListener) {
338 gAlertListener.observe(null, "alertfinished", gAlertCookie);
342 function onAlertClick() {
343 if (gAlertListener && gAlertTextClickable) {
344 gAlertListener.observe(null, "alertclickcallback", gAlertCookie);
347 let alertBox = document.getElementById("alertBox");
348 if (alertBox.getAttribute("animate") == "true") {
349 // Closed when the animation ends.
350 alertBox.setAttribute("clicked", "true");
351 } else {
352 window.close();
356 function doNotDisturb() {
357 const alertService = Cc["@mozilla.org/alerts-service;1"]
358 .getService(Ci.nsIAlertsService)
359 .QueryInterface(Ci.nsIAlertsDoNotDisturb);
360 alertService.manualDoNotDisturb = true;
361 onAlertClose();
364 function disableForOrigin() {
365 gAlertListener.observe(null, "alertdisablecallback", gAlertCookie);
366 onAlertClose();
369 function onAlertSettingsFocus(event) {
370 event.target.removeAttribute("focusedViaMouse");
373 function onAlertSettingsClick(event) {
374 // XXXjaws Hack used to remove the focus-ring only
375 // from mouse interaction, but focus-ring drawing
376 // should only be enabled when interacting via keyboard.
377 event.target.setAttribute("focusedViaMouse", true);
378 event.stopPropagation();
381 function openSettings() {
382 gAlertListener.observe(null, "alertsettingscallback", gAlertCookie);
383 onAlertClose();
386 function onAlertClose() {
387 let alertBox = document.getElementById("alertBox");
388 if (alertBox.getAttribute("animate") == "true") {
389 // Closed when the animation ends.
390 alertBox.setAttribute("closing", "true");
391 } else {
392 window.close();