Backed out 12 changesets (bug 1843308, bug 1848406, bug 1888500, bug 1888504, bug...
[gecko.git] / toolkit / components / tooltiptext / TooltipTextProvider.sys.mjs
bloba231f5b4cc1f189e68cf79a92a60496033269fbe
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 export function TooltipTextProvider() {}
7 function getFileInputTitleText(tipElement) {
8   let files = tipElement.files;
9   let bundle = Services.strings.createBundle(
10     "chrome://global/locale/layout/HtmlForm.properties"
11   );
12   if (!files.length) {
13     return bundle.GetStringFromName(
14       tipElement.multiple ? "NoFilesSelected" : "NoFileSelected"
15     );
16   }
17   let titleText = files[0].name;
18   // For UX and performance (jank) reasons we cap the number of
19   // files that we list in the tooltip to 20 plus a "and xxx more"
20   // line, or to 21 if exactly 21 files were picked.
21   const TRUNCATED_FILE_COUNT = 20;
22   let count = Math.min(files.length, TRUNCATED_FILE_COUNT);
23   for (let i = 1; i < count; ++i) {
24     titleText += "\n" + files[i].name;
25   }
26   if (files.length == TRUNCATED_FILE_COUNT + 1) {
27     titleText += "\n" + files[TRUNCATED_FILE_COUNT].name;
28   } else if (files.length > TRUNCATED_FILE_COUNT + 1) {
29     const l10n = new Localization(["toolkit/global/htmlForm.ftl"], true);
30     const andXMoreStr = l10n.formatValueSync("input-file-and-more-files", {
31       fileCount: files.length - TRUNCATED_FILE_COUNT,
32     });
33     titleText += "\n" + andXMoreStr;
34   }
35   return titleText;
38 TooltipTextProvider.prototype = {
39   getNodeText(tipElement, textOut, directionOut) {
40     // Don't show the tooltip if the tooltip node is a document or browser.
41     // Caller should ensure the node is in (composed) document.
42     if (
43       !tipElement ||
44       !tipElement.ownerDocument ||
45       tipElement.localName == "browser"
46     ) {
47       return false;
48     }
50     var defView = tipElement.ownerGlobal;
51     // XXX Work around bug 350679:
52     // "Tooltips can be fired in documents with no view".
53     if (!defView) {
54       return false;
55     }
57     const XLinkNS = "http://www.w3.org/1999/xlink";
58     const XUL_NS =
59       "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
61     var titleText = null;
62     var XLinkTitleText = null;
63     var lookingForSVGTitle = true;
64     var direction = tipElement.ownerDocument.dir;
66     for (; tipElement; tipElement = tipElement.flattenedTreeParentNode) {
67       if (tipElement.nodeType != defView.Node.ELEMENT_NODE) {
68         continue;
69       }
70       if (tipElement.namespaceURI == XUL_NS) {
71         lookingForSVGTitle = false;
72         titleText = tipElement.getAttribute("tooltiptext");
73       } else if (!defView.SVGElement.isInstance(tipElement)) {
74         lookingForSVGTitle = false;
75         titleText = tipElement.getAttribute("title");
76       }
78       if (
79         (defView.HTMLAnchorElement.isInstance(tipElement) ||
80           defView.HTMLAreaElement.isInstance(tipElement) ||
81           defView.HTMLLinkElement.isInstance(tipElement) ||
82           defView.SVGAElement.isInstance(tipElement)) &&
83         tipElement.href
84       ) {
85         XLinkTitleText = tipElement.getAttributeNS(XLinkNS, "title");
86       }
88       // If the element is invalid per HTML5 Forms specifications and has no title,
89       // show the constraint validation error message.
90       if (
91         titleText == null &&
92         (defView.HTMLInputElement.isInstance(tipElement) ||
93           defView.HTMLTextAreaElement.isInstance(tipElement) ||
94           defView.HTMLSelectElement.isInstance(tipElement) ||
95           defView.HTMLButtonElement.isInstance(tipElement)) &&
96         !tipElement.form?.noValidate
97       ) {
98         // If the element is barred from constraint validation or valid,
99         // the validation message will be the empty string.
100         titleText = tipElement.validationMessage || null;
101       }
103       // If the element is an <input type='file'> without a title, we should show
104       // the current file selection.
105       if (
106         titleText == null &&
107         defView.HTMLInputElement.isInstance(tipElement) &&
108         tipElement.type == "file"
109       ) {
110         try {
111           titleText = getFileInputTitleText(tipElement);
112         } catch (ex) {}
113       }
115       if (
116         lookingForSVGTitle &&
117         tipElement.parentNode.nodeType != defView.Node.DOCUMENT_NODE
118       ) {
119         for (let childNode of tipElement.childNodes) {
120           if (defView.SVGTitleElement.isInstance(childNode)) {
121             titleText = childNode.textContent;
122             break;
123           }
124         }
125       }
127       // Check texts against null so that title="" can be used to undefine a
128       // title on a child element.
129       if (titleText != null || XLinkTitleText != null) {
130         break;
131       }
132     }
134     return [titleText, XLinkTitleText].some(function (t) {
135       if (t && /\S/.test(t)) {
136         // Make CRLF and CR render one line break each.
137         textOut.value = t.replace(/\r\n?/g, "\n");
139         if (tipElement) {
140           direction = defView
141             .getComputedStyle(tipElement)
142             .getPropertyValue("direction");
143         }
145         directionOut.value = direction;
146         return true;
147       }
149       return false;
150     });
151   },
153   classID: Components.ID("{f376627f-0bbc-47b8-887e-fc92574cc91f}"),
154   QueryInterface: ChromeUtils.generateQI(["nsITooltipTextProvider"]),