Bug 760876 - Part 2: make output links keyboard accessible; r=robcee
authorMihai Sucan <mihai.sucan@gmail.com>
Thu, 05 Sep 2013 19:29:13 +0300
changeset 159965 c68a3f506dbd55ed6aa145d88f5b5683288fe947
parent 159964 9d115a4053710a710291c4024761d3b76f5a7b1c
child 159966 a4e288cfa8d3ad35b917874c2a1dd6813e73b821
push id2961
push userlsblakk@mozilla.com
push dateMon, 28 Oct 2013 21:59:28 +0000
treeherdermozilla-beta@73ef4f13486f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrobcee
bugs760876
milestone26.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 760876 - Part 2: make output links keyboard accessible; r=robcee
browser/devtools/webconsole/console-output.js
browser/devtools/webconsole/webconsole.js
browser/themes/shared/devtools/webconsole.inc.css
--- a/browser/devtools/webconsole/console-output.js
+++ b/browser/devtools/webconsole/console-output.js
@@ -386,16 +386,27 @@ Messages.NavigationMarker.prototype = He
       url = url.substr(0, pos);
     }
 
     let doc = this.output.document;
     let urlnode = doc.createElementNS(XHTML_NS, "a");
     urlnode.className = "url";
     urlnode.textContent = url;
     urlnode.title = this._url;
+    urlnode.href = this._url;
+    urlnode.draggable = false;
+
+    // This is going into the WebConsoleFrame object instance that owns
+    // the ConsoleOutput object. The WebConsoleFrame owner is the WebConsole
+    // object instance from hudservice.js.
+    // TODO: move _addMessageLinkCallback() into ConsoleOutput once bug 778766
+    // is fixed.
+    this.output.owner._addMessageLinkCallback(urlnode, () => {
+      this.output.owner.owner.openLink(this._url);
+    });
 
     let render = Messages.BaseMessage.prototype.render.bind(this);
     render().element.appendChild(urlnode);
     this.element.classList.add("navigation-marker");
     this.element.url = this._url;
 
     return this;
   },
--- a/browser/devtools/webconsole/webconsole.js
+++ b/browser/devtools/webconsole/webconsole.js
@@ -1080,20 +1080,32 @@ WebConsoleFrame.prototype = {
         break;
       }
 
       case "trace": {
         let filename = WebConsoleUtils.abbreviateSourceURL(aMessage.filename);
         let functionName = aMessage.functionName ||
                            l10n.getStr("stacktrace.anonymousFunction");
 
-        body = l10n.getFormatStr("stacktrace.outputMessage",
-                                 [filename, functionName, sourceLine]);
-
-        clipboardText = body + "\n";
+        body = this.document.createElementNS(XHTML_NS, "a");
+        body.setAttribute("aria-haspopup", true);
+        body.href = "#";
+        body.draggable = false;
+        body.textContent = l10n.getFormatStr("stacktrace.outputMessage",
+                                             [filename, functionName,
+                                              sourceLine]);
+
+        this._addMessageLinkCallback(body, () => {
+          this.jsterm.openVariablesView({
+            rawObject: aMessage.stacktrace,
+            autofocus: true,
+          });
+        });
+
+        clipboardText = body.textContent + "\n";
 
         aMessage.stacktrace.forEach(function(aFrame) {
           clipboardText += aFrame.filename + " :: " +
                            aFrame.functionName + " :: " +
                            aFrame.lineNumber + "\n";
         });
 
         clipboardText = clipboardText.trimRight();
@@ -1170,26 +1182,18 @@ WebConsoleFrame.prototype = {
 
     if (objectActors.size > 0) {
       node._objectActors = objectActors;
 
       let repeatNode = node.getElementsByClassName("webconsole-msg-repeat")[0];
       repeatNode._uid += [...objectActors].join("-");
     }
 
-    // Make the node bring up the variables view, to allow the user to inspect
-    // the stack trace.
     if (level == "trace") {
       node._stacktrace = aMessage.stacktrace;
-
-      this.makeOutputMessageLink(node, () =>
-        this.jsterm.openVariablesView({
-          rawObject: node._stacktrace,
-          autofocus: true,
-        }));
     }
 
     return node;
   },
 
   /**
    * Handle ConsoleAPICall objects received from the server. This method outputs
    * the window.console API call.
@@ -1370,34 +1374,39 @@ WebConsoleFrame.prototype = {
     let pos = displayUrl.indexOf("?");
     if (pos > -1) {
       displayUrl = displayUrl.substr(0, pos);
     }
 
     let urlNode = this.document.createElementNS(XHTML_NS, "a");
     urlNode.classList.add("webconsole-msg-url");
     urlNode.setAttribute("title", request.url);
+    urlNode.href = request.url;
     urlNode.textContent = displayUrl;
+    urlNode.draggable = false;
     body.appendChild(urlNode);
     body.appendChild(this.document.createTextNode(" "));
 
     if (mixedRequest) {
       urlNode.classList.add("webconsole-mixed-content");
       this.makeMixedContentNode(body);
     }
 
     let statusNode = this.document.createElementNS(XHTML_NS, "a");
     statusNode.classList.add("webconsole-msg-status");
     body.appendChild(statusNode);
 
-    this.makeOutputMessageLink(messageNode, function WCF_net_message_link() {
+    let onClick = () => {
       if (!messageNode._panelOpen) {
         this.openNetworkPanel(messageNode, networkInfo);
       }
-    }.bind(this));
+    };
+
+    this._addMessageLinkCallback(urlNode, onClick);
+    this._addMessageLinkCallback(statusNode, onClick);
 
     networkInfo.node = messageNode;
 
     this._updateNetMessage(aActorId);
 
     return messageNode;
   },
 
@@ -1409,26 +1418,27 @@ WebConsoleFrame.prototype = {
    */
   makeMixedContentNode: function WCF_makeMixedContentNode(aLinkNode)
   {
     let mixedContentWarning = "[" + l10n.getStr("webConsoleMixedContentWarning") + "]";
 
     // Mixed content warning message links to a Learn More page
     let mixedContentWarningNode = this.document.createElementNS(XHTML_NS, "a");
     mixedContentWarningNode.title = MIXED_CONTENT_LEARN_MORE;
+    mixedContentWarningNode.href = MIXED_CONTENT_LEARN_MORE;
     mixedContentWarningNode.classList.add("webconsole-mixed-content-link");
     mixedContentWarningNode.textContent = mixedContentWarning;
+    mixedContentWarningNode.draggable = false;
 
     aLinkNode.appendChild(mixedContentWarningNode);
 
-    mixedContentWarningNode.addEventListener("click", function(aEvent) {
+    this._addMessageLinkCallback(mixedContentWarningNode, (aNode, aEvent) => {
+      aEvent.stopPropagation();
       this.owner.openLink(MIXED_CONTENT_LEARN_MORE);
-      aEvent.preventDefault();
-      aEvent.stopPropagation();
-    }.bind(this));
+    });
   },
 
   /**
    * Adds a more info link node to messages based on the nsIScriptError object
    * that we need to report to the console
    *
    * @param aNode
    *        The node to which we will be adding the more info link node
@@ -1471,24 +1481,25 @@ WebConsoleFrame.prototype = {
    */
   addLearnMoreWarningNode:
   function WCF_addLearnMoreWarningNode(aNode, aURL)
   {
     let moreInfoLabel = "[" + l10n.getStr("webConsoleMoreInfoLabel") + "]";
 
     let warningNode = this.document.createElementNS(XHTML_NS, "a");
     warningNode.title = aURL;
+    warningNode.href = aURL;
+    warningNode.draggable = false;
     warningNode.textContent = moreInfoLabel;
     warningNode.classList.add("webconsole-learn-more-link");
 
-    warningNode.addEventListener("click", function(aEvent) {
+    this._addMessageLinkCallback(warningNode, (aNode, aEvent) => {
+      aEvent.stopPropagation();
       this.owner.openLink(aURL);
-      aEvent.preventDefault();
-      aEvent.stopPropagation();
-    }.bind(this));
+    });
 
     aNode.appendChild(warningNode);
   },
 
   /**
    * Log file activity.
    *
    * @param string aFileURI
@@ -1497,23 +1508,25 @@ WebConsoleFrame.prototype = {
    *         The message element to display in the Web Console output.
    */
   logFileActivity: function WCF_logFileActivity(aFileURI)
   {
     let urlNode = this.document.createElementNS(XHTML_NS, "a");
     urlNode.setAttribute("title", aFileURI);
     urlNode.classList.add("webconsole-msg-url");
     urlNode.textContent = aFileURI;
+    urlNode.draggable = false;
+    urlNode.href = aFileURI;
 
     let outputNode = this.createMessageNode(CATEGORY_NETWORK, SEVERITY_LOG,
                                             urlNode, null, null, aFileURI);
 
-    this.makeOutputMessageLink(outputNode, function WCF__onFileClick() {
+    this._addMessageLinkCallback(urlNode, () => {
       this.owner.viewSource(aFileURI);
-    }.bind(this));
+    });
 
     return outputNode;
   },
 
   /**
    * Handle the file activity messages coming from the remote Web Console.
    *
    * @param string aFileURI
@@ -2441,31 +2454,35 @@ WebConsoleFrame.prototype = {
 
       if (aItem && typeof aItem != "object" || !inspectable) {
         aContainer.appendChild(this.document.createTextNode(text));
 
         if (aItem.type && aItem.type == "longString") {
           let ellipsis = this.document.createElementNS(XHTML_NS, "a");
           ellipsis.classList.add("longStringEllipsis");
           ellipsis.textContent = l10n.getStr("longStringEllipsis");
+          ellipsis.href = "#";
+          ellipsis.draggable = false;
 
           let formatter = function(s) '"' + s + '"';
 
           this._addMessageLinkCallback(ellipsis,
             this._longStringClick.bind(this, aMessage, aItem, formatter));
 
           aContainer.appendChild(ellipsis);
         }
         return;
       }
 
       // For inspectable objects.
       let elem = this.document.createElementNS(XHTML_NS, "a");
       elem.setAttribute("aria-haspopup", "true");
       elem.textContent = text;
+      elem.href = "#";
+      elem.draggable = false;
 
       this._addMessageLinkCallback(elem,
         this._consoleLogClick.bind(this, elem, aItem));
 
       aContainer.appendChild(elem);
     }, this);
   },
 
@@ -2479,24 +2496,20 @@ WebConsoleFrame.prototype = {
    *        The message element.
    * @param object aActor
    *        The LongStringActor instance we work with.
    * @param [function] aFormatter
    *        Optional function you can use to format the string received from the
    *        server, before being displayed in the console.
    * @param nsIDOMElement aEllipsis
    *        The DOM element the user can click on to expand the string.
-   * @param nsIDOMEvent aEvent
-   *        The DOM click event triggered by the user.
    */
   _longStringClick:
-  function WCF__longStringClick(aMessage, aActor, aFormatter, aEllipsis, aEvent)
+  function WCF__longStringClick(aMessage, aActor, aFormatter, aEllipsis)
   {
-    aEvent.preventDefault();
-
     if (!aFormatter) {
       aFormatter = function(s) s;
     }
 
     let longString = this.webConsoleClient.longString(aActor);
     let toIndex = Math.min(longString.length, MAX_LONG_STRING_LENGTH);
     longString.substring(longString.initial.length, toIndex,
       function WCF__onSubstring(aResponse) {
@@ -2538,39 +2551,43 @@ WebConsoleFrame.prototype = {
   createLocationNode: function WCF_createLocationNode(aSourceURL, aSourceLine)
   {
     let locationNode = this.document.createElementNS(XHTML_NS, "a");
 
     // Create the text, which consists of an abbreviated version of the URL
     // plus an optional line number. Scratchpad URLs should not be abbreviated.
     let displayLocation;
     let fullURL;
+    let isScratchpad = false;
 
     if (/^Scratchpad\/\d+$/.test(aSourceURL)) {
       displayLocation = aSourceURL;
       fullURL = aSourceURL;
+      isScratchpad = true;
     }
     else {
       fullURL = aSourceURL.split(" -> ").pop();
       displayLocation = WebConsoleUtils.abbreviateSourceURL(fullURL);
     }
 
     if (aSourceLine) {
       displayLocation += ":" + aSourceLine;
       locationNode.sourceLine = aSourceLine;
     }
 
     locationNode.textContent = " " + displayLocation;
+    locationNode.href = isScratchpad ? "#" : fullURL;
+    locationNode.draggable = false;
     locationNode.setAttribute("title", aSourceURL);
     locationNode.classList.add("webconsole-location");
     locationNode.classList.add("devtools-monospace");
 
     // Make the location clickable.
-    locationNode.addEventListener("click", () => {
-      if (/^Scratchpad\/\d+$/.test(aSourceURL)) {
+    this._addMessageLinkCallback(locationNode, () => {
+      if (isScratchpad) {
         let wins = Services.wm.getEnumerator("devtools:scratchpad");
 
         while (wins.hasMoreElements()) {
           let win = wins.getNext();
 
           if (win.Scratchpad.uniqueName === aSourceURL) {
             win.focus();
             return;
@@ -2582,17 +2599,17 @@ WebConsoleFrame.prototype = {
       }
       else if (locationNode.parentNode.category == CATEGORY_JS ||
                locationNode.parentNode.category == CATEGORY_WEBDEV) {
         this.owner.viewSourceInDebugger(fullURL, aSourceLine);
       }
       else {
         this.owner.viewSource(fullURL, aSourceLine);
       }
-    }, true);
+    });
 
     return locationNode;
   },
 
   /**
    * Adjusts the category and severity of the given message, clearing the old
    * category and severity if present.
    *
@@ -2626,60 +2643,47 @@ WebConsoleFrame.prototype = {
                                CATEGORY_CLASS_FRAGMENTS[aNewCategory]);
     aMessageNode.classList.add("webconsole-msg-" +
                                SEVERITY_CLASS_FRAGMENTS[aNewSeverity]);
     let key = "hud-" + MESSAGE_PREFERENCE_KEYS[aNewCategory][aNewSeverity];
     aMessageNode.classList.add(key);
   },
 
   /**
-   * Make a link given an output element.
-   *
-   * @param nsIDOMNode aNode
-   *        The message element you want to make a link for.
-   * @param function aCallback
-   *        The function you want invoked when the user clicks on the message
-   *        element.
-   */
-  makeOutputMessageLink: function WCF_makeOutputMessageLink(aNode, aCallback)
-  {
-    let linkNode;
-    if (aNode.category === CATEGORY_NETWORK) {
-      linkNode = aNode.querySelector(".webconsole-msg-link, .webconsole-msg-url");
-    }
-    else {
-      linkNode = aNode.querySelector(".webconsole-msg-body");
-      linkNode.classList.add("hud-clickable");
-    }
-
-    linkNode.setAttribute("aria-haspopup", "true");
-
-    this._addMessageLinkCallback(aNode, aCallback);
-  },
-
-  /**
    * Add the mouse event handlers needed to make a link.
    *
    * @private
    * @param nsIDOMNode aNode
    *        The node for which you want to add the event handlers.
    * @param function aCallback
    *        The function you want to invoke on click.
    */
   _addMessageLinkCallback: function WCF__addMessageLinkCallback(aNode, aCallback)
   {
     aNode.addEventListener("mousedown", function(aEvent) {
+      this._mousedown = true;
       this._startX = aEvent.clientX;
       this._startY = aEvent.clientY;
     }, false);
 
     aNode.addEventListener("click", function(aEvent) {
-      if (aEvent.detail != 1 || aEvent.button != 0 ||
-          (this._startX != aEvent.clientX &&
-           this._startY != aEvent.clientY)) {
+      let mousedown = this._mousedown;
+      this._mousedown = false;
+
+      // Do not allow middle/right-click or 2+ clicks.
+      if (aEvent.detail != 1 || aEvent.button != 0) {
+        return;
+      }
+
+      aEvent.preventDefault();
+
+      // If this event started with a mousedown event and it ends at a different
+      // location, we consider this text selection.
+      if (mousedown && this._startX != aEvent.clientX &&
+          this._startY != aEvent.clientY) {
         return;
       }
 
       aCallback(this, aEvent);
     }, false);
   },
 
   /**
@@ -3137,20 +3141,22 @@ JSTerm.prototype = {
 
     if (WebConsoleUtils.isActorGrip(result)) {
       node._objectActors.add(result.actor);
 
       if (result.type == "longString") {
         // Add an ellipsis to expand the short string if the object is not
         // inspectable.
 
-        let body = node.querySelector(".webconsole-msg-body");
+        let body = node.getElementsByClassName("webconsole-msg-body")[0];
         let ellipsis = this.hud.document.createElementNS(XHTML_NS, "a");
         ellipsis.classList.add("longStringEllipsis");
         ellipsis.textContent = l10n.getStr("longStringEllipsis");
+        ellipsis.href = "#";
+        ellipsis.draggable = false;
 
         let formatter = function(s) '"' + s + '"';
         let onclick = this.hud._longStringClick.bind(this.hud, node, result,
                                                     formatter);
         this.hud._addMessageLinkCallback(ellipsis, onclick);
 
         body.appendChild(ellipsis);
 
@@ -3660,29 +3666,35 @@ JSTerm.prototype = {
    *        the UNIX epoch). If no timestamp is provided then Date.now() is
    *        used.
    * @return nsIDOMNode
    *         The new message node.
    */
   writeOutputJS:
   function JST_writeOutputJS(aOutputMessage, aCallback, aNodeAfter, aTimestamp)
   {
-    let node = this.writeOutput(aOutputMessage, CATEGORY_OUTPUT, SEVERITY_LOG,
-                                aNodeAfter, aTimestamp);
+    let link = null;
     if (aCallback) {
-      this.hud.makeOutputMessageLink(node, aCallback);
-    }
-    return node;
+      link = this.hud.document.createElementNS(XHTML_NS, "a");
+      link.setAttribute("aria-haspopup", true);
+      link.textContent = aOutputMessage;
+      link.href = "#";
+      link.draggable = false;
+      this.hud._addMessageLinkCallback(link, aCallback);
+    }
+
+    return this.writeOutput(link || aOutputMessage, CATEGORY_OUTPUT,
+                            SEVERITY_LOG, aNodeAfter, aTimestamp);
   },
 
   /**
    * Writes a message to the HUD that originates from the interactive
    * JavaScript console.
    *
-   * @param string aOutputMessage
+   * @param nsIDOMNode|string aOutputMessage
    *        The message to display.
    * @param number aCategory
    *        The category of message: one of the CATEGORY_ constants.
    * @param number aSeverity
    *        The severity of message: one of the SEVERITY_ constants.
    * @param nsIDOMNode [aNodeAfter]
    *        Optional DOM node after which you want to insert the new message.
    *        This is used when execution results need to be inserted immediately
--- a/browser/themes/shared/devtools/webconsole.inc.css
+++ b/browser/themes/shared/devtools/webconsole.inc.css
@@ -37,21 +37,16 @@ a:focus {
   background: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 0, 1, 0, 0) no-repeat;
   background-position: center 0.3em;
   flex: 0 0 auto;
   margin: 0 6px;
   padding: 0 4px;
   width: 8px;
 }
 
-.hud-clickable {
-  cursor: pointer;
-  text-decoration: underline;
-}
-
 .webconsole-msg-body {
   flex: 1 1 100%;
   white-space: pre-wrap;
   word-wrap: break-word;
 }
 
 .webconsole-msg-url {
   flex: 1 1 auto;