diff --git a/browser/devtools/inspector/inspector-panel.js b/browser/devtools/inspector/inspector-panel.js
index 5c0f583c3298..b8ca39838776 100644
--- a/browser/devtools/inspector/inspector-panel.js
+++ b/browser/devtools/inspector/inspector-panel.js
@@ -1047,7 +1047,7 @@ InspectorPanel.prototype = {
if (!this.selection.isNode()) {
return;
}
- this._copyLongStr(this.walker.innerHTML(this.selection.nodeFront));
+ this._copyLongString(this.walker.innerHTML(this.selection.nodeFront));
},
/**
@@ -1057,8 +1057,21 @@ InspectorPanel.prototype = {
if (!this.selection.isNode()) {
return;
}
+ let node = this.selection.nodeFront;
- this._copyLongStr(this.walker.outerHTML(this.selection.nodeFront));
+ switch (node.nodeType) {
+ case Ci.nsIDOMNode.ELEMENT_NODE :
+ this._copyLongString(this.walker.outerHTML(node));
+ break;
+ case Ci.nsIDOMNode.COMMENT_NODE :
+ this._getLongString(node.getNodeValue()).then(comment => {
+ clipboardHelper.copyString("");
+ });
+ break;
+ case Ci.nsIDOMNode.DOCUMENT_TYPE_NODE :
+ clipboardHelper.copyString(node.doctypeString);
+ break;
+ }
},
/**
@@ -1071,13 +1084,29 @@ InspectorPanel.prototype = {
}
},
- _copyLongStr: function(promise) {
- return promise.then(longstr => {
- return longstr.string().then(toCopy => {
- longstr.release().then(null, console.error);
- clipboardHelper.copyString(toCopy);
+ /**
+ * Copy the content of a longString (via a promise resolving a LongStringActor) to the clipboard
+ * @param {Promise} longStringActorPromise promise expected to resolve a LongStringActor instance
+ * @return {Promise} promise resolving (with no argument) when the string is sent to the clipboard
+ */
+ _copyLongString: function(longStringActorPromise) {
+ return this._getLongString(longStringActorPromise).then(string => {
+ clipboardHelper.copyString(string);
+ }).catch(Cu.reportError);
+ },
+
+ /**
+ * Retrieve the content of a longString (via a promise resolving a LongStringActor)
+ * @param {Promise} longStringActorPromise promise expected to resolve a LongStringActor instance
+ * @return {Promise} promise resolving with the retrieved string as argument
+ */
+ _getLongString: function(longStringActorPromise) {
+ return longStringActorPromise.then(longStringActor => {
+ return longStringActor.string().then(string => {
+ longStringActor.release().catch(Cu.reportError);
+ return string;
});
- }).then(null, console.error);
+ }).catch(Cu.reportError);
},
/**
diff --git a/browser/devtools/inspector/test/browser.ini b/browser/devtools/inspector/test/browser.ini
index 02a2e150fde9..304276c3c67e 100644
--- a/browser/devtools/inspector/test/browser.ini
+++ b/browser/devtools/inspector/test/browser.ini
@@ -21,6 +21,7 @@ support-files =
doc_inspector_infobar_01.html
doc_inspector_infobar_02.html
doc_inspector_menu.html
+ doc_inspector_outerhtml.html
doc_inspector_remove-iframe-during-load.html
doc_inspector_search.html
doc_inspector_search-reserved.html
@@ -76,6 +77,7 @@ skip-if = e10s # GCLI isn't e10s compatible. See bug 1128988.
[browser_inspector_initialization.js]
[browser_inspector_inspect-object-element.js]
[browser_inspector_invalidate.js]
+[browser_inspector_keyboard-shortcuts-copy-outerhtml.js]
[browser_inspector_keyboard-shortcuts.js]
[browser_inspector_menu-01-sensitivity.js]
[browser_inspector_menu-02-copy-items.js]
diff --git a/browser/devtools/inspector/test/browser_inspector_keyboard-shortcuts-copy-outerhtml.js b/browser/devtools/inspector/test/browser_inspector_keyboard-shortcuts-copy-outerhtml.js
new file mode 100644
index 000000000000..e1a6c0266bd0
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_keyboard-shortcuts-copy-outerhtml.js
@@ -0,0 +1,58 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test copy outer HTML from the keyboard/copy event
+
+const TEST_URL = TEST_URL_ROOT + "doc_inspector_outerhtml.html";
+
+add_task(function *() {
+ let { inspector } = yield openInspectorForURL(TEST_URL);
+ let root = inspector.markup._elt;
+
+ info("Test copy outerHTML for COMMENT node");
+ let comment = getElementByType(inspector, Ci.nsIDOMNode.COMMENT_NODE);
+ yield setSelectionNodeFront(comment, inspector);
+ yield checkClipboard("", root);
+
+ info("Test copy outerHTML for DOCTYPE node");
+ let doctype = getElementByType(inspector, Ci.nsIDOMNode.DOCUMENT_TYPE_NODE);
+ yield setSelectionNodeFront(doctype, inspector);
+ yield checkClipboard("", root);
+
+ info("Test copy outerHTML for ELEMENT node");
+ yield selectAndHighlightNode("div", inspector);
+ yield checkClipboard("
", root);
+});
+
+function* setSelectionNodeFront(node, inspector) {
+ let updated = inspector.once("inspector-updated");
+ inspector.selection.setNodeFront(node);
+ yield updated;
+}
+
+function* checkClipboard(expectedText, node) {
+ let deferred = promise.defer();
+ waitForClipboard(
+ expectedText,
+ () => fireCopyEvent(node),
+ deferred.resolve,
+ deferred.reject
+ );
+
+ try {
+ yield deferred.promise;
+ ok(true, "Clipboard successfully filled with : " + expectedText);
+ } catch (e) {
+ ok(false, "Clipboard could not be filled with the expected text : " + expectedText);
+ }
+}
+
+function getElementByType(inspector, type) {
+ for (let [node] of inspector.markup._containers) {
+ if (node.nodeType === type) {
+ return node;
+ }
+ }
+}
diff --git a/browser/devtools/inspector/test/doc_inspector_outerhtml.html b/browser/devtools/inspector/test/doc_inspector_outerhtml.html
new file mode 100644
index 000000000000..cc400674d2ff
--- /dev/null
+++ b/browser/devtools/inspector/test/doc_inspector_outerhtml.html
@@ -0,0 +1,11 @@
+
+
+
+
+ Inspector Copy OuterHTML Test
+
+
+
+
+
+
diff --git a/browser/devtools/inspector/test/head.js b/browser/devtools/inspector/test/head.js
index d52b6b8706b8..9bb78db81494 100644
--- a/browser/devtools/inspector/test/head.js
+++ b/browser/devtools/inspector/test/head.js
@@ -723,6 +723,15 @@ function wait(ms) {
return def.promise;
}
+/**
+ * Dispatch the copy event on the given element
+ */
+function fireCopyEvent(element) {
+ let evt = element.ownerDocument.createEvent("Event");
+ evt.initEvent("copy", true, true);
+ element.dispatchEvent(evt);
+}
+
/**
* Send an async message to the frame script (chrome -> content) and wait for a
* response message with the same name (content -> chrome).
diff --git a/browser/devtools/markupview/markup-view.css b/browser/devtools/markupview/markup-view.css
index 475c40e069d1..161eaaa839ab 100644
--- a/browser/devtools/markupview/markup-view.css
+++ b/browser/devtools/markupview/markup-view.css
@@ -6,6 +6,10 @@
-moz-control-character-visibility: visible;
}
+body {
+ -moz-user-select: none;
+}
+
/* Force height and width (possibly overflowing) from inline elements.
* This allows long overflows of text or input fields to still be styled with
* the container, rather than the background disappearing when scrolling */
@@ -16,7 +20,6 @@
body.dragging .tag-line {
cursor: grabbing;
- -moz-user-select: none;
}
#root-wrapper:after {
diff --git a/browser/devtools/markupview/markup-view.js b/browser/devtools/markupview/markup-view.js
index 703cd8cf4cbd..536410feeb8c 100644
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -111,6 +111,9 @@ function MarkupView(aInspector, aFrame, aControllerWindow) {
this._boundKeyDown = this._onKeyDown.bind(this);
this._frame.contentWindow.addEventListener("keydown", this._boundKeyDown, false);
+ this._onCopy = this._onCopy.bind(this);
+ this._frame.contentWindow.addEventListener("copy", this._onCopy);
+
this._boundFocus = this._onFocus.bind(this);
this._frame.addEventListener("focus", this._boundFocus, false);
@@ -507,6 +510,20 @@ MarkupView.prototype = {
return walker;
},
+ _onCopy: function (evt) {
+ // Ignore copy events from editors
+ if (this._isInputOrTextarea(evt.target)) {
+ return;
+ }
+
+ let selection = this._inspector.selection;
+ if (selection.isNode()) {
+ this._inspector.copyOuterHTML();
+ }
+ evt.stopPropagation();
+ evt.preventDefault();
+ },
+
/**
* Key handling.
*/
@@ -514,8 +531,7 @@ MarkupView.prototype = {
let handled = true;
// Ignore keystrokes that originated in editors.
- if (aEvent.target.tagName.toLowerCase() === "input" ||
- aEvent.target.tagName.toLowerCase() === "textarea") {
+ if (this._isInputOrTextarea(aEvent.target)) {
return;
}
@@ -614,6 +630,14 @@ MarkupView.prototype = {
}
},
+ /**
+ * Check if a node is an input or textarea
+ */
+ _isInputOrTextarea : function (element) {
+ let name = element.tagName.toLowerCase();
+ return name === "input" || name === "textarea";
+ },
+
/**
* Delete a node from the DOM.
* This is an undoable action.
@@ -1485,6 +1509,9 @@ MarkupView.prototype = {
this._boundKeyDown, false);
this._boundKeyDown = null;
+ this._frame.contentWindow.removeEventListener("copy", this._onCopy);
+ this._onCopy = null;
+
this._inspector.selection.off("new-node-front", this._boundOnNewSelection);
this._boundOnNewSelection = null;
@@ -2314,10 +2341,7 @@ function GenericEditor(aContainer, aNode) {
this.tag.textContent = aNode.isBeforePseudoElement ? "::before" : "::after";
} else if (aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE) {
this.elt.classList.add("comment");
- this.tag.textContent = '';
+ this.tag.textContent = aNode.doctypeString;
} else {
this.tag.textContent = aNode.nodeName;
}
diff --git a/toolkit/devtools/server/actors/inspector.js b/toolkit/devtools/server/actors/inspector.js
index fc8b264683ac..1ef0736c22f9 100644
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -849,6 +849,12 @@ let NodeFront = protocol.FrontClass(NodeActor, {
get nodeName() {
return this._form.nodeName;
},
+ get doctypeString() {
+ return '';
+ },
get baseURI() {
return this._form.baseURI;
@@ -2399,11 +2405,11 @@ var WalkerActor = protocol.ActorClass({
* @param {NodeActor} node The node.
*/
outerHTML: method(function(node) {
- let html = "";
+ let outerHTML = "";
if (!isNodeDead(node)) {
- html = node.rawNode.outerHTML;
+ outerHTML = node.rawNode.outerHTML;
}
- return LongStringActor(this.conn, html);
+ return LongStringActor(this.conn, outerHTML);
}, {
request: {
node: Arg(0, "domnode")