summaryrefslogtreecommitdiffstats
path: root/toolkit/content/widgets/text.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/content/widgets/text.js')
-rw-r--r--toolkit/content/widgets/text.js389
1 files changed, 389 insertions, 0 deletions
diff --git a/toolkit/content/widgets/text.js b/toolkit/content/widgets/text.js
new file mode 100644
index 0000000000..ca10f1489e
--- /dev/null
+++ b/toolkit/content/widgets/text.js
@@ -0,0 +1,389 @@
+/* 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";
+
+// This is loaded into all XUL windows. Wrap in a block to prevent
+// leaking to window scope.
+{
+ const MozXULTextElement = MozElements.MozElementMixin(XULTextElement);
+
+ let gInsertSeparator = false;
+ let gAlwaysAppendAccessKey = false;
+ let gUnderlineAccesskey =
+ Services.prefs.getIntPref("ui.key.menuAccessKey") != 0;
+ if (gUnderlineAccesskey) {
+ try {
+ const nsIPrefLocalizedString = Ci.nsIPrefLocalizedString;
+ const prefNameInsertSeparator =
+ "intl.menuitems.insertseparatorbeforeaccesskeys";
+ const prefNameAlwaysAppendAccessKey =
+ "intl.menuitems.alwaysappendaccesskeys";
+
+ let val = Services.prefs.getComplexValue(
+ prefNameInsertSeparator,
+ nsIPrefLocalizedString
+ ).data;
+ gInsertSeparator = val == "true";
+
+ val = Services.prefs.getComplexValue(
+ prefNameAlwaysAppendAccessKey,
+ nsIPrefLocalizedString
+ ).data;
+ gAlwaysAppendAccessKey = val == "true";
+ } catch (e) {
+ gInsertSeparator = gAlwaysAppendAccessKey = true;
+ }
+ }
+
+ class MozTextLabel extends MozXULTextElement {
+ constructor() {
+ super();
+ this._lastFormattedAccessKey = null;
+ this.addEventListener("click", this._onClick);
+ }
+
+ static get observedAttributes() {
+ return ["accesskey"];
+ }
+
+ set textContent(val) {
+ super.textContent = val;
+ this._lastFormattedAccessKey = null;
+ this.formatAccessKey();
+ }
+
+ get textContent() {
+ return super.textContent;
+ }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ if (!this.isConnectedAndReady || oldValue == newValue) {
+ return;
+ }
+
+ // Note that this is only happening when "accesskey" attribute change:
+ this.formatAccessKey();
+ }
+
+ _onClick(event) {
+ let controlElement = this.labeledControlElement;
+ if (!controlElement || this.disabled) {
+ return;
+ }
+ controlElement.focus();
+ const XUL_NS =
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+ if (controlElement.namespaceURI != XUL_NS) {
+ return;
+ }
+
+ if (
+ (controlElement.localName == "checkbox" ||
+ controlElement.localName == "radio") &&
+ controlElement.getAttribute("disabled") == "true"
+ ) {
+ return;
+ }
+
+ if (controlElement.localName == "checkbox") {
+ controlElement.checked = !controlElement.checked;
+ } else if (controlElement.localName == "radio") {
+ controlElement.control.selectedItem = controlElement;
+ }
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ return;
+ }
+
+ this.formatAccessKey();
+ }
+
+ set accessKey(val) {
+ this.setAttribute("accesskey", val);
+ var control = this.labeledControlElement;
+ if (control) {
+ control.setAttribute("accesskey", val);
+ }
+ }
+
+ get accessKey() {
+ let accessKey = this.getAttribute("accesskey");
+ return accessKey ? accessKey[0] : null;
+ }
+
+ get labeledControlElement() {
+ let control = this.control;
+ return control ? document.getElementById(control) : null;
+ }
+
+ set control(val) {
+ this.setAttribute("control", val);
+ }
+
+ get control() {
+ return this.getAttribute("control");
+ }
+
+ // This is used to match the rendering of accesskeys from nsTextBoxFrame.cpp (i.e. when the
+ // label uses [value]). So this is just for when we have textContent.
+ formatAccessKey() {
+ // Skip doing any DOM manipulation whenever possible:
+ let accessKey = this.accessKey;
+ if (
+ !gUnderlineAccesskey ||
+ !this.isConnectedAndReady ||
+ this._lastFormattedAccessKey == accessKey ||
+ !this.textContent
+ ) {
+ return;
+ }
+ this._lastFormattedAccessKey = accessKey;
+ if (this.accessKeySpan) {
+ // Clear old accesskey
+ mergeElement(this.accessKeySpan);
+ this.accessKeySpan = null;
+ }
+
+ if (this.hiddenColon) {
+ mergeElement(this.hiddenColon);
+ this.hiddenColon = null;
+ }
+
+ if (this.accessKeyParens) {
+ this.accessKeyParens.remove();
+ this.accessKeyParens = null;
+ }
+
+ // If we used to have an accessKey but not anymore, we're done here
+ if (!accessKey) {
+ return;
+ }
+
+ let labelText = this.textContent;
+ let accessKeyIndex = -1;
+ if (!gAlwaysAppendAccessKey) {
+ accessKeyIndex = labelText.indexOf(accessKey);
+ if (accessKeyIndex < 0) {
+ // Try again in upper case
+ accessKeyIndex = labelText
+ .toUpperCase()
+ .indexOf(accessKey.toUpperCase());
+ }
+ } else if (labelText.endsWith(`(${accessKey.toUpperCase()})`)) {
+ accessKeyIndex = labelText.length - (1 + accessKey.length); // = index of accessKey.
+ }
+
+ const HTML_NS = "http://www.w3.org/1999/xhtml";
+ this.accessKeySpan = document.createElementNS(HTML_NS, "span");
+ this.accessKeySpan.className = "accesskey";
+
+ // Note that if you change the following code, see the comment of
+ // nsTextBoxFrame::UpdateAccessTitle.
+
+ // If accesskey is in the string, underline it:
+ if (accessKeyIndex >= 0) {
+ wrapChar(this, this.accessKeySpan, accessKeyIndex);
+ return;
+ }
+
+ // If accesskey is not in string, append in parentheses
+ // If end is colon, we should insert before colon.
+ // i.e., "label:" -> "label(X):"
+ let colonHidden = false;
+ if (/:$/.test(labelText)) {
+ labelText = labelText.slice(0, -1);
+ this.hiddenColon = document.createElementNS(HTML_NS, "span");
+ this.hiddenColon.className = "hiddenColon";
+ this.hiddenColon.style.display = "none";
+ // Hide the last colon by using span element.
+ // I.e., label<span style="display:none;">:</span>
+ wrapChar(this, this.hiddenColon, labelText.length);
+ colonHidden = true;
+ }
+ // If end is space(U+20),
+ // we should not add space before parentheses.
+ let endIsSpace = false;
+ if (/ $/.test(labelText)) {
+ endIsSpace = true;
+ }
+
+ this.accessKeyParens = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "span"
+ );
+ this.appendChild(this.accessKeyParens);
+ if (gInsertSeparator && !endIsSpace) {
+ this.accessKeyParens.textContent = " (";
+ } else {
+ this.accessKeyParens.textContent = "(";
+ }
+ this.accessKeySpan.textContent = accessKey.toUpperCase();
+ this.accessKeyParens.appendChild(this.accessKeySpan);
+ if (!colonHidden) {
+ this.accessKeyParens.appendChild(document.createTextNode(")"));
+ } else {
+ this.accessKeyParens.appendChild(document.createTextNode("):"));
+ }
+ }
+ }
+
+ customElements.define("label", MozTextLabel);
+
+ function mergeElement(element) {
+ // If the element has been removed already, return:
+ if (!element.isConnected) {
+ return;
+ }
+ if (Text.isInstance(element.previousSibling)) {
+ element.previousSibling.appendData(element.textContent);
+ } else {
+ element.parentNode.insertBefore(element.firstChild, element);
+ }
+ element.remove();
+ }
+
+ function wrapChar(parent, element, index) {
+ let treeWalker = document.createNodeIterator(
+ parent,
+ NodeFilter.SHOW_TEXT,
+ null
+ );
+ let node = treeWalker.nextNode();
+ while (index >= node.length) {
+ index -= node.length;
+ node = treeWalker.nextNode();
+ }
+ if (index) {
+ node = node.splitText(index);
+ }
+
+ node.parentNode.insertBefore(element, node);
+ if (node.length > 1) {
+ node.splitText(1);
+ }
+ element.appendChild(node);
+ }
+
+ class MozTextLink extends MozXULTextElement {
+ constructor() {
+ super();
+
+ this.addEventListener(
+ "click",
+ event => {
+ if (event.button == 0 || event.button == 1) {
+ this.open(event);
+ }
+ },
+ true
+ );
+
+ this.addEventListener("keypress", event => {
+ if (event.keyCode != KeyEvent.DOM_VK_RETURN) {
+ return;
+ }
+ this.click();
+ });
+ }
+
+ connectedCallback() {
+ this.classList.add("text-link");
+ }
+
+ set href(val) {
+ this.setAttribute("href", val);
+ }
+
+ get href() {
+ return this.getAttribute("href");
+ }
+
+ open(aEvent) {
+ var href = this.href;
+ if (!href || this.disabled || aEvent.defaultPrevented) {
+ return;
+ }
+
+ var uri = null;
+ try {
+ const nsISSM = Ci.nsIScriptSecurityManager;
+ const secMan =
+ Cc["@mozilla.org/scriptsecuritymanager;1"].getService(nsISSM);
+
+ uri = Services.io.newURI(href);
+
+ let principal;
+ if (this.getAttribute("useoriginprincipal") == "true") {
+ principal = this.nodePrincipal;
+ } else {
+ principal = secMan.createNullPrincipal({});
+ }
+ try {
+ secMan.checkLoadURIWithPrincipal(
+ principal,
+ uri,
+ nsISSM.DISALLOW_INHERIT_PRINCIPAL
+ );
+ } catch (ex) {
+ var msg =
+ "Error: Cannot open a " +
+ uri.scheme +
+ ": link using \
+ the text-link binding.";
+ console.error(msg);
+ return;
+ }
+
+ const cID = "@mozilla.org/uriloader/external-protocol-service;1";
+ const nsIEPS = Ci.nsIExternalProtocolService;
+ var protocolSvc = Cc[cID].getService(nsIEPS);
+
+ // if the scheme is not an exposed protocol, then opening this link
+ // should be deferred to the system's external protocol handler
+ if (!protocolSvc.isExposedProtocol(uri.scheme)) {
+ protocolSvc.loadURI(uri, principal);
+ aEvent.preventDefault();
+ return;
+ }
+ } catch (ex) {
+ console.error(ex);
+ }
+
+ aEvent.preventDefault();
+ href = uri ? uri.spec : href;
+
+ // Try handing off the link to the host application, e.g. for
+ // opening it in a tabbed browser.
+ var linkHandled = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
+ Ci.nsISupportsPRBool
+ );
+ linkHandled.data = false;
+ let { shiftKey, ctrlKey, metaKey, altKey, button } = aEvent;
+ let data = { shiftKey, ctrlKey, metaKey, altKey, button, href };
+ Services.obs.notifyObservers(
+ linkHandled,
+ "handle-xul-text-link",
+ JSON.stringify(data)
+ );
+ if (linkHandled.data) {
+ return;
+ }
+
+ // otherwise, fall back to opening the anchor directly
+ var win = window;
+ if (window.isChromeWindow) {
+ while (win.opener && !win.opener.closed) {
+ win = win.opener;
+ }
+ }
+ win.open(href, "_blank", "noopener");
+ }
+ }
+
+ customElements.define("text-link", MozTextLink, { extends: "label" });
+}