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 147058 c68a3f506dbd55ed6aa145d88f5b5683288fe947
parent 147057 9d115a4053710a710291c4024761d3b76f5a7b1c
child 147059 a4e288cfa8d3ad35b917874c2a1dd6813e73b821
push idunknown
push userunknown
push dateunknown
reviewersrobcee
bugs760876
milestone26.0a1
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;