diff options
Diffstat (limited to 'toolkit/components/tooltiptext/TooltipTextProvider.js')
-rw-r--r-- | toolkit/components/tooltiptext/TooltipTextProvider.js | 148 |
1 files changed, 148 insertions, 0 deletions
diff --git a/toolkit/components/tooltiptext/TooltipTextProvider.js b/toolkit/components/tooltiptext/TooltipTextProvider.js new file mode 100644 index 000000000..a63ab83ad --- /dev/null +++ b/toolkit/components/tooltiptext/TooltipTextProvider.js @@ -0,0 +1,148 @@ +/* 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/. */ + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +function TooltipTextProvider() {} + +TooltipTextProvider.prototype = { + getNodeText(tipElement, textOut, directionOut) { + // Don't show the tooltip if the tooltip node is a document, browser, or disconnected. + if (!tipElement || !tipElement.ownerDocument || + tipElement.localName == "browser" || + (tipElement.ownerDocument.compareDocumentPosition(tipElement) & + tipElement.ownerDocument.DOCUMENT_POSITION_DISCONNECTED)) { + return false; + } + + var defView = tipElement.ownerDocument.defaultView; + // XXX Work around bug 350679: + // "Tooltips can be fired in documents with no view". + if (!defView) + return false; + + const XLinkNS = "http://www.w3.org/1999/xlink"; + const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + + var titleText = null; + var XLinkTitleText = null; + var SVGTitleText = null; + var XULtooltiptextText = null; + var lookingForSVGTitle = true; + var direction = tipElement.ownerDocument.dir; + + // If the element is invalid per HTML5 Forms specifications and has no title, + // show the constraint validation error message. + if ((tipElement instanceof defView.HTMLInputElement || + tipElement instanceof defView.HTMLTextAreaElement || + tipElement instanceof defView.HTMLSelectElement || + tipElement instanceof defView.HTMLButtonElement) && + !tipElement.hasAttribute('title') && + (!tipElement.form || !tipElement.form.noValidate)) { + // If the element is barred from constraint validation or valid, + // the validation message will be the empty string. + titleText = tipElement.validationMessage || null; + } + + // If the element is an <input type='file'> without a title, we should show + // the current file selection. + if (!titleText && + tipElement instanceof defView.HTMLInputElement && + tipElement.type == 'file' && + !tipElement.hasAttribute('title')) { + let files = tipElement.files; + + try { + var bundle = + Services.strings.createBundle("chrome://global/locale/layout/HtmlForm.properties"); + if (files.length == 0) { + if (tipElement.multiple) { + titleText = bundle.GetStringFromName("NoFilesSelected"); + } else { + titleText = bundle.GetStringFromName("NoFileSelected"); + } + } else { + titleText = files[0].name; + // For UX and performance (jank) reasons we cap the number of + // files that we list in the tooltip to 20 plus a "and xxx more" + // line, or to 21 if exactly 21 files were picked. + const TRUNCATED_FILE_COUNT = 20; + let count = Math.min(files.length, TRUNCATED_FILE_COUNT); + for (let i = 1; i < count; ++i) { + titleText += "\n" + files[i].name; + } + if (files.length == TRUNCATED_FILE_COUNT + 1) { + titleText += "\n" + files[TRUNCATED_FILE_COUNT].name; + } else if (files.length > TRUNCATED_FILE_COUNT + 1) { + let xmoreStr = bundle.GetStringFromName("AndNMoreFiles"); + let xmoreNum = files.length - TRUNCATED_FILE_COUNT; + let tmp = {}; + Cu.import("resource://gre/modules/PluralForm.jsm", tmp); + let andXMoreStr = tmp.PluralForm.get(xmoreNum, xmoreStr).replace("#1", xmoreNum); + titleText += "\n" + andXMoreStr; + } + } + } catch (e) {} + } + + // Check texts against null so that title="" can be used to undefine a + // title on a child element. + while (tipElement && + (titleText == null) && (XLinkTitleText == null) && + (SVGTitleText == null) && (XULtooltiptextText == null)) { + + if (tipElement.nodeType == defView.Node.ELEMENT_NODE) { + if (tipElement.namespaceURI == XULNS) + XULtooltiptextText = tipElement.getAttribute("tooltiptext"); + else if (!(tipElement instanceof defView.SVGElement)) + titleText = tipElement.getAttribute("title"); + + if ((tipElement instanceof defView.HTMLAnchorElement || + tipElement instanceof defView.HTMLAreaElement || + tipElement instanceof defView.HTMLLinkElement || + tipElement instanceof defView.SVGAElement) && tipElement.href) { + XLinkTitleText = tipElement.getAttributeNS(XLinkNS, "title"); + } + if (lookingForSVGTitle && + (!(tipElement instanceof defView.SVGElement) || + tipElement.parentNode.nodeType == defView.Node.DOCUMENT_NODE)) { + lookingForSVGTitle = false; + } + if (lookingForSVGTitle) { + for (let childNode of tipElement.childNodes) { + if (childNode instanceof defView.SVGTitleElement) { + SVGTitleText = childNode.textContent; + break; + } + } + } + + direction = defView.getComputedStyle(tipElement, "") + .getPropertyValue("direction"); + } + + tipElement = tipElement.parentNode; + } + + return [titleText, XLinkTitleText, SVGTitleText, XULtooltiptextText].some(function (t) { + if (t && /\S/.test(t)) { + // Make CRLF and CR render one line break each. + textOut.value = t.replace(/\r\n?/g, '\n'); + directionOut.value = direction; + return true; + } + + return false; + }); + }, + + classID : Components.ID("{f376627f-0bbc-47b8-887e-fc92574cc91f}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsITooltipTextProvider]), +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TooltipTextProvider]); + |