Bug 968241 - Copy outerHTML using keyboard shortcut. r=pbrosset

Copy outerHTML of the currently selected node of the inspector.
Works for ELEMENT, DOCUMENT_TYPE and COMMENT node types.

- bound "copy" event in markup-view to copy outerHTML
- added doctypeString property to NodeFront in actors/inspector.js
- markup-view.js is also using this property now
- added mochitest with dedicated html
This commit is contained in:
Julian Descottes
2015-07-02 22:43:19 +02:00
parent 80c0a6bf68
commit eb82d20c8b
8 changed files with 160 additions and 18 deletions

View File

@@ -1047,7 +1047,7 @@ InspectorPanel.prototype = {
if (!this.selection.isNode()) { if (!this.selection.isNode()) {
return; 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()) { if (!this.selection.isNode()) {
return; 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("<!--" + comment + "-->");
});
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 => { * Copy the content of a longString (via a promise resolving a LongStringActor) to the clipboard
return longstr.string().then(toCopy => { * @param {Promise} longStringActorPromise promise expected to resolve a LongStringActor instance
longstr.release().then(null, console.error); * @return {Promise} promise resolving (with no argument) when the string is sent to the clipboard
clipboardHelper.copyString(toCopy); */
_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);
}, },
/** /**

View File

@@ -21,6 +21,7 @@ support-files =
doc_inspector_infobar_01.html doc_inspector_infobar_01.html
doc_inspector_infobar_02.html doc_inspector_infobar_02.html
doc_inspector_menu.html doc_inspector_menu.html
doc_inspector_outerhtml.html
doc_inspector_remove-iframe-during-load.html doc_inspector_remove-iframe-during-load.html
doc_inspector_search.html doc_inspector_search.html
doc_inspector_search-reserved.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_initialization.js]
[browser_inspector_inspect-object-element.js] [browser_inspector_inspect-object-element.js]
[browser_inspector_invalidate.js] [browser_inspector_invalidate.js]
[browser_inspector_keyboard-shortcuts-copy-outerhtml.js]
[browser_inspector_keyboard-shortcuts.js] [browser_inspector_keyboard-shortcuts.js]
[browser_inspector_menu-01-sensitivity.js] [browser_inspector_menu-01-sensitivity.js]
[browser_inspector_menu-02-copy-items.js] [browser_inspector_menu-02-copy-items.js]

View File

@@ -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("<!-- Comment -->", root);
info("Test copy outerHTML for DOCTYPE node");
let doctype = getElementByType(inspector, Ci.nsIDOMNode.DOCUMENT_TYPE_NODE);
yield setSelectionNodeFront(doctype, inspector);
yield checkClipboard("<!DOCTYPE html>", root);
info("Test copy outerHTML for ELEMENT node");
yield selectAndHighlightNode("div", inspector);
yield checkClipboard("<div><p>Test copy OuterHTML</p></div>", 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;
}
}
}

View File

@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Inspector Copy OuterHTML Test</title>
</head>
<body>
<!-- Comment -->
<div><p>Test copy OuterHTML</p></div>
</body>
</html>

View File

@@ -723,6 +723,15 @@ function wait(ms) {
return def.promise; 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 * Send an async message to the frame script (chrome -> content) and wait for a
* response message with the same name (content -> chrome). * response message with the same name (content -> chrome).

View File

@@ -6,6 +6,10 @@
-moz-control-character-visibility: visible; -moz-control-character-visibility: visible;
} }
body {
-moz-user-select: none;
}
/* Force height and width (possibly overflowing) from inline elements. /* Force height and width (possibly overflowing) from inline elements.
* This allows long overflows of text or input fields to still be styled with * This allows long overflows of text or input fields to still be styled with
* the container, rather than the background disappearing when scrolling */ * the container, rather than the background disappearing when scrolling */
@@ -16,7 +20,6 @@
body.dragging .tag-line { body.dragging .tag-line {
cursor: grabbing; cursor: grabbing;
-moz-user-select: none;
} }
#root-wrapper:after { #root-wrapper:after {

View File

@@ -111,6 +111,9 @@ function MarkupView(aInspector, aFrame, aControllerWindow) {
this._boundKeyDown = this._onKeyDown.bind(this); this._boundKeyDown = this._onKeyDown.bind(this);
this._frame.contentWindow.addEventListener("keydown", this._boundKeyDown, false); 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._boundFocus = this._onFocus.bind(this);
this._frame.addEventListener("focus", this._boundFocus, false); this._frame.addEventListener("focus", this._boundFocus, false);
@@ -507,6 +510,20 @@ MarkupView.prototype = {
return walker; 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. * Key handling.
*/ */
@@ -514,8 +531,7 @@ MarkupView.prototype = {
let handled = true; let handled = true;
// Ignore keystrokes that originated in editors. // Ignore keystrokes that originated in editors.
if (aEvent.target.tagName.toLowerCase() === "input" || if (this._isInputOrTextarea(aEvent.target)) {
aEvent.target.tagName.toLowerCase() === "textarea") {
return; 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. * Delete a node from the DOM.
* This is an undoable action. * This is an undoable action.
@@ -1485,6 +1509,9 @@ MarkupView.prototype = {
this._boundKeyDown, false); this._boundKeyDown, false);
this._boundKeyDown = null; this._boundKeyDown = null;
this._frame.contentWindow.removeEventListener("copy", this._onCopy);
this._onCopy = null;
this._inspector.selection.off("new-node-front", this._boundOnNewSelection); this._inspector.selection.off("new-node-front", this._boundOnNewSelection);
this._boundOnNewSelection = null; this._boundOnNewSelection = null;
@@ -2314,10 +2341,7 @@ function GenericEditor(aContainer, aNode) {
this.tag.textContent = aNode.isBeforePseudoElement ? "::before" : "::after"; this.tag.textContent = aNode.isBeforePseudoElement ? "::before" : "::after";
} else if (aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE) { } else if (aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE) {
this.elt.classList.add("comment"); this.elt.classList.add("comment");
this.tag.textContent = '<!DOCTYPE ' + aNode.name + this.tag.textContent = aNode.doctypeString;
(aNode.publicId ? ' PUBLIC "' + aNode.publicId + '"': '') +
(aNode.systemId ? ' "' + aNode.systemId + '"' : '') +
'>';
} else { } else {
this.tag.textContent = aNode.nodeName; this.tag.textContent = aNode.nodeName;
} }

View File

@@ -849,6 +849,12 @@ let NodeFront = protocol.FrontClass(NodeActor, {
get nodeName() { get nodeName() {
return this._form.nodeName; return this._form.nodeName;
}, },
get doctypeString() {
return '<!DOCTYPE ' + this._form.name +
(this._form.publicId ? ' PUBLIC "' + this._form.publicId + '"': '') +
(this._form.systemId ? ' "' + this._form.systemId + '"' : '') +
'>';
},
get baseURI() { get baseURI() {
return this._form.baseURI; return this._form.baseURI;
@@ -2399,11 +2405,11 @@ var WalkerActor = protocol.ActorClass({
* @param {NodeActor} node The node. * @param {NodeActor} node The node.
*/ */
outerHTML: method(function(node) { outerHTML: method(function(node) {
let html = ""; let outerHTML = "";
if (!isNodeDead(node)) { if (!isNodeDead(node)) {
html = node.rawNode.outerHTML; outerHTML = node.rawNode.outerHTML;
} }
return LongStringActor(this.conn, html); return LongStringActor(this.conn, outerHTML);
}, { }, {
request: { request: {
node: Arg(0, "domnode") node: Arg(0, "domnode")