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("

Test copy OuterHTML

", 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 + + + +

Test copy OuterHTML

+ + 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")