Bug 760876 - Part 1: switch from XUL to XHTML for the Web Console output; r=robcee,paul
authorMihai Sucan <mihai.sucan@gmail.com>
Fri, 13 Sep 2013 15:06:46 +0300
changeset 159964 9d115a4053710a710291c4024761d3b76f5a7b1c
parent 159885 15ba1cea7963b634705ba4cf8c20c98ca0b0f353
child 159965 c68a3f506dbd55ed6aa145d88f5b5683288fe947
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, paul
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 1: switch from XUL to XHTML for the Web Console output; r=robcee,paul
browser/devtools/webconsole/console-output.js
browser/devtools/webconsole/webconsole.js
browser/devtools/webconsole/webconsole.xul
browser/locales/en-US/chrome/browser/devtools/webconsole.properties
browser/themes/shared/devtools/webconsole.inc.css
toolkit/devtools/webconsole/client.js
toolkit/devtools/webconsole/network-helper.js
toolkit/devtools/webconsole/utils.js
--- a/browser/devtools/webconsole/console-output.js
+++ b/browser/devtools/webconsole/console-output.js
@@ -50,26 +50,38 @@ const COMPAT = {
 function ConsoleOutput(owner)
 {
   this.owner = owner;
   this._onFlushOutputMessage = this._onFlushOutputMessage.bind(this);
 }
 
 ConsoleOutput.prototype = {
   /**
+   * The output container.
+   * @type DOMElement
+   */
+  get element() {
+    return this.owner.outputNode;
+  },
+
+  /**
    * The document that holds the output.
    * @type DOMDocument
    */
-  get document() this.owner.document,
+  get document() {
+    return this.owner.document;
+  },
 
   /**
    * The DOM window that holds the output.
    * @type Window
    */
-  get window() this.owner.window,
+  get window() {
+    return this.owner.window;
+  },
 
   /**
    * Add a message to output.
    *
    * @param object ...args
    *        Any number of Message objects.
    * @return this
    */
@@ -98,16 +110,110 @@ ConsoleOutput.prototype = {
    *         The message DOM element that can be added to the console output.
    */
   _onFlushOutputMessage: function(message)
   {
     return message.render().element;
   },
 
   /**
+   * Get an array of selected messages. This list is based on the text selection
+   * start and end points.
+   *
+   * @param number [limit]
+   *        Optional limit of selected messages you want. If no value is given,
+   *        all of the selected messages are returned.
+   * @return array
+   *         Array of DOM elements for each message that is currently selected.
+   */
+  getSelectedMessages: function(limit)
+  {
+    let selection = this.window.getSelection();
+    if (selection.isCollapsed) {
+      return [];
+    }
+
+    if (selection.containsNode(this.element, true)) {
+      return Array.slice(this.element.children);
+    }
+
+    let anchor = this.getMessageForElement(selection.anchorNode);
+    let focus = this.getMessageForElement(selection.focusNode);
+    if (!anchor || !focus) {
+      return [];
+    }
+
+    let start, end;
+    if (anchor.timestamp > focus.timestamp) {
+      start = focus;
+      end = anchor;
+    } else {
+      start = anchor;
+      end = focus;
+    }
+
+    let result = [];
+    let current = start;
+    while (current) {
+      result.push(current);
+      if (current == end || (limit && result.length == limit)) {
+        break;
+      }
+      current = current.nextSibling;
+    }
+    return result;
+  },
+
+  /**
+   * Find the DOM element of a message for any given descendant.
+   *
+   * @param DOMElement elem
+   *        The element to start the search from.
+   * @return DOMElement|null
+   *         The DOM element of the message, if any.
+   */
+  getMessageForElement: function(elem)
+  {
+    while (elem && elem.parentNode) {
+      if (elem.classList && elem.classList.contains("hud-msg-node")) {
+        return elem;
+      }
+      elem = elem.parentNode;
+    }
+    return null;
+  },
+
+  /**
+   * Select all messages.
+   */
+  selectAllMessages: function()
+  {
+    let selection = this.window.getSelection();
+    selection.removeAllRanges();
+    let range = this.document.createRange();
+    range.selectNodeContents(this.element);
+    selection.addRange(range);
+  },
+
+  /**
+   * Add a message to the selection.
+   *
+   * @param DOMElement elem
+   *        The message element to select.
+   */
+  selectMessage: function(elem)
+  {
+    let selection = this.window.getSelection();
+    selection.removeAllRanges();
+    let range = this.document.createRange();
+    range.selectNodeContents(elem);
+    selection.addRange(range);
+  },
+
+  /**
    * Destroy this ConsoleOutput instance.
    */
   destroy: function()
   {
     this.owner = null;
   },
 }; // ConsoleOutput.prototype
 
@@ -211,31 +317,25 @@ Messages.BaseMessage.prototype = {
   /**
    * Prepare the message container for the Web Console, such that it is
    * compatible with the current implementation.
    * TODO: remove this once bug 778766.
    */
   _renderCompat: function()
   {
     let doc = this.output.document;
-    let container = doc.createElementNS(XUL_NS, "richlistitem");
-    container.setAttribute("id", "console-msg-" + gSequenceId());
-    container.setAttribute("class", "hud-msg-node " + this._elementClassCompat);
+    let container = doc.createElementNS(XHTML_NS, "div");
+    container.id = "console-msg-" + gSequenceId();
+    container.className = "hud-msg-node " + this._elementClassCompat;
     container.category = this._categoryCompat;
     container.severity = this._severityCompat;
     container.clipboardText = this.textContent;
     container.timestamp = this.timestamp;
     container._messageObject = this;
 
-    let body = doc.createElementNS(XUL_NS, "description");
-    body.flex = 1;
-    body.classList.add("webconsole-msg-body");
-    body.classList.add("devtools-monospace");
-    container.appendChild(body);
-
     return container;
   },
 }; // Messages.BaseMessage.prototype
 
 
 /**
  * The NavigationMarker is used to show a page load event.
  *
@@ -282,23 +382,23 @@ Messages.NavigationMarker.prototype = He
 
     let url = this._url;
     let pos = url.indexOf("?");
     if (pos > -1) {
       url = url.substr(0, pos);
     }
 
     let doc = this.output.document;
-    let urlnode = doc.createElementNS(XHTML_NS, "span");
+    let urlnode = doc.createElementNS(XHTML_NS, "a");
     urlnode.className = "url";
     urlnode.textContent = url;
+    urlnode.title = this._url;
 
-    // Add the text in the xul:description.webconsole-msg-body element.
     let render = Messages.BaseMessage.prototype.render.bind(this);
-    render().element.firstChild.appendChild(urlnode);
+    render().element.appendChild(urlnode);
     this.element.classList.add("navigation-marker");
     this.element.url = this._url;
 
     return this;
   },
 }); // Messages.NavigationMarker.prototype
 
 
--- a/browser/devtools/webconsole/webconsole.js
+++ b/browser/devtools/webconsole/webconsole.js
@@ -28,21 +28,19 @@ loader.lazyGetter(this, "Messages",
                   () => require("devtools/webconsole/console-output").Messages);
 loader.lazyImporter(this, "ObjectClient", "resource://gre/modules/devtools/dbg-client.jsm");
 loader.lazyImporter(this, "VariablesView", "resource:///modules/devtools/VariablesView.jsm");
 loader.lazyImporter(this, "VariablesViewController", "resource:///modules/devtools/VariablesViewController.jsm");
 
 const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
 let l10n = new WebConsoleUtils.l10n(STRINGS_URI);
 
-
-// The XUL namespace.
-const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-
-const MIXED_CONTENT_LEARN_MORE = "https://developer.mozilla.org/Security/MixedContent";
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+const MIXED_CONTENT_LEARN_MORE = "https://developer.mozilla.org/docs/Security/MixedContent";
 
 const INSECURE_PASSWORDS_LEARN_MORE = "https://developer.mozilla.org/docs/Security/InsecurePasswords";
 
 const STRICT_TRANSPORT_SECURITY_LEARN_MORE = "https://developer.mozilla.org/docs/Security/HTTP_Strict_Transport_Security";
 
 const HELP_URL = "https://developer.mozilla.org/docs/Tools/Web_Console/Helpers";
 
 const VARIABLES_VIEW_URL = "chrome://browser/content/devtools/widgets/VariablesView.xul";
@@ -935,25 +933,25 @@ WebConsoleFrame.prototype = {
    * @param nsIDOMNode aOriginal
    *        The Original Node. The one being merged into.
    * @param nsIDOMNode aFiltered
    *        The node being filtered out because it is repeated.
    */
   mergeFilteredMessageNode:
   function WCF_mergeFilteredMessageNode(aOriginal, aFiltered)
   {
-    // childNodes[3].firstChild is the node containing the number of repetitions
-    // of a node.
-    let repeatNode = aOriginal.childNodes[3].firstChild;
+    let repeatNode = aOriginal.getElementsByClassName("webconsole-msg-repeat")[0];
     if (!repeatNode) {
       return; // no repeat node, return early.
     }
 
     let occurrences = parseInt(repeatNode.getAttribute("value")) + 1;
     repeatNode.setAttribute("value", occurrences);
+    repeatNode.textContent = occurrences;
+    repeatNode.title = l10n.getFormatStr("messageRepeats.tooltip", [occurrences]);
   },
 
   /**
    * Filter the message node from the output if it is a repeat.
    *
    * @private
    * @param nsIDOMNode aNode
    *        The message node to be filtered or not.
@@ -1085,17 +1083,17 @@ WebConsoleFrame.prototype = {
       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 = "";
+        clipboardText = body + "\n";
 
         aMessage.stacktrace.forEach(function(aFrame) {
           clipboardText += aFrame.filename + " :: " +
                            aFrame.functionName + " :: " +
                            aFrame.lineNumber + "\n";
         });
 
         clipboardText = clipboardText.trimRight();
@@ -1168,17 +1166,17 @@ WebConsoleFrame.prototype = {
                                       level, aMessage.timeStamp);
     if (aMessage.private) {
       node.setAttribute("private", true);
     }
 
     if (objectActors.size > 0) {
       node._objectActors = objectActors;
 
-      let repeatNode = node.querySelector(".webconsole-msg-repeat");
+      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;
 
@@ -1339,70 +1337,62 @@ WebConsoleFrame.prototype = {
   logNetEvent: function WCF_logNetEvent(aActorId)
   {
     let networkInfo = this._networkRequests[aActorId];
     if (!networkInfo) {
       return null;
     }
 
     let request = networkInfo.request;
-
-    let msgNode = this.document.createElementNS(XUL_NS, "hbox");
-
-    let methodNode = this.document.createElementNS(XUL_NS, "label");
-    methodNode.setAttribute("value", request.method);
-    methodNode.classList.add("webconsole-msg-body-piece");
-    msgNode.appendChild(methodNode);
-
-    let linkNode = this.document.createElementNS(XUL_NS, "hbox");
-    linkNode.flex = 1;
-    linkNode.classList.add("webconsole-msg-body-piece");
-    linkNode.classList.add("webconsole-msg-link");
-    msgNode.appendChild(linkNode);
-
-    let urlNode = this.document.createElementNS(XUL_NS, "label");
-    urlNode.flex = 1;
-    urlNode.setAttribute("crop", "center");
-    urlNode.setAttribute("title", request.url);
-    urlNode.setAttribute("tooltiptext", request.url);
-    urlNode.setAttribute("value", request.url);
-    urlNode.classList.add("hud-clickable");
-    urlNode.classList.add("webconsole-msg-body-piece");
-    urlNode.classList.add("webconsole-msg-url");
-    linkNode.appendChild(urlNode);
-
+    let clipboardText = request.method + " " + request.url;
     let severity = SEVERITY_LOG;
     let mixedRequest =
       WebConsoleUtils.isMixedHTTPSRequest(request.url, this.contentLocation);
     if (mixedRequest) {
-      urlNode.classList.add("webconsole-mixed-content");
-      this.makeMixedContentNode(linkNode);
-      // If we define a SEVERITY_SECURITY in the future, switch this to
-      // SEVERITY_SECURITY.
       severity = SEVERITY_WARNING;
     }
 
-    let statusNode = this.document.createElementNS(XUL_NS, "label");
-    statusNode.setAttribute("value", "");
-    statusNode.classList.add("hud-clickable");
-    statusNode.classList.add("webconsole-msg-body-piece");
-    statusNode.classList.add("webconsole-msg-status");
-    linkNode.appendChild(statusNode);
-
-    let clipboardText = request.method + " " + request.url;
+    let methodNode = this.document.createElementNS(XHTML_NS, "span");
+    methodNode.className = "webconsole-msg-method";
+    methodNode.textContent = request.method + " ";
 
     let messageNode = this.createMessageNode(CATEGORY_NETWORK, severity,
-                                             msgNode, null, null, clipboardText);
+                                             methodNode, null, null,
+                                             clipboardText);
     if (networkInfo.private) {
       messageNode.setAttribute("private", true);
     }
-
     messageNode._connectionId = aActorId;
     messageNode.url = request.url;
 
+    let body = methodNode.parentNode;
+    body.setAttribute("aria-haspopup", true);
+
+    let displayUrl = request.url;
+    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.textContent = displayUrl;
+    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() {
       if (!messageNode._panelOpen) {
         this.openNetworkPanel(messageNode, networkInfo);
       }
     }.bind(this));
 
     networkInfo.node = messageNode;
 
@@ -1417,21 +1407,20 @@ WebConsoleFrame.prototype = {
    * @param aLinkNode
    *        Parent to the requested urlNode.
    */
   makeMixedContentNode: function WCF_makeMixedContentNode(aLinkNode)
   {
     let mixedContentWarning = "[" + l10n.getStr("webConsoleMixedContentWarning") + "]";
 
     // Mixed content warning message links to a Learn More page
-    let mixedContentWarningNode = this.document.createElement("label");
-    mixedContentWarningNode.setAttribute("value", mixedContentWarning);
-    mixedContentWarningNode.setAttribute("title", mixedContentWarning);
-    mixedContentWarningNode.classList.add("hud-clickable");
+    let mixedContentWarningNode = this.document.createElementNS(XHTML_NS, "a");
+    mixedContentWarningNode.title = MIXED_CONTENT_LEARN_MORE;
     mixedContentWarningNode.classList.add("webconsole-mixed-content-link");
+    mixedContentWarningNode.textContent = mixedContentWarning;
 
     aLinkNode.appendChild(mixedContentWarningNode);
 
     mixedContentWarningNode.addEventListener("click", function(aEvent) {
       this.owner.openLink(MIXED_CONTENT_LEARN_MORE);
       aEvent.preventDefault();
       aEvent.stopPropagation();
     }.bind(this));
@@ -1478,60 +1467,46 @@ WebConsoleFrame.prototype = {
    * @param aURL
    *        The url which points to the page where the user can learn more
    *        about security issues associated with the specific message that's
    *        being logged.
    */
   addLearnMoreWarningNode:
   function WCF_addLearnMoreWarningNode(aNode, aURL)
   {
-    let moreInfoLabel =
-      "[" + l10n.getStr("webConsoleMoreInfoLabel") + "]";
-
-    // The node that holds the clickable warning node.
-    let linkNode = this.document.createElementNS(XUL_NS, "hbox");
-    linkNode.flex = 1;
-    linkNode.classList.add("webconsole-msg-body-piece");
-    linkNode.classList.add("webconsole-msg-link");
-    aNode.appendChild(linkNode);
-
-    // Create the actual warning node and make it clickable
-    let warningNode = this.document.createElement("label");
-    warningNode.setAttribute("value", moreInfoLabel);
-    warningNode.setAttribute("title", moreInfoLabel);
-    warningNode.classList.add("hud-clickable");
+    let moreInfoLabel = "[" + l10n.getStr("webConsoleMoreInfoLabel") + "]";
+
+    let warningNode = this.document.createElementNS(XHTML_NS, "a");
+    warningNode.title = aURL;
+    warningNode.textContent = moreInfoLabel;
     warningNode.classList.add("webconsole-learn-more-link");
 
     warningNode.addEventListener("click", function(aEvent) {
       this.owner.openLink(aURL);
       aEvent.preventDefault();
       aEvent.stopPropagation();
     }.bind(this));
 
-    linkNode.appendChild(warningNode);
+    aNode.appendChild(warningNode);
   },
 
   /**
    * Log file activity.
    *
    * @param string aFileURI
    *        The file URI that was loaded.
    * @return nsIDOMElement|undefined
    *         The message element to display in the Web Console output.
    */
   logFileActivity: function WCF_logFileActivity(aFileURI)
   {
-    let urlNode = this.document.createElementNS(XUL_NS, "label");
-    urlNode.flex = 1;
-    urlNode.setAttribute("crop", "center");
+    let urlNode = this.document.createElementNS(XHTML_NS, "a");
     urlNode.setAttribute("title", aFileURI);
-    urlNode.setAttribute("tooltiptext", aFileURI);
-    urlNode.setAttribute("value", aFileURI);
-    urlNode.classList.add("hud-clickable");
     urlNode.classList.add("webconsole-msg-url");
+    urlNode.textContent = aFileURI;
 
     let outputNode = this.createMessageNode(CATEGORY_NETWORK, SEVERITY_LOG,
                                             urlNode, null, null, aFileURI);
 
     this.makeOutputMessageLink(outputNode, function WCF__onFileClick() {
       this.owner.viewSource(aFileURI);
     }.bind(this));
 
@@ -1640,18 +1615,18 @@ WebConsoleFrame.prototype = {
         networkInfo.response.bodySize = aPacket.contentSize;
         networkInfo.discardResponseBody = aPacket.discardResponseBody;
         break;
       case "eventTimings":
         networkInfo.totalTime = aPacket.totalTime;
         break;
     }
 
-    if (networkInfo.node) {
-      this._updateNetMessage(aActorId);
+    if (networkInfo.node && this._updateNetMessage(aActorId)) {
+      this.emit("messages-updated", new Set([networkInfo.node]));
     }
 
     // For unit tests we pass the HTTP activity object to the test callback,
     // once requests complete.
     if (this.owner.lastFinishedRequestCallback &&
         networkInfo.updates.indexOf("responseContent") > -1 &&
         networkInfo.updates.indexOf("eventTimings") > -1) {
       this.owner.lastFinishedRequestCallback(networkInfo, this);
@@ -1660,58 +1635,64 @@ WebConsoleFrame.prototype = {
 
   /**
    * Update an output message to reflect the latest state of a network request,
    * given a network event actor ID.
    *
    * @private
    * @param string aActorId
    *        The network event actor ID for which you want to update the message.
+   * @return boolean
+   *         |true| if the message node was updated, or |false| otherwise.
    */
   _updateNetMessage: function WCF__updateNetMessage(aActorId)
   {
     let networkInfo = this._networkRequests[aActorId];
     if (!networkInfo || !networkInfo.node) {
       return;
     }
 
     let messageNode = networkInfo.node;
     let updates = networkInfo.updates;
     let hasEventTimings = updates.indexOf("eventTimings") > -1;
     let hasResponseStart = updates.indexOf("responseStart") > -1;
     let request = networkInfo.request;
     let response = networkInfo.response;
+    let updated = false;
 
     if (hasEventTimings || hasResponseStart) {
       let status = [];
       if (response.httpVersion && response.status) {
         status = [response.httpVersion, response.status, response.statusText];
       }
       if (hasEventTimings) {
         status.push(l10n.getFormatStr("NetworkPanel.durationMS",
                                       [networkInfo.totalTime]));
       }
       let statusText = "[" + status.join(" ") + "]";
 
-      let linkNode = messageNode.querySelector(".webconsole-msg-link");
-      let statusNode = linkNode.querySelector(".webconsole-msg-status");
-      statusNode.setAttribute("value", statusText);
+      let statusNode = messageNode.getElementsByClassName("webconsole-msg-status")[0];
+      statusNode.textContent = statusText;
 
       messageNode.clipboardText = [request.method, request.url, statusText]
                                   .join(" ");
 
       if (hasResponseStart && response.status >= MIN_HTTP_ERROR_CODE &&
           response.status <= MAX_HTTP_ERROR_CODE) {
         this.setMessageType(messageNode, CATEGORY_NETWORK, SEVERITY_ERROR);
       }
+
+      updated = true;
     }
 
     if (messageNode._netPanel) {
       messageNode._netPanel.update();
     }
+
+    return updated;
   },
 
   /**
    * Opens a NetworkPanel.
    *
    * @param nsIDOMNode aNode
    *        The message node you want the panel to be anchored to.
    * @param object aHttpActivity
@@ -1955,19 +1936,18 @@ WebConsoleFrame.prototype = {
     let batch = this._outputQueue.splice(0, toDisplay);
     if (!batch.length) {
       this._outputTimerInitialized = false;
       return;
     }
 
     let outputNode = this.outputNode;
     let lastVisibleNode = null;
+    let scrollNode = outputNode.parentNode;
     let scrolledToBottom = Utils.isOutputScrolledToBottom(outputNode);
-    let scrollBox = outputNode.scrollBoxObject.element;
-
     let hudIdSupportsString = WebConsoleUtils.supportsString(this.hudId);
 
     // Output the current batch of messages.
     let newMessages = new Set();
     let updatedMessages = new Set();
     for (let item of batch) {
       let result = this._outputMessageFromQueue(hudIdSupportsString, item);
       if (result) {
@@ -1984,17 +1964,17 @@ WebConsoleFrame.prototype = {
     }
 
     let oldScrollHeight = 0;
 
     // Prune messages if needed. We do not do this for every flush call to
     // improve performance.
     let removedNodes = 0;
     if (shouldPrune || !this._outputQueue.length) {
-      oldScrollHeight = scrollBox.scrollHeight;
+      oldScrollHeight = scrollNode.scrollHeight;
 
       let categories = Object.keys(this._pruneCategoriesQueue);
       categories.forEach(function _pruneOutput(aCategory) {
         removedNodes += this.pruneOutputIfNecessary(aCategory);
       }, this);
       this._pruneCategoriesQueue = {};
     }
 
@@ -2004,36 +1984,43 @@ WebConsoleFrame.prototype = {
 
     // Scroll to the new node if it is not filtered, and if the output node is
     // scrolled at the bottom or if the new node is a jsterm input/output
     // message.
     if (lastVisibleNode && (scrolledToBottom || isInputOutput)) {
       Utils.scrollToVisible(lastVisibleNode);
     }
     else if (!scrolledToBottom && removedNodes > 0 &&
-             oldScrollHeight != scrollBox.scrollHeight) {
+             oldScrollHeight != scrollNode.scrollHeight) {
       // If there were pruned messages and if scroll is not at the bottom, then
       // we need to adjust the scroll location.
-      scrollBox.scrollTop -= oldScrollHeight - scrollBox.scrollHeight;
+      scrollNode.scrollTop -= oldScrollHeight - scrollNode.scrollHeight;
     }
 
     if (newMessages.size) {
       this.emit("messages-added", newMessages);
     }
     if (updatedMessages.size) {
       this.emit("messages-updated", updatedMessages);
     }
 
     // If the queue is not empty, schedule another flush.
     if (this._outputQueue.length > 0) {
       this._initOutputTimer();
     }
     else {
       this._outputTimerInitialized = false;
-      this._flushCallback && this._flushCallback();
+      if (this._flushCallback) {
+        try {
+          this._flushCallback();
+        }
+        catch (ex) {
+          console.error(ex);
+        }
+      }
     }
 
     this._lastOutputFlush = Date.now();
   },
 
   /**
    * Initialize the output timer.
    * @private
@@ -2286,62 +2273,55 @@ WebConsoleFrame.prototype = {
    *        copied. If omitted, defaults to the body text. If `aBody` is not
    *        a string, then the clipboard text must be supplied.
    * @param number aLevel [optional]
    *        The level of the console API message.
    * @param number aTimeStamp [optional]
    *        The timestamp to use for this message node. If omitted, the current
    *        date and time is used.
    * @return nsIDOMNode
-   *         The message node: a XUL richlistitem ready to be inserted into
-   *         the Web Console output node.
+   *         The message node: a DIV ready to be inserted into the Web Console
+   *         output node.
    */
   createMessageNode:
   function WCF_createMessageNode(aCategory, aSeverity, aBody, aSourceURL,
                                  aSourceLine, aClipboardText, aLevel, aTimeStamp)
   {
     if (typeof aBody != "string" && aClipboardText == null && aBody.innerText) {
       aClipboardText = aBody.innerText;
     }
 
     // Make the icon container, which is a vertical box. Its purpose is to
     // ensure that the icon stays anchored at the top of the message even for
     // long multi-line messages.
-    let iconContainer = this.document.createElementNS(XUL_NS, "vbox");
-    iconContainer.classList.add("webconsole-msg-icon-container");
-    // Apply the curent group by indenting appropriately.
-    iconContainer.style.marginLeft = this.groupDepth * GROUP_INDENT + "px";
-
-    // Make the icon node. It's sprited and the actual region of the image is
-    // determined by CSS rules.
-    let iconNode = this.document.createElementNS(XUL_NS, "image");
-    iconNode.classList.add("webconsole-msg-icon");
-    iconContainer.appendChild(iconNode);
-
-    // Make the spacer that positions the icon.
-    let spacer = this.document.createElementNS(XUL_NS, "spacer");
-    spacer.flex = 1;
-    iconContainer.appendChild(spacer);
+    let iconContainer = this.document.createElementNS(XHTML_NS, "span");
+    iconContainer.className = "webconsole-msg-icon";
 
     // Create the message body, which contains the actual text of the message.
-    let bodyNode = this.document.createElementNS(XUL_NS, "description");
-    bodyNode.flex = 1;
+    let bodyNode = this.document.createElementNS(XHTML_NS, "span");
     bodyNode.classList.add("webconsole-msg-body");
     bodyNode.classList.add("devtools-monospace");
 
     // Store the body text, since it is needed later for the variables view.
     let body = aBody;
     // If a string was supplied for the body, turn it into a DOM node and an
     // associated clipboard string now.
     aClipboardText = aClipboardText ||
                      (aBody + (aSourceURL ? " @ " + aSourceURL : "") +
                               (aSourceLine ? ":" + aSourceLine : ""));
 
+    let timestamp = aTimeStamp || Date.now();
+
     // Create the containing node and append all its elements to it.
-    let node = this.document.createElementNS(XUL_NS, "richlistitem");
+    let node = this.document.createElementNS(XHTML_NS, "div");
+    node.id = "console-msg-" + gSequenceId();
+    node.className = "hud-msg-node";
+    node.clipboardText = aClipboardText;
+    node.timestamp = timestamp;
+    this.setMessageType(node, aCategory, aSeverity);
 
     if (aBody instanceof Ci.nsIDOMNode) {
       bodyNode.appendChild(aBody);
     }
     else {
       let str = undefined;
       if (aLevel == "dir") {
         str = VariablesView.getString(aBody.arguments[0]);
@@ -2355,86 +2335,76 @@ WebConsoleFrame.prototype = {
       }
 
       if (str !== undefined) {
         aBody = this.document.createTextNode(str);
         bodyNode.appendChild(aBody);
       }
     }
 
-    let repeatContainer = this.document.createElementNS(XUL_NS, "hbox");
-    repeatContainer.setAttribute("align", "start");
-    let repeatNode = this.document.createElementNS(XUL_NS, "label");
-    repeatNode.setAttribute("value", "1");
-    repeatNode.classList.add("webconsole-msg-repeat");
-    repeatNode._uid = [bodyNode.textContent, aCategory, aSeverity, aLevel,
-                       aSourceURL, aSourceLine].join(":");
-    repeatContainer.appendChild(repeatNode);
+    // Add the message repeats node only when needed.
+    let repeatNode = null;
+    if (aCategory != CATEGORY_INPUT && aCategory != CATEGORY_OUTPUT &&
+        aCategory != CATEGORY_NETWORK) {
+      repeatNode = this.document.createElementNS(XHTML_NS, "span");
+      repeatNode.setAttribute("value", "1");
+      repeatNode.classList.add("webconsole-msg-repeat");
+      repeatNode.textContent = 1;
+      repeatNode._uid = [bodyNode.textContent, aCategory, aSeverity, aLevel,
+                         aSourceURL, aSourceLine].join(":");
+    }
 
     // Create the timestamp.
-    let timestampNode = this.document.createElementNS(XUL_NS, "label");
+    let timestampNode = this.document.createElementNS(XHTML_NS, "span");
     timestampNode.classList.add("webconsole-timestamp");
     timestampNode.classList.add("devtools-monospace");
-
-    let timestamp = aTimeStamp || Date.now();
+    // Apply the current group by indenting appropriately.
+    timestampNode.style.marginRight = this.groupDepth * GROUP_INDENT + "px";
+
     let timestampString = l10n.timestampString(timestamp);
-    timestampNode.setAttribute("value", timestampString);
+    timestampNode.textContent = timestampString + " ";
 
     // Create the source location (e.g. www.example.com:6) that sits on the
     // right side of the message, if applicable.
     let locationNode;
     if (aSourceURL && IGNORED_SOURCE_URLS.indexOf(aSourceURL) == -1) {
       locationNode = this.createLocationNode(aSourceURL, aSourceLine);
     }
 
-    node.clipboardText = aClipboardText;
-    node.classList.add("hud-msg-node");
-
-    node.timestamp = timestamp;
-    this.setMessageType(node, aCategory, aSeverity);
-
     node.appendChild(timestampNode);
     node.appendChild(iconContainer);
 
     // Display the variables view after the message node.
     if (aLevel == "dir") {
-      let viewContainer = this.document.createElement("hbox");
-      viewContainer.flex = 1;
-      viewContainer.height = this.outputNode.clientHeight *
-                             CONSOLE_DIR_VIEW_HEIGHT;
+      bodyNode.style.height = (this.window.innerHeight *
+                               CONSOLE_DIR_VIEW_HEIGHT) + "px";
 
       let options = {
         objectActor: body.arguments[0],
-        targetElement: viewContainer,
+        targetElement: bodyNode,
         hideFilterInput: true,
       };
       this.jsterm.openVariablesView(options).then((aView) => {
         node._variablesView = aView;
         if (node.classList.contains("hidden-message")) {
           node.classList.remove("hidden-message");
         }
       });
 
-      let bodyContainer = this.document.createElement("vbox");
-      bodyContainer.flex = 1;
-      bodyContainer.appendChild(bodyNode);
-      bodyContainer.appendChild(viewContainer);
-      node.appendChild(bodyContainer);
       node.classList.add("webconsole-msg-inspector");
     }
-    else {
-      node.appendChild(bodyNode);
-    }
-    node.appendChild(repeatContainer);
+
+    node.appendChild(bodyNode);
+    if (repeatNode) {
+      node.appendChild(repeatNode);
+    }
     if (locationNode) {
       node.appendChild(locationNode);
     }
 
-    node.setAttribute("id", "console-msg-" + gSequenceId());
-
     return node;
   },
 
   /**
    * Make the message body for console.log() calls.
    *
    * @private
    * @param nsIDOMElement aMessage
@@ -2447,17 +2417,17 @@ WebConsoleFrame.prototype = {
    *        the call information that we need to display - mainly the arguments
    *        array of the given API call.
    */
   _makeConsoleLogMessageBody:
   function WCF__makeConsoleLogMessageBody(aMessage, aContainer, aBody)
   {
     Object.defineProperty(aMessage, "_panelOpen", {
       get: function() {
-        let nodes = aContainer.querySelectorAll(".hud-clickable");
+        let nodes = aContainer.getElementsByTagName("a");
         return Array.prototype.some.call(nodes, function(aNode) {
           return aNode._panelOpen;
         });
       },
       enumerable: true,
       configurable: false
     });
 
@@ -2468,36 +2438,34 @@ WebConsoleFrame.prototype = {
 
       let text = VariablesView.getString(aItem);
       let inspectable = !VariablesView.isPrimitive({ value: aItem });
 
       if (aItem && typeof aItem != "object" || !inspectable) {
         aContainer.appendChild(this.document.createTextNode(text));
 
         if (aItem.type && aItem.type == "longString") {
-          let ellipsis = this.document.createElement("description");
-          ellipsis.classList.add("hud-clickable");
+          let ellipsis = this.document.createElementNS(XHTML_NS, "a");
           ellipsis.classList.add("longStringEllipsis");
           ellipsis.textContent = l10n.getStr("longStringEllipsis");
 
           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.createElement("description");
-      elem.classList.add("hud-clickable");
+      let elem = this.document.createElementNS(XHTML_NS, "a");
       elem.setAttribute("aria-haspopup", "true");
-      elem.appendChild(this.document.createTextNode(text));
+      elem.textContent = text;
 
       this._addMessageLinkCallback(elem,
         this._consoleLogClick.bind(this, elem, aItem));
 
       aContainer.appendChild(elem);
     }, this);
   },
 
@@ -2551,30 +2519,30 @@ WebConsoleFrame.prototype = {
 
         if (toIndex != longString.length) {
           this.logWarningAboutStringTooLong();
         }
       }.bind(this));
   },
 
   /**
-   * Creates the XUL label that displays the textual location of an incoming
+   * Creates the anchor that displays the textual location of an incoming
    * message.
    *
    * @param string aSourceURL
    *        The URL of the source file responsible for the error.
    * @param number aSourceLine [optional]
    *        The line number on which the error occurred. If zero or omitted,
    *        there is no line number associated with this message.
    * @return nsIDOMNode
-   *         The new XUL label node, ready to be added to the message node.
+   *         The new anchor element, ready to be added to the message node.
    */
   createLocationNode: function WCF_createLocationNode(aSourceURL, aSourceLine)
   {
-    let locationNode = this.document.createElementNS(XUL_NS, "label");
+    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;
 
     if (/^Scratchpad\/\d+$/.test(aSourceURL)) {
       displayLocation = aSourceURL;
@@ -2585,24 +2553,19 @@ WebConsoleFrame.prototype = {
       displayLocation = WebConsoleUtils.abbreviateSourceURL(fullURL);
     }
 
     if (aSourceLine) {
       displayLocation += ":" + aSourceLine;
       locationNode.sourceLine = aSourceLine;
     }
 
-    locationNode.setAttribute("value", displayLocation);
-
-    // Style appropriately.
-    locationNode.setAttribute("crop", "center");
+    locationNode.textContent = " " + displayLocation;
     locationNode.setAttribute("title", aSourceURL);
-    locationNode.setAttribute("tooltiptext", aSourceURL);
     locationNode.classList.add("webconsole-location");
-    locationNode.classList.add("text-link");
     locationNode.classList.add("devtools-monospace");
 
     // Make the location clickable.
     locationNode.addEventListener("click", () => {
       if (/^Scratchpad\/\d+$/.test(aSourceURL)) {
         let wins = Services.wm.getEnumerator("devtools:scratchpad");
 
         while (wins.hasMoreElements()) {
@@ -2724,29 +2687,27 @@ WebConsoleFrame.prototype = {
    *
    * @param object aOptions
    *        - linkOnly:
    *        An optional flag to copy only URL without timestamp and
    *        other meta-information. Default is false.
    */
   copySelectedItems: function WCF_copySelectedItems(aOptions)
   {
-    aOptions = aOptions || { linkOnly: false };
+    aOptions = aOptions || { linkOnly: false, contextmenu: false };
 
     // Gather up the selected items and concatenate their clipboard text.
     let strings = [];
 
-    let children = this.outputNode.children;
-
-    for (let i = 0; i < children.length; i++) {
-      let item = children[i];
-      if (!item.selected) {
-        continue;
-      }
-
+    let children = this.output.getSelectedMessages();
+    if (!children.length && aOptions.contextmenu) {
+      children = [this._contextMenuHandler.lastClickedMessage];
+    }
+
+    for (let item of children) {
       // Ensure the selected item hasn't been filtered by type or string.
       if (!item.classList.contains("hud-filtered-by-type") &&
           !item.classList.contains("hud-filtered-by-string")) {
         let timestampString = l10n.timestampString(item.timestamp);
         if (aOptions.linkOnly) {
           strings.push(item.url);
         }
         else {
@@ -2795,17 +2756,18 @@ WebConsoleFrame.prototype = {
     }
   },
 
   /**
    * Open the selected item's URL in a new tab.
    */
   openSelectedItemInTab: function WCF_openSelectedItemInTab()
   {
-    let item = this.outputNode.selectedItem;
+    let item = this.output.getSelectedMessages(1)[0] ||
+               this._contextMenuHandler.lastClickedMessage;
 
     if (!item || !item.url) {
       return;
     }
 
     this.owner.openLink(item.url);
   },
 
@@ -3131,58 +3093,62 @@ JSTerm.prototype = {
     // Hide undefined results coming from JSTerm helper functions.
     if (!errorMessage && result && typeof result == "object" &&
         result.type == "undefined" &&
         helperResult && !helperHasRawOutput) {
       aCallback && aCallback();
       return;
     }
 
-    if (aCallback) {
-      let oldFlushCallback = this.hud._flushCallback;
-      this.hud._flushCallback = function() {
-        aCallback();
-        oldFlushCallback && oldFlushCallback();
-        this.hud._flushCallback = oldFlushCallback;
-      }.bind(this);
-    }
-
     let node;
 
     if (errorMessage) {
       node = this.writeOutput(errorMessage, CATEGORY_OUTPUT, SEVERITY_ERROR,
                               aAfterNode, aResponse.timestamp);
     }
     else if (inspectable) {
       node = this.writeOutputJS(resultString,
                                 this._evalOutputClick.bind(this, aResponse),
                                 aAfterNode, aResponse.timestamp);
     }
     else {
       node = this.writeOutput(resultString, CATEGORY_OUTPUT, SEVERITY_LOG,
                               aAfterNode, aResponse.timestamp);
     }
 
+    if (aCallback) {
+      let oldFlushCallback = this.hud._flushCallback;
+      this.hud._flushCallback = () => {
+        aCallback(node);
+        if (oldFlushCallback) {
+          oldFlushCallback();
+          this.hud._flushCallback = oldFlushCallback;
+        }
+        else {
+          this.hud._flushCallback = null;
+        }
+      };
+    }
+
     node._objectActors = new Set();
 
     let error = aResponse.exception;
     if (WebConsoleUtils.isActorGrip(error)) {
       node._objectActors.add(error.actor);
     }
 
     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 ellipsis = this.hud.document.createElement("description");
-        ellipsis.classList.add("hud-clickable");
+        let ellipsis = this.hud.document.createElementNS(XHTML_NS, "a");
         ellipsis.classList.add("longStringEllipsis");
         ellipsis.textContent = l10n.getStr("longStringEllipsis");
 
         let formatter = function(s) '"' + s + '"';
         let onclick = this.hud._longStringClick.bind(this.hud, node, result,
                                                     formatter);
         this.hud._addMessageLinkCallback(ellipsis, onclick);
 
@@ -3347,17 +3313,17 @@ JSTerm.prototype = {
       return view;
     };
 
     let openPromise;
     if (aOptions.targetElement) {
       let deferred = promise.defer();
       openPromise = deferred.promise;
       let document = aOptions.targetElement.ownerDocument;
-      let iframe = document.createElement("iframe");
+      let iframe = document.createElementNS(XHTML_NS, "iframe");
 
       iframe.addEventListener("load", function onIframeLoad(aEvent) {
         iframe.removeEventListener("load", onIframeLoad, true);
         deferred.resolve(iframe.contentWindow);
       }, true);
 
       iframe.flex = 1;
       iframe.setAttribute("src", VARIABLES_VIEW_URL);
@@ -3772,17 +3738,17 @@ JSTerm.prototype = {
 
   /**
    * Remove all of the private messages from the Web Console output.
    *
    * This method emits the "private-messages-cleared" notification.
    */
   clearPrivateMessages: function JST_clearPrivateMessages()
   {
-    let nodes = this.hud.outputNode.querySelectorAll("richlistitem[private]");
+    let nodes = this.hud.outputNode.querySelectorAll(".hud-msg-node[private]");
     for (let node of nodes) {
       this.hud.removeOutputMessage(node);
     }
     this.emit("private-messages-cleared");
   },
 
   /**
    * Updates the size of the input field (command line) to fit its contents.
@@ -4502,62 +4468,42 @@ JSTerm.prototype = {
   },
 };
 
 /**
  * Utils: a collection of globally used functions.
  */
 var Utils = {
   /**
-   * Flag to turn on and off scrolling.
-   */
-  scroll: true,
-
-  /**
-   * Scrolls a node so that it's visible in its containing XUL "scrollbox"
-   * element.
+   * Scrolls a node so that it's visible in its containing element.
    *
    * @param nsIDOMNode aNode
    *        The node to make visible.
    * @returns void
    */
   scrollToVisible: function Utils_scrollToVisible(aNode)
   {
-    if (!this.scroll) {
-      return;
-    }
-
-    // Find the enclosing richlistbox node.
-    let richListBoxNode = aNode.parentNode;
-    while (richListBoxNode.tagName != "richlistbox") {
-      richListBoxNode = richListBoxNode.parentNode;
-    }
-
-    // Use the scroll box object interface to ensure the element is visible.
-    let boxObject = richListBoxNode.scrollBoxObject;
-    let nsIScrollBoxObject = boxObject.QueryInterface(Ci.nsIScrollBoxObject);
-    nsIScrollBoxObject.ensureElementIsVisible(aNode);
+    aNode.scrollIntoView(false);
   },
 
   /**
    * Check if the given output node is scrolled to the bottom.
    *
    * @param nsIDOMNode aOutputNode
    * @return boolean
    *         True if the output node is scrolled to the bottom, or false
    *         otherwise.
    */
   isOutputScrolledToBottom: function Utils_isOutputScrolledToBottom(aOutputNode)
   {
     let lastNodeHeight = aOutputNode.lastChild ?
                          aOutputNode.lastChild.clientHeight : 0;
-    let scrollBox = aOutputNode.scrollBoxObject.element;
-
-    return scrollBox.scrollTop + scrollBox.clientHeight >=
-           scrollBox.scrollHeight - lastNodeHeight / 2;
+    let scrollNode = aOutputNode.parentNode;
+    return scrollNode.scrollTop + scrollNode.clientHeight >=
+           scrollNode.scrollHeight - lastNodeHeight / 2;
   },
 
   /**
    * Determine the category of a given nsIScriptError.
    *
    * @param nsIScriptError aScriptError
    *        The script error you want to determine the category for.
    * @return CATEGORY_JS|CATEGORY_CSS|CATEGORY_SECURITY
@@ -4617,60 +4563,52 @@ var Utils = {
  */
 function CommandController(aWebConsole)
 {
   this.owner = aWebConsole;
 }
 
 CommandController.prototype = {
   /**
-   * Copies the currently-selected entries in the Web Console output to the
-   * clipboard.
-   */
-  copy: function CommandController_copy()
-  {
-    this.owner.copySelectedItems();
-  },
-
-  /**
    * Selects all the text in the HUD output.
    */
   selectAll: function CommandController_selectAll()
   {
-    this.owner.outputNode.selectAll();
+    this.owner.output.selectAllMessages();
   },
 
   /**
    * Open the URL of the selected message in a new tab.
    */
   openURL: function CommandController_openURL()
   {
     this.owner.openSelectedItemInTab();
   },
 
   copyURL: function CommandController_copyURL()
   {
-    this.owner.copySelectedItems({ linkOnly: true });
+    this.owner.copySelectedItems({ linkOnly: true, contextmenu: true });
   },
 
   supportsCommand: function CommandController_supportsCommand(aCommand)
   {
+    if (!this.owner || !this.owner.output) {
+      return false;
+    }
     return this.isCommandEnabled(aCommand);
   },
 
   isCommandEnabled: function CommandController_isCommandEnabled(aCommand)
   {
     switch (aCommand) {
-      case "cmd_copy":
-        // Only enable "copy" if nodes are selected.
-        return this.owner.outputNode.selectedCount > 0;
       case "consoleCmd_openURL":
       case "consoleCmd_copyURL": {
         // Only enable URL-related actions if node is Net Activity.
-        let selectedItem = this.owner.outputNode.selectedItem;
+        let selectedItem = this.owner.output.getSelectedMessages(1)[0] ||
+                           this.owner._contextMenuHandler.lastClickedMessage;
         return selectedItem && "url" in selectedItem;
       }
       case "consoleCmd_clearOutput":
       case "cmd_fontSizeEnlarge":
       case "cmd_fontSizeReduce":
       case "cmd_fontSizeReset":
       case "cmd_selectAll":
       case "cmd_find":
@@ -4679,19 +4617,16 @@ CommandController.prototype = {
         return this.owner.owner._browserConsole;
     }
     return false;
   },
 
   doCommand: function CommandController_doCommand(aCommand)
   {
     switch (aCommand) {
-      case "cmd_copy":
-        this.copy();
-        break;
       case "consoleCmd_openURL":
         this.openURL();
         break;
       case "consoleCmd_copyURL":
         this.copyURL();
         break;
       case "consoleCmd_clearOutput":
         this.owner.jsterm.clearOutput(true);
@@ -5163,47 +5098,53 @@ function ConsoleContextMenu(aOwner)
 {
   this.owner = aOwner;
   this.popup = this.owner.document.getElementById("output-contextmenu");
   this.build = this.build.bind(this);
   this.popup.addEventListener("popupshowing", this.build);
 }
 
 ConsoleContextMenu.prototype = {
+  lastClickedMessage: null,
+
   /*
    * Handle to show/hide context menu item.
    */
   build: function CCM_build(aEvent)
   {
-    let view = this.owner.outputNode;
-    let metadata = this.getSelectionMetadata(view);
-
+    let metadata = this.getSelectionMetadata(aEvent.rangeParent);
     for (let element of this.popup.children) {
       element.hidden = this.shouldHideMenuItem(element, metadata);
     }
   },
 
   /*
    * Get selection information from the view.
    *
-   * @param nsIDOMElement aView
-   *        This should be <xul:richlistbox>.
-   *
+   * @param nsIDOMElement aClickElement
+   *        The DOM element the user clicked on.
    * @return object
    *         Selection metadata.
    */
-  getSelectionMetadata: function CCM_getSelectionMetadata(aView)
+  getSelectionMetadata: function CCM_getSelectionMetadata(aClickElement)
   {
     let metadata = {
       selectionType: "",
       selection: new Set(),
     };
-    let selectedItems = aView.selectedItems;
-
-    metadata.selectionType = (selectedItems > 1) ? "multiple" : "single";
+    let selectedItems = this.owner.output.getSelectedMessages();
+    if (!selectedItems.length) {
+      let clickedItem = this.owner.output.getMessageForElement(aClickElement);
+      if (clickedItem) {
+        this.lastClickedMessage = clickedItem;
+        selectedItems = [clickedItem];
+      }
+    }
+
+    metadata.selectionType = selectedItems.length > 1 ? "multiple" : "single";
 
     let selection = metadata.selection;
     for (let item of selectedItems) {
       switch (item.category) {
         case CATEGORY_NETWORK:
           selection.add("network");
           break;
         case CATEGORY_CSS:
@@ -5257,11 +5198,12 @@ ConsoleContextMenu.prototype = {
   /**
    * Destroy the ConsoleContextMenu object instance.
    */
   destroy: function CCM_destroy()
   {
     this.popup.removeEventListener("popupshowing", this.build);
     this.popup = null;
     this.owner = null;
+    this.lastClickedMessage = null;
   },
 };
 
--- a/browser/devtools/webconsole/webconsole.xul
+++ b/browser/devtools/webconsole/webconsole.xul
@@ -28,17 +28,17 @@ function goUpdateConsoleCommands() {
   goUpdateCommand("consoleCmd_copyURL");
 }
   // ]]></script>
 
   <commandset id="editMenuCommands"/>
 
   <commandset id="consoleCommands"
               commandupdater="true"
-              events="richlistbox-select"
+              events="focus,select"
               oncommandupdate="goUpdateConsoleCommands();">
     <command id="consoleCmd_openURL"
              oncommand="goDoCommand('consoleCmd_openURL');"/>
     <command id="consoleCmd_copyURL"
              oncommand="goDoCommand('consoleCmd_copyURL');"/>
     <command id="consoleCmd_clearOutput"
              oncommand="goDoCommand('consoleCmd_clearOutput');"/>
     <command id="cmd_find" oncommand="goDoCommand('cmd_find');"/>
@@ -58,30 +58,32 @@ function goUpdateConsoleCommands() {
     <key key="&findCmd.key;" command="cmd_find" modifiers="accel"/>
     <key key="&clearOutputCmd.key;" command="consoleCmd_clearOutput" modifiers="accel"/>
     <key key="&closeCmd.key;" command="cmd_close" modifiers="accel"/>
     <key id="key_clearOutput" key="&clearOutputCtrl.key;" command="consoleCmd_clearOutput" modifiers="accel"/>
   </keyset>
   <keyset id="editMenuKeys"/>
 
   <popupset id="mainPopupSet">
-    <menupopup id="output-contextmenu">
+    <menupopup id="output-contextmenu" onpopupshowing="goUpdateGlobalEditMenuItems()">
       <menuitem id="saveBodiesContextMenu" type="checkbox" label="&saveBodies.label;"
                 accesskey="&saveBodies.accesskey;"/>
       <menuitem id="menu_openURL" label="&openURL.label;"
                 accesskey="&openURL.accesskey;" command="consoleCmd_openURL"
                 selection="network" selectionType="single"/>
       <menuitem id="menu_copyURL" label="&copyURLCmd.label;"
                 accesskey="&copyURLCmd.accesskey;" command="consoleCmd_copyURL"
                 selection="network" selectionType="single"/>
       <menuitem id="cMenu_copy"/>
       <menuitem id="cMenu_selectAll"/>
     </menupopup>
   </popupset>
 
+  <tooltip id="aHTMLTooltip" page="true"/>
+
   <box class="hud-outer-wrapper devtools-responsive-container" flex="1">
     <vbox class="hud-console-wrapper" flex="1">
       <toolbar class="hud-console-filter-toolbar devtools-toolbar" mode="full">
         <toolbarbutton label="&btnPageNet.label;" type="menu-button"
                        category="net" class="devtools-toolbarbutton webconsole-filter-button"
                        tooltiptext="&btnPageNet.tooltip;"
                        accesskeyMacOSX="&btnPageNet.accesskeyMacOSX;"
                        accesskey="&btnPageNet.accesskey;"
@@ -158,19 +160,21 @@ function goUpdateConsoleCommands() {
                        tabindex="8"/>
 
         <spacer flex="1"/>
 
         <textbox class="compact hud-filter-box devtools-searchinput" type="search"
                  placeholder="&filterOutput.placeholder;" tabindex="2"/>
       </toolbar>
 
-      <richlistbox class="hud-output-node" orient="vertical" flex="1"
-                   seltype="multiple" context="output-contextmenu"
-                   style="direction:ltr;" tabindex="1"/>
+      <hbox id="output-wrapper" flex="1" context="output-contextmenu" tooltip="aHTMLTooltip">
+        <div xmlns="http://www.w3.org/1999/xhtml" id="output-wrapper2">
+	  <div class="hud-output-node" tabindex="1"/>
+	</div>
+      </hbox>
 
       <hbox class="jsterm-input-container" style="direction:ltr">
         <stack class="jsterm-stack-node" flex="1">
           <textbox class="jsterm-complete-node devtools-monospace"
                    multiline="true" rows="1" tabindex="-1"/>
           <textbox class="jsterm-input-node devtools-monospace"
                    multiline="true" rows="1" tabindex="0"/>
         </stack>
--- a/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
@@ -182,8 +182,13 @@ connectionTimeout=Connection timeout. Ch
 
 # LOCALIZATION NOTE (propertiesFilterPlaceholder): this is the text that
 # appears in the filter text box for the properties view container.
 propertiesFilterPlaceholder=Filter properties
 
 # LOCALIZATION NOTE (emptyPropertiesList): the text that is displayed in the
 # properties pane when there are no properties to display.
 emptyPropertiesList=No properties to display
+
+# LOCALIZATION NOTE (messageRepeats.tooltip): the tooltip text that is displayed
+# when you hover the red bubble that shows how many times a message is repeated
+# in the web console output.
+messageRepeats.tooltip=%S repeats
--- a/browser/themes/shared/devtools/webconsole.inc.css
+++ b/browser/themes/shared/devtools/webconsole.inc.css
@@ -1,105 +1,143 @@
 /* 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/. */
 
 /* General output styles */
 
+a {
+  -moz-user-focus: normal;
+  -moz-user-input: enabled;
+  cursor: pointer;
+  text-decoration: underline;
+}
+
+a:focus {
+  outline: 1px dashed gray;
+}
+
+/* Workaround for Bug 575675 - FindChildWithRules aRelevantLinkVisited
+ * assertion when loading HTML page with links in XUL iframe */
+*:visited { }
+
 .webconsole-timestamp {
+  flex: 0 0 auto;
   color: GrayText;
-  margin-top: 0;
-  margin-bottom: 0;
+  margin: 4px 0;
+  font-size: 0.9em;
 }
 
 .hud-msg-node {
-  list-style-image: url(chrome://browser/skin/devtools/webconsole.png);
-  -moz-image-region: rect(0, 1px, 0, 0);
+  display: flex;
+  -moz-margin-start: 6px;
+  -moz-margin-end: 8px;
+  width: calc(100% - 6px - 8px);
 }
 
 .webconsole-msg-icon {
-  margin: 3px 4px;
+  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;
-  height: 8px;
 }
 
 .hud-clickable {
   cursor: pointer;
   text-decoration: underline;
 }
 
 .webconsole-msg-body {
-  margin-top: 0;
-  margin-bottom: 3px;
-  -moz-margin-start: 3px;
-  -moz-margin-end: 6px;
+  flex: 1 1 100%;
   white-space: pre-wrap;
-}
-
-.webconsole-msg-body-piece {
-  margin: 0;
+  word-wrap: break-word;
 }
 
 .webconsole-msg-url {
-  margin: 0 6px;
+  flex: 1 1 auto;
+  min-width: 5em;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
 
 /* Repeated messages */
 .webconsole-msg-repeat {
-  margin: 2px 0;
-  padding-left: 4px;
-  padding-right: 4px;
+  -moz-user-select: none;
+  flex: 0 0 auto;
+  margin: 2px 6px;
+  padding: 0 6px;
+  height: 1.25em;
   color: white;
   background-color: red;
   border-radius: 40px;
   font: message-box;
   font-size: 0.9em;
   font-weight: 600;
 }
 
 .webconsole-msg-repeat[value="1"] {
   display: none;
 }
 
 .webconsole-location {
-  margin-top: 0;
-  margin-bottom: 0;
-  -moz-margin-start: 0;
-  -moz-margin-end: 6px;
+  -moz-margin-start: 6px;
+  flex: 0 0 auto;
+  align-self: flex-start;
   width: 10em;
   text-align: end;
+  color: -moz-nativehyperlinktext;
+  text-overflow: ellipsis;
+  text-decoration: none;
+  overflow: hidden;
+  white-space: nowrap;
+}
+
+.webconsole-location:hover,
+.webconsole-location:focus {
+  text-decoration: underline;
 }
 
 .webconsole-mixed-content {
   color: #FF0000;
 }
 
-.webconsole-mixed-content-link {
-  color: #0000EE;
-  margin: 0;
-}
-
+.webconsole-mixed-content-link,
 .webconsole-learn-more-link {
   color: #0000EE;
-  margin: 0 0 0 4px;
-}
-
-.hud-msg-node[selected="true"] > .webconsole-timestamp,
-.hud-msg-node[selected="true"] > .webconsole-location {
-  color: inherit;
+  margin: 0 6px;
 }
 
 .jsterm-input-container {
   background: white;
 }
 
+#output-wrapper {
+  background: #fff;
+  color: #000;
+  direction: ltr;
+  border-bottom: 1px solid ThreeDShadow;
+}
+
+#output-wrapper2 {
+  -moz-user-select: text;
+  -moz-box-flex: 1;
+  overflow: auto;
+  /* Set dimensions to show scroll bars when needed. Actual size is changed by
+   * the flexbox. */
+  width: 1px;
+  height: 1px;
+}
+
 .hud-output-node {
-  -moz-appearance: none;
-  border-bottom: 1px solid ThreeDShadow;
-  margin: 0;
+  display: table;
+  table-layout: fixed;
+  width: 100%;
 }
 
 .hud-filtered-by-type,
 .hud-filtered-by-string {
   display: none;
 }
 
 .hidden-message {
@@ -131,95 +169,108 @@
 }
 
 /* Network styles */
 .webconsole-filter-button[category="net"] > .toolbarbutton-menubutton-button:before {
   background-image: linear-gradient(#444444, #000000);
   border-color: #777;
 }
 
-.webconsole-msg-network > .webconsole-msg-icon-container {
+.webconsole-msg-network > .webconsole-msg-icon {
   -moz-border-start: solid #000 6px;
 }
 
-.webconsole-msg-network.webconsole-msg-error {
-  -moz-image-region: rect(0, 16px, 8px, 8px);
+.webconsole-msg-network.webconsole-msg-error > .webconsole-msg-icon {
+  background-image: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 0, 16, 8, 8);
+}
+
+.webconsole-msg-network > .webconsole-msg-body {
+  display: flex;
+}
+
+.webconsole-msg-method {
+  flex: 0 0 auto;
+}
+
+.webconsole-msg-status {
+  flex: 0 0 auto;
+  -moz-margin-start: 6px;
 }
 
 /* CSS styles */
 .webconsole-filter-button[category="css"] > .toolbarbutton-menubutton-button:before {
   background-image: linear-gradient(#2DC3F3, #00B6F0);
   border-color: #1BA2CC;
 }
 
-.webconsole-msg-cssparser > .webconsole-msg-icon-container {
+.webconsole-msg-cssparser > .webconsole-msg-icon {
   -moz-border-start: solid #00b6f0 6px;
 }
 
-.webconsole-msg-cssparser.webconsole-msg-error {
-  -moz-image-region: rect(8px, 16px, 16px, 8px);
+.webconsole-msg-cssparser.webconsole-msg-error > .webconsole-msg-icon {
+  background-image: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 8, 16, 16, 8);
 }
 
-.webconsole-msg-cssparser.webconsole-msg-warn {
-  -moz-image-region: rect(8px, 24px, 16px, 16px);
+.webconsole-msg-cssparser.webconsole-msg-warn > .webconsole-msg-icon {
+  background-image: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 8, 24, 16, 16);
 }
 
 /* JS styles */
 .webconsole-filter-button[category="js"] > .toolbarbutton-menubutton-button:before {
   background-image: linear-gradient(#FCB142, #FB9500);
   border-color: #E98A00;
 }
 
-.webconsole-msg-exception > .webconsole-msg-icon-container {
+.webconsole-msg-exception > .webconsole-msg-icon {
   -moz-border-start: solid #fb9500 6px;
 }
 
-.webconsole-msg-exception.webconsole-msg-error {
-  -moz-image-region: rect(16px, 16px, 24px, 8px);
+.webconsole-msg-exception.webconsole-msg-error > .webconsole-msg-icon {
+  background-image: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 16, 16, 24, 8);
 }
 
-.webconsole-msg-exception.webconsole-msg-warn {
-  -moz-image-region: rect(16px, 24px, 24px, 16px);
+.webconsole-msg-exception.webconsole-msg-warn > .webconsole-msg-icon {
+  background-image: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 16, 24, 24, 16);
 }
 
 /* Web Developer styles */
 .webconsole-filter-button[category="logging"] > .toolbarbutton-menubutton-button:before {
   background-image: linear-gradient(#B9B9B9, #AAAAAA);
   border-color: #929292;
 }
 
-.webconsole-msg-console > .webconsole-msg-icon-container {
+.webconsole-msg-console > .webconsole-msg-icon {
   -moz-border-start: solid #cbcbcb 6px;
 }
 
-.webconsole-msg-console.webconsole-msg-error,
-.webconsole-msg-output.webconsole-msg-error {
-  -moz-image-region: rect(24px, 16px, 32px, 8px);
+.webconsole-msg-console.webconsole-msg-error > .webconsole-msg-icon,
+.webconsole-msg-output.webconsole-msg-error > .webconsole-msg-icon {
+  background-image: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 24, 16, 32, 8);
 }
 
-.webconsole-msg-console.webconsole-msg-warn {
-  -moz-image-region: rect(24px, 24px, 32px, 16px);
+.webconsole-msg-console.webconsole-msg-warn > .webconsole-msg-icon {
+  background-image: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 24, 24, 32, 16);
 }
 
-.webconsole-msg-console.webconsole-msg-info {
-  -moz-image-region: rect(24px, 32px, 32px, 24px);
+.webconsole-msg-console.webconsole-msg-info > .webconsole-msg-icon {
+  background-image: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 24, 32, 32, 24);
 }
 
 /* Input and output styles */
-.webconsole-msg-input > .webconsole-msg-icon-container,
-.webconsole-msg-output > .webconsole-msg-icon-container {
-  border-left: solid #808080 6px;
+.webconsole-msg-input > .webconsole-msg-icon,
+.webconsole-msg-output > .webconsole-msg-icon {
+  -moz-border-start: solid #808080 6px;
 }
 
-.webconsole-msg-input {
-  -moz-image-region: rect(24px, 40px, 32px, 32px);
+.webconsole-msg-input > .webconsole-msg-icon {
+  background-image: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 24, 40, 32, 32);
 }
 
-.webconsole-msg-output {
-  -moz-image-region: rect(24px, 48px, 32px, 40px);
+.webconsole-msg-output > .webconsole-msg-icon {
+  background-image: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 24, 48, 32, 40);
 }
 
 /* JSTerm Styles */
 .jsterm-input-node,
 .jsterm-complete-node {
   border: none;
   padding: 0 0 0 16px;
   -moz-appearance: none;
@@ -234,53 +285,60 @@
           .jsterm-complete-node) > .textbox-input-box > .textbox-textarea {
   overflow-x: hidden;
 }
 
 .jsterm-complete-node > .textbox-input-box > .textbox-textarea {
   color: GrayText;
 }
 
+.webconsole-msg-inspector .webconsole-msg-body {
+  display: flex;
+  flex-direction: column;
+}
 .webconsole-msg-inspector iframe {
-  height: 7em;
+  display: block;
+  flex: 1;
   margin-bottom: 15px;
   -moz-margin-end: 15px;
+  border: 1px solid #ccc;
   border-radius: 4px;
   box-shadow: 0 0 12px #dfdfdf;
 }
 
 #webconsole-sidebar > tabs {
   height: 0;
   border: none;
 }
 
 /* Security styles */
 
-.webconsole-msg-security > .webconsole-msg-icon-container {
+.webconsole-msg-security > .webconsole-msg-icon {
   -moz-border-start: solid red 6px;
 }
 
 .webconsole-filter-button[category="security"] > .toolbarbutton-menubutton-button:before {
   background-image: linear-gradient(#FF3030, #FF7D7D);
   border-color: #D12C2C;
 }
 
-.webconsole-msg-security.webconsole-msg-error {
-  -moz-image-region: rect(32px, 16px, 40px, 8px);
+.webconsole-msg-security.webconsole-msg-error > .webconsole-msg-icon {
+  background-image: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 32, 16, 40, 8);
 }
 
-.webconsole-msg-security.webconsole-msg-warn {
-  -moz-image-region: rect(32px, 24px, 40px, 16px);
+.webconsole-msg-security.webconsole-msg-warn > .webconsole-msg-icon {
+  background-image: -moz-image-rect(url(chrome://browser/skin/devtools/webconsole.png), 32, 24, 40, 16);
 }
 
 .navigation-marker {
   color: #aaa;
   background: linear-gradient(#fff, #bbb, #fff) no-repeat left 50%;
   background-size: 100% 2px;
-  -moz-margin-start: 3px;
-  -moz-margin-end: 6px;
+  margin-top: 6px;
+  margin-bottom: 6px;
   font-size: 0.9em;
 }
 
 .navigation-marker .url {
   background: #fff;
-  -moz-padding-end: 6px;
+  -moz-padding-end: 9px;
+  text-decoration: none;
 }
--- a/toolkit/devtools/webconsole/client.js
+++ b/toolkit/devtools/webconsole/client.js
@@ -143,18 +143,18 @@ WebConsoleClient.prototype = {
       type: "clearMessagesCache",
     };
     this._client.request(packet);
   },
 
   /**
    * Get Web Console-related preferences on the server.
    *
-   * @param object aPreferences
-   *        An object with the preferences you want to retrieve.
+   * @param array aPreferences
+   *        An array with the preferences you want to retrieve.
    * @param function [aOnResponse]
    *        Optional function to invoke when the response is received.
    */
   getPreferences: function WCC_getPreferences(aPreferences, aOnResponse)
   {
     let packet = {
       to: this._actor,
       type: "getPreferences",
--- a/toolkit/devtools/webconsole/network-helper.js
+++ b/toolkit/devtools/webconsole/network-helper.js
@@ -47,17 +47,17 @@
  *  Mike Ratcliffe (Comartis AG)
  *  Hernan Rodríguez Colmeiro
  *  Austin Andrews
  *  Christoph Dorn
  *  Steven Roussey (AppCenter Inc, Network54)
  *  Mihai Sucan (Mozilla Corp.)
  */
 
-const {Cc, Ci, Cu} = require("chrome");
+const {components, Cc, Ci, Cu} = require("chrome");
 loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
 
 /**
  * Helper object for networking stuff.
  *
  * Most of the following functions have been taken from the Firebug source. They
  * have been modified to match the Firefox coding rules.
  */
@@ -239,17 +239,17 @@ let NetworkHelper = {
     let channel = NetUtil.newChannel(aUrl);
 
     // Ensure that we only read from the cache and not the server.
     channel.loadFlags = Ci.nsIRequest.LOAD_FROM_CACHE |
       Ci.nsICachingChannel.LOAD_ONLY_FROM_CACHE |
       Ci.nsICachingChannel.LOAD_BYPASS_LOCAL_CACHE_IF_BUSY;
 
     NetUtil.asyncFetch(channel, function (aInputStream, aStatusCode, aRequest) {
-      if (!Components.isSuccessCode(aStatusCode)) {
+      if (!components.isSuccessCode(aStatusCode)) {
         aCallback(null);
         return;
       }
 
       // Try to get the encoding from the channel. If there is none, then use
       // the passed assumed aCharset.
       let aChannel = aRequest.QueryInterface(Ci.nsIChannel);
       let contentCharset = aChannel.contentCharset || aCharset;
--- a/toolkit/devtools/webconsole/utils.js
+++ b/toolkit/devtools/webconsole/utils.js
@@ -169,22 +169,35 @@ let WebConsoleUtils = {
    *
    * @param string aSourceURL
    *        The source URL to shorten.
    * @return string
    *         The abbreviated form of the source URL.
    */
   abbreviateSourceURL: function WCU_abbreviateSourceURL(aSourceURL)
   {
+    if (aSourceURL.substr(0, 5) == "data:") {
+      let commaIndex = aSourceURL.indexOf(",");
+      if (commaIndex > -1) {
+        aSourceURL = "data:" + aSourceURL.substring(commaIndex + 1);
+      }
+    }
+
     // Remove any query parameters.
     let hookIndex = aSourceURL.indexOf("?");
     if (hookIndex > -1) {
       aSourceURL = aSourceURL.substring(0, hookIndex);
     }
 
+    // Remove any hash fragments.
+    let hashIndex = aSourceURL.indexOf("#");
+    if (hashIndex > -1) {
+      aSourceURL = aSourceURL.substring(0, hashIndex);
+    }
+
     // Remove a trailing "/".
     if (aSourceURL[aSourceURL.length - 1] == "/") {
       aSourceURL = aSourceURL.substring(0, aSourceURL.length - 1);
     }
 
     // Remove all but the last path component.
     let slashIndex = aSourceURL.lastIndexOf("/");
     if (slashIndex > -1) {