author | fayolle-florent@orange.fr <fayolle-florent@orange.fr> |
Sat, 22 Nov 2014 08:48:00 +0100 | |
changeset 241416 | d3032b990c98363c75e3e1c5eba0d82892bc3b24 |
parent 241415 | c3d184a01153220e3ac00406ef09af0d451b6c03 |
child 241417 | a7bd9b15a0715d881f8fa6d727be58839d09cace |
push id | 4311 |
push user | raliiev@mozilla.com |
push date | Mon, 12 Jan 2015 19:37:41 +0000 |
treeherder | mozilla-beta@150c9fed433b [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | pbrosset |
bugs | 1095521 |
milestone | 36.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
|
--- a/browser/devtools/framework/selection.js +++ b/browser/devtools/framework/selection.js @@ -246,17 +246,17 @@ Selection.prototype = { } node = node.parentNode(); }; return false; }, isHTMLNode: function() { let xhtml_ns = "http://www.w3.org/1999/xhtml"; - return this.isNode() && this.node.namespaceURI == xhtml_ns; + return this.isNode() && this.nodeFront.namespaceURI == xhtml_ns; }, // Node type isElementNode: function() { return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.ELEMENT_NODE; }, @@ -295,16 +295,34 @@ Selection.prototype = { isCommentNode: function() { return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.PROCESSING_INSTRUCTION_NODE; }, isDocumentNode: function() { return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE; }, + /** + * @returns true if the selection is the <body> HTML element. + */ + isBodyNode: function() { + return this.isHTMLNode() && + this.isConnected() && + this.nodeFront.nodeName === "BODY"; + }, + + /** + * @returns true if the selection is the <head> HTML element. + */ + isHeadNode: function() { + return this.isHTMLNode() && + this.isConnected() && + this.nodeFront.nodeName === "HEAD"; + }, + isDocumentTypeNode: function() { return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE; }, isDocumentFragmentNode: function() { return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE; },
--- a/browser/devtools/inspector/inspector-panel.js +++ b/browser/devtools/inspector/inspector-panel.js @@ -52,17 +52,16 @@ const LAYOUT_CHANGE_TIMER = 250; * Fired when the rule view updates to a new node */ function InspectorPanel(iframeWindow, toolbox) { this._toolbox = toolbox; this._target = toolbox._target; this.panelDoc = iframeWindow.document; this.panelWin = iframeWindow; this.panelWin.inspector = this; - this._inspector = null; this._onBeforeNavigate = this._onBeforeNavigate.bind(this); this._target.on("will-navigate", this._onBeforeNavigate); EventEmitter.decorate(this); } exports.InspectorPanel = InspectorPanel; @@ -104,16 +103,20 @@ InspectorPanel.prototype = { get hasUrlToImageDataResolver() { return this._target.client.traits.urlToImageDataResolver; }, get canGetUniqueSelector() { return this._target.client.traits.getUniqueSelector; }, + get canPasteInnerOrAdjacentHTML() { + return this._target.client.traits.pasteHTML; + }, + _deferredOpen: function(defaultSelection) { let deferred = promise.defer(); this.onNewRoot = this.onNewRoot.bind(this); this.walker.on("new-root", this.onNewRoot); this.nodemenu = this.panelDoc.getElementById("inspector-node-popup"); this.lastNodemenuItem = this.nodemenu.lastChild; @@ -568,17 +571,17 @@ InspectorPanel.prototype = { hideNodeMenu: function InspectorPanel_hideNodeMenu() { this.nodemenu.hidePopup(); }, /** * Returns the clipboard content if it is appropriate for pasting * into the current node's outer HTML, otherwise returns null. */ - _getClipboardContentForOuterHTML: function Inspector_getClipboardContentForOuterHTML() { + _getClipboardContentForPaste: function Inspector_getClipboardContentForPaste() { let flavors = clipboard.currentFlavors; if (flavors.indexOf("text") != -1 || (flavors.indexOf("html") != -1 && flavors.indexOf("image") == -1)) { let content = clipboard.get(); if (content && content.trim().length > 0) { return content; } } @@ -637,25 +640,44 @@ InspectorPanel.prototype = { // actor has the appropriate trait (isOuterHTMLEditable) let editHTML = this.panelDoc.getElementById("node-menu-edithtml"); if (isEditableElement && this.isOuterHTMLEditable) { editHTML.removeAttribute("disabled"); } else { editHTML.setAttribute("disabled", "true"); } - // Enable the "paste outer HTML" item if the selection is an element and - // the root actor has the appropriate trait (isOuterHTMLEditable) and if - // the clipbard content is appropriate. let pasteOuterHTML = this.panelDoc.getElementById("node-menu-pasteouterhtml"); - if (isEditableElement && this.isOuterHTMLEditable && - this._getClipboardContentForOuterHTML()) { - pasteOuterHTML.removeAttribute("disabled"); + let pasteInnerHTML = this.panelDoc.getElementById("node-menu-pasteinnerhtml"); + let pasteBefore = this.panelDoc.getElementById("node-menu-pastebefore"); + let pasteAfter = this.panelDoc.getElementById("node-menu-pasteafter"); + let pasteFirstChild = this.panelDoc.getElementById("node-menu-pastefirstchild"); + let pasteLastChild = this.panelDoc.getElementById("node-menu-pastelastchild"); + + // Is the clipboard content appropriate? Is the element editable? + if (isEditableElement && this._getClipboardContentForPaste()) { + pasteInnerHTML.disabled = !this.canPasteInnerOrAdjacentHTML; + // Enable the "paste outer HTML" item if the selection is an element and + // the root actor has the appropriate trait (isOuterHTMLEditable). + pasteOuterHTML.disabled = !this.isOuterHTMLEditable; + // Don't paste before / after a root or a BODY or a HEAD element. + pasteBefore.disabled = pasteAfter.disabled = + !this.canPasteInnerOrAdjacentHTML || this.selection.isRoot() || + this.selection.isBodyNode() || this.selection.isHeadNode(); + // Don't paste as a first / last child of a HTML document element. + pasteFirstChild.disabled = pasteLastChild.disabled = + !this.canPasteInnerOrAdjacentHTML || (this.selection.isHTMLNode() && + this.selection.isRoot()); } else { - pasteOuterHTML.setAttribute("disabled", "true"); + pasteOuterHTML.disabled = true; + pasteInnerHTML.disabled = true; + pasteBefore.disabled = true; + pasteAfter.disabled = true; + pasteFirstChild.disabled = true; + pasteLastChild.disabled = true; } // Enable the "copy image data-uri" item if the selection is previewable // which essentially checks if it's an image or canvas tag let copyImageData = this.panelDoc.getElementById("node-menu-copyimagedatauri"); let markupContainer = this.markup.getContainer(this.selection.nodeFront); if (isSelectionElement && markupContainer && markupContainer.isPreviewable()) { copyImageData.removeAttribute("disabled"); @@ -685,17 +707,17 @@ InspectorPanel.prototype = { // This is needed to enable tooltips inside the iframe document. this._boundMarkupFrameLoad = this._onMarkupFrameLoad.bind(this); this._markupFrame.addEventListener("load", this._boundMarkupFrameLoad, true); this._markupBox.setAttribute("collapsed", true); this._markupBox.appendChild(this._markupFrame); this._markupFrame.setAttribute("src", "chrome://browser/content/devtools/markup-view.xhtml"); - this._markupFrame.setAttribute("aria-label", this.strings.GetStringFromName("inspector.panelLabel.markupView")) + this._markupFrame.setAttribute("aria-label", this.strings.GetStringFromName("inspector.panelLabel.markupView")); }, _onMarkupFrameLoad: function InspectorPanel__onMarkupFrameLoad() { this._markupFrame.removeEventListener("load", this._boundMarkupFrameLoad, true); delete this._boundMarkupFrameLoad; this._markupFrame.contentWindow.focus(); @@ -768,45 +790,71 @@ InspectorPanel.prototype = { return; } return this.walker.clearPseudoClassLocks().then(null, console.error); }, /** * Edit the outerHTML of the selected Node. */ - editHTML: function InspectorPanel_editHTML() - { + editHTML: function InspectorPanel_editHTML() { if (!this.selection.isNode()) { return; } if (this.markup) { this.markup.beginEditingOuterHTML(this.selection.nodeFront); } }, /** * Paste the contents of the clipboard into the selected Node's outer HTML. */ - pasteOuterHTML: function InspectorPanel_pasteOuterHTML() - { - let content = this._getClipboardContentForOuterHTML(); - if (content) { - let node = this.selection.nodeFront; - this.markup.getNodeOuterHTML(node).then((oldContent) => { - this.markup.updateNodeOuterHTML(node, content, oldContent); - }); - } + pasteOuterHTML: function InspectorPanel_pasteOuterHTML() { + let content = this._getClipboardContentForPaste(); + if (!content) + return promise.reject("No clipboard content for paste"); + + let node = this.selection.nodeFront; + return this.markup.getNodeOuterHTML(node).then(oldContent => { + this.markup.updateNodeOuterHTML(node, content, oldContent); + }); + }, + + /** + * Paste the contents of the clipboard into the selected Node's inner HTML. + */ + pasteInnerHTML: function InspectorPanel_pasteInnerHTML() { + let content = this._getClipboardContentForPaste(); + if (!content) + return promise.reject("No clipboard content for paste"); + + let node = this.selection.nodeFront; + return this.markup.getNodeInnerHTML(node).then(oldContent => { + this.markup.updateNodeInnerHTML(node, content, oldContent); + }); + }, + + /** + * Paste the contents of the clipboard as adjacent HTML to the selected Node. + * @param position The position as specified for Element.insertAdjacentHTML + * (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd"). + */ + pasteAdjacentHTML: function InspectorPanel_pasteAdjacent(position) { + let content = this._getClipboardContentForPaste(); + if (!content) + return promise.reject("No clipboard content for paste"); + + let node = this.selection.nodeFront; + return this.markup.insertAdjacentHTMLToNode(node, position, content); }, /** * Copy the innerHTML of the selected Node to the clipboard. */ - copyInnerHTML: function InspectorPanel_copyInnerHTML() - { + copyInnerHTML: function InspectorPanel_copyInnerHTML() { if (!this.selection.isNode()) { return; } this._copyLongStr(this.walker.innerHTML(this.selection.nodeFront)); }, /** * Copy the outerHTML of the selected Node to the clipboard.
--- a/browser/devtools/inspector/inspector.xul +++ b/browser/devtools/inspector/inspector.xul @@ -54,20 +54,46 @@ oncommand="inspector.copyUniqueSelector()"/> <menuitem id="node-menu-copyimagedatauri" label="&inspectorCopyImageDataUri.label;" oncommand="inspector.copyImageDataUri()"/> <menuitem id="node-menu-showdomproperties" label="&inspectorShowDOMProperties.label;" oncommand="inspector.showDOMProperties()"/> <menuseparator/> + <menuitem id="node-menu-pasteinnerhtml" + label="&inspectorHTMLPasteInner.label;" + accesskey="&inspectorHTMLPasteInner.accesskey;" + oncommand="inspector.pasteInnerHTML()"/> <menuitem id="node-menu-pasteouterhtml" label="&inspectorHTMLPasteOuter.label;" accesskey="&inspectorHTMLPasteOuter.accesskey;" oncommand="inspector.pasteOuterHTML()"/> + <menu id="node-menu-paste-extra-submenu" + label="&inspectorHTMLPasteExtraSubmenu.label;" + accesskey="&inspectorHTMLPasteExtraSubmenu.accesskey;"> + <menupopup> + <menuitem id="node-menu-pastebefore" + label="&inspectorHTMLPasteBefore.label;" + accesskey="&inspectorHTMLPasteBefore.accesskey;" + oncommand="inspector.pasteAdjacentHTML('beforeBegin')"/> + <menuitem id="node-menu-pasteafter" + label="&inspectorHTMLPasteAfter.label;" + accesskey="&inspectorHTMLPasteAfter.accesskey;" + oncommand="inspector.pasteAdjacentHTML('afterEnd')"/> + <menuitem id="node-menu-pastefirstchild" + label="&inspectorHTMLPasteFirstChild.label;" + accesskey="&inspectorHTMLPasteFirstChild.accesskey;" + oncommand="inspector.pasteAdjacentHTML('afterBegin')"/> + <menuitem id="node-menu-pastelastchild" + label="&inspectorHTMLPasteLastChild.label;" + accesskey="&inspectorHTMLPasteLastChild.accesskey;" + oncommand="inspector.pasteAdjacentHTML('beforeEnd')"/> + </menupopup> + </menu> <menuseparator/> <menuitem id="node-menu-delete" label="&inspectorHTMLDelete.label;" accesskey="&inspectorHTMLDelete.accesskey;" oncommand="inspector.deleteNode()"/> <menuseparator/> <menuitem id="node-menu-pseudo-hover" label=":hover" type="checkbox"
--- a/browser/devtools/inspector/test/browser.ini +++ b/browser/devtools/inspector/test/browser.ini @@ -10,17 +10,18 @@ support-files = doc_inspector_highlight_after_transition.html doc_inspector_highlighter-comments.html doc_inspector_highlighter_csstransform.html doc_inspector_highlighter.html doc_inspector_highlighter_rect.html doc_inspector_highlighter_rect_iframe.html doc_inspector_infobar_01.html doc_inspector_infobar_02.html - doc_inspector_menu.html + doc_inspector_menu-01.html + doc_inspector_menu-02.html doc_inspector_remove-iframe-during-load.html doc_inspector_search.html doc_inspector_search-suggestions.html doc_inspector_select-last-selected-01.html doc_inspector_select-last-selected-02.html head.js [browser_inspector_breadcrumbs.js] @@ -49,17 +50,18 @@ support-files = [browser_inspector_highlighter-selector_02.js] [browser_inspector_highlighter-zoom.js] [browser_inspector_iframe-navigation.js] [browser_inspector_infobar_01.js] [browser_inspector_initialization.js] [browser_inspector_inspect-object-element.js] [browser_inspector_invalidate.js] [browser_inspector_keyboard-shortcuts.js] -[browser_inspector_menu.js] +[browser_inspector_menu-01.js] +[browser_inspector_menu-02.js] [browser_inspector_navigation.js] [browser_inspector_picker-stop-on-destroy.js] [browser_inspector_picker-stop-on-tool-change.js] [browser_inspector_pseudoclass-lock.js] [browser_inspector_pseudoclass-menu.js] [browser_inspector_reload-01.js] [browser_inspector_reload-02.js] [browser_inspector_remove-iframe-during-load.js]
rename from browser/devtools/inspector/test/browser_inspector_menu.js rename to browser/devtools/inspector/test/browser_inspector_menu-01.js --- a/browser/devtools/inspector/test/browser_inspector_menu.js +++ b/browser/devtools/inspector/test/browser_inspector_menu-01.js @@ -9,65 +9,30 @@ http://creativecommons.org/publicdomain/ // As part of bug 1077403, the leaking uncaught rejection should be fixed. // thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: jsterm.focusInput is not a function"); // Test context menu functionality: // 1) menu items are disabled/enabled depending on the clicked node // 2) actions triggered by the items work correctly -const TEST_URL = TEST_URL_ROOT + "doc_inspector_menu.html"; +const TEST_URL = TEST_URL_ROOT + "doc_inspector_menu-01.html"; const MENU_SENSITIVITY_TEST_DATA = [ { desc: "doctype node", selector: null, disabled: true, }, { desc: "element node", selector: "p", disabled: false, } ]; -const PASTE_OUTER_HTML_TEST_DATA = [ - { - desc: "some text", - clipboardData: "some text", - clipboardDataType: undefined, - disabled: false - }, - { - desc: "base64 encoded image data uri", - clipboardData: - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABC" + - "AAAAAA6fptVAAAACklEQVQYV2P4DwABAQEAWk1v8QAAAABJRU5ErkJggg==", - clipboardDataType: undefined, - disabled: true - }, - { - desc: "html", - clipboardData: "<p>some text</p>", - clipboardDataType: "html", - disabled: false - }, - { - desc: "empty string", - clipboardData: "", - clipboardDataType: undefined, - disabled: true - }, - { - desc: "whitespace only", - clipboardData: " \n\n\t\n\n \n", - clipboardDataType: undefined, - disabled: true - }, -]; - const COPY_ITEMS_TEST_DATA = [ { desc: "copy inner html", id: "node-menu-copyinner", text: "This is some example text", }, { desc: "copy outer html", @@ -85,40 +50,35 @@ let clipboard = require("sdk/clipboard") registerCleanupFunction(() => { clipboard = null; }); add_task(function* () { let { inspector, toolbox } = yield openInspectorForURL(TEST_URL); yield testMenuItemSensitivity(); - yield testPasteOuterHTMLMenuItemSensitivity(); yield testCopyMenuItems(); yield testShowDOMProperties(); - yield testPasteOuterHTMLMenu(); yield testDeleteNode(); yield testDeleteRootNode(); function* testMenuItemSensitivity() { info("Testing sensitivity of menu items for different elements."); + // The sensibility for paste options are described in browser_inspector_menu-02.js const MENU_ITEMS = [ "node-menu-copyinner", "node-menu-copyouter", "node-menu-copyuniqueselector", "node-menu-delete", - "node-menu-pasteouterhtml", "node-menu-pseudo-hover", "node-menu-pseudo-active", "node-menu-pseudo-focus" ]; - // To ensure clipboard contains something to paste. - clipboard.set("<p>test</p>", "html"); - for (let {desc, selector, disabled} of MENU_SENSITIVITY_TEST_DATA) { info("Testing context menu entries for " + desc); let front; if (selector) { front = yield getNodeFront(selector, inspector); } else { // Select the docType if no selector is provided @@ -130,35 +90,16 @@ add_task(function* () { contextMenuClick(getContainerForNodeFront(front, inspector).tagLine); for (let name of MENU_ITEMS) { checkMenuItem(name, disabled); } } } - function* testPasteOuterHTMLMenuItemSensitivity() { - info("Checking 'Paste Outer HTML' menu item sensitivity for different types" + - "of data"); - - let nodeFront = yield getNodeFront("p", inspector); - let markupTagLine = getContainerForNodeFront(nodeFront, inspector).tagLine; - - for (let data of PASTE_OUTER_HTML_TEST_DATA) { - let { desc, clipboardData, clipboardDataType, disabled } = data; - info("Checking 'Paste Outer HTML' for " + desc); - clipboard.set(clipboardData, clipboardDataType); - - yield selectNode(nodeFront, inspector); - - contextMenuClick(markupTagLine); - checkMenuItem("node-menu-pasteouterhtml", disabled); - } - } - function* testCopyMenuItems() { info("Testing various copy actions of context menu."); for (let {desc, id, text} of COPY_ITEMS_TEST_DATA) { info("Testing " + desc); let item = inspector.panelDoc.getElementById(id); ok(item, "The popup has a " + desc + " menu item."); @@ -185,37 +126,16 @@ add_task(function* () { yield messagesAdded; info("Checking if 'inspect($0)' was evaluated"); ok(webconsoleUI.jsterm.history[0] === 'inspect($0)'); yield toolbox.toggleSplitConsole(); } - function* testPasteOuterHTMLMenu() { - info("Testing that 'Paste Outer HTML' menu item works."); - clipboard.set("this was pasted"); - - let nodeFront = yield getNodeFront("h1", inspector); - yield selectNode(nodeFront, inspector); - - contextMenuClick(getContainerForNodeFront(nodeFront, inspector).tagLine); - - let onNodeReselected = inspector.markup.once("reselectedonremoved"); - let menu = inspector.panelDoc.getElementById("node-menu-pasteouterhtml"); - dispatchCommandEvent(menu); - - info("Waiting for inspector selection to update"); - yield onNodeReselected; - - ok(content.document.body.outerHTML.contains(clipboard.get()), - "Clipboard content was pasted into the node's outer HTML."); - ok(!getNode("h1", { expectNoMatch: true }), "The original node was removed."); - } - function* testDeleteNode() { info("Testing 'Delete Node' menu item for normal elements."); yield selectNode("p", inspector); let deleteNode = inspector.panelDoc.getElementById("node-menu-delete"); ok(deleteNode, "the popup menu has a delete menu item"); let updated = inspector.once("inspector-updated");
new file mode 100644 --- /dev/null +++ b/browser/devtools/inspector/test/browser_inspector_menu-02.js @@ -0,0 +1,326 @@ +/* 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 context menu functionality: +// 1) menu items are disabled/enabled depending on the clicked node +// 2) actions triggered by the items work correctly + +/////////////////// +// +// Whitelisting this test. +// As part of bug 1077403, the leaking uncaught rejection should be fixed. +// +thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: jsterm.focusInput is not a function"); + +const MENU_SENSITIVITY_TEST_DATA = [ + { + desc: "doctype node", + selector: null, + disabled: true, + }, + { + desc: "element node", + selector: "#sensitivity", + disabled: false, + }, + { + desc: "document element", + selector: "html", + disabled: { + "node-menu-pastebefore": true, + "node-menu-pasteafter": true, + "node-menu-pastefirstchild": true, + "node-menu-pastelastchild": true, + } + }, + { + desc: "body", + selector: "body", + disabled: { + "node-menu-pastebefore": true, + "node-menu-pasteafter": true, + } + }, + { + desc: "head", + selector: "head", + disabled: { + "node-menu-pastebefore": true, + "node-menu-pasteafter": true, + } + } +]; + +const TEST_URL = TEST_URL_ROOT + "doc_inspector_menu-02.html"; + +const PASTE_HTML_TEST_SENSITIVITY_DATA = [ + { + desc: "some text", + clipboardData: "some text", + clipboardDataType: undefined, + disabled: false + }, + { + desc: "base64 encoded image data uri", + clipboardData: + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABC" + + "AAAAAA6fptVAAAACklEQVQYV2P4DwABAQEAWk1v8QAAAABJRU5ErkJggg==", + clipboardDataType: undefined, + disabled: true + }, + { + desc: "html", + clipboardData: "<p>some text</p>", + clipboardDataType: "html", + disabled: false + }, + { + desc: "empty string", + clipboardData: "", + clipboardDataType: undefined, + disabled: true + }, + { + desc: "whitespace only", + clipboardData: " \n\n\t\n\n \n", + clipboardDataType: undefined, + disabled: true + }, +]; + +const PASTE_ADJACENT_HTML_DATA = [ + { + desc: "As First Child", + clipboardData: "2", + menuId: "node-menu-pastefirstchild", + }, + { + desc: "As Last Child", + clipboardData: "4", + menuId: "node-menu-pastelastchild", + }, + { + desc: "Before", + clipboardData: "1", + menuId: "node-menu-pastebefore", + }, + { + desc: "After", + clipboardData: "<span>5</span>", + menuId: "node-menu-pasteafter", + }, +]; + + +let clipboard = require("sdk/clipboard"); +registerCleanupFunction(() => { + clipboard = null; +}); + +add_task(function* () { + let { inspector, toolbox } = yield openInspectorForURL(TEST_URL); + + yield testMenuItemSensitivity(); + yield testPasteHTMLMenuItemsSensitivity(); + yield testPasteOuterHTMLMenu(); + yield testPasteInnerHTMLMenu(); + yield testPasteAdjacentHTMLMenu(); + + function* testMenuItemSensitivity() { + info("Testing sensitivity of menu items for different elements."); + + const MENU_ITEMS = [ + "node-menu-pasteinnerhtml", + "node-menu-pasteouterhtml", + "node-menu-pastebefore", + "node-menu-pasteafter", + "node-menu-pastefirstchild", + "node-menu-pastelastchild", + ]; + + // To ensure clipboard contains something to paste. + clipboard.set("<p>test</p>", "html"); + + for (let {desc, selector, disabled} of MENU_SENSITIVITY_TEST_DATA) { + info("Testing context menu entries for " + desc); + + let front; + if (selector) { + front = yield getNodeFront(selector, inspector); + } else { + // Select the docType if no selector is provided + let {nodes} = yield inspector.walker.children(inspector.walker.rootNode); + front = nodes[0]; + } + yield selectNode(front, inspector); + + contextMenuClick(getContainerForNodeFront(front, inspector).tagLine); + + for (let name of MENU_ITEMS) { + let disabledForMenu = typeof disabled === "object" ? + disabled[name] : disabled; + info(`${name} should be ${disabledForMenu ? "disabled" : "enabled"} ` + + `for ${desc}`); + checkMenuItem(name, disabledForMenu); + } + } + } + + function* testPasteHTMLMenuItemsSensitivity() { + let menus = [ + "node-menu-pasteinnerhtml", + "node-menu-pasteouterhtml", + "node-menu-pastebefore", + "node-menu-pasteafter", + "node-menu-pastefirstchild", + "node-menu-pastelastchild", + ]; + + info("Checking Paste menu items sensitivity for different types" + + "of data"); + + let nodeFront = yield getNodeFront("#paste-area", inspector); + let markupTagLine = getContainerForNodeFront(nodeFront, inspector).tagLine; + + for (let menuId of menus) { + for (let data of PASTE_HTML_TEST_SENSITIVITY_DATA) { + let { desc, clipboardData, clipboardDataType, disabled } = data; + let menuLabel = getLabelFor("#" + menuId); + info(`Checking ${menuLabel} for ${desc}`); + clipboard.set(clipboardData, clipboardDataType); + + yield selectNode(nodeFront, inspector); + + contextMenuClick(markupTagLine); + checkMenuItem(menuId, disabled); + } + } + } + + function* testPasteOuterHTMLMenu() { + info("Testing that 'Paste Outer HTML' menu item works."); + clipboard.set("this was pasted (outerHTML)"); + let outerHTMLSelector = "#paste-area h1"; + + let nodeFront = yield getNodeFront(outerHTMLSelector, inspector); + yield selectNode(nodeFront, inspector); + + contextMenuClick(getContainerForNodeFront(nodeFront, inspector).tagLine); + + let onNodeReselected = inspector.markup.once("reselectedonremoved"); + let menu = inspector.panelDoc.getElementById("node-menu-pasteouterhtml"); + dispatchCommandEvent(menu); + + info("Waiting for inspector selection to update"); + yield onNodeReselected; + + ok(content.document.body.outerHTML.contains(clipboard.get()), + "Clipboard content was pasted into the node's outer HTML."); + ok(!getNode(outerHTMLSelector, { expectNoMatch: true }), + "The original node was removed."); + } + + function* testPasteInnerHTMLMenu() { + info("Testing that 'Paste Inner HTML' menu item works."); + clipboard.set("this was pasted (innerHTML)"); + let innerHTMLSelector = "#paste-area .inner"; + let getInnerHTML = () => content.document.querySelector(innerHTMLSelector).innerHTML; + let origInnerHTML = getInnerHTML(); + + let nodeFront = yield getNodeFront(innerHTMLSelector, inspector); + yield selectNode(nodeFront, inspector); + + contextMenuClick(getContainerForNodeFront(nodeFront, inspector).tagLine); + + let onMutation = inspector.once("markupmutation"); + let menu = inspector.panelDoc.getElementById("node-menu-pasteinnerhtml"); + dispatchCommandEvent(menu); + + info("Waiting for mutation to occur"); + yield onMutation; + + ok(getInnerHTML() === clipboard.get(), + "Clipboard content was pasted into the node's inner HTML."); + ok(getNode(innerHTMLSelector), "The original node has been preserved."); + yield undoChange(inspector); + ok(getInnerHTML() === origInnerHTML, "Previous innerHTML has been " + + "restored after undo"); + } + + function* testPasteAdjacentHTMLMenu() { + let refSelector = "#paste-area .adjacent .ref"; + let adjacentNode = content.document.querySelector(refSelector).parentNode; + let nodeFront = yield getNodeFront(refSelector, inspector); + yield selectNode(nodeFront, inspector); + let markupTagLine = getContainerForNodeFront(nodeFront, inspector).tagLine; + + for (let { desc, clipboardData, menuId } of PASTE_ADJACENT_HTML_DATA) { + let menu = inspector.panelDoc.getElementById(menuId); + info(`Testing ${getLabelFor(menu)} for ${clipboardData}`); + clipboard.set(clipboardData); + + contextMenuClick(markupTagLine); + let onMutation = inspector.once("markupmutation"); + dispatchCommandEvent(menu); + + info("Waiting for mutation to occur"); + yield onMutation; + } + + ok(adjacentNode.innerHTML.trim() === "1<span class=\"ref\">234</span>" + + "<span>5</span>", "The Paste as Last Child / as First Child / Before " + + "/ After worked as expected"); + yield undoChange(inspector); + ok(adjacentNode.innerHTML.trim() === "1<span class=\"ref\">234</span>", + "Undo works for paste adjacent HTML"); + } + + function checkMenuItem(elementId, disabled) { + if (disabled) { + checkDisabled(elementId); + } else { + checkEnabled(elementId); + } + } + + function checkEnabled(elementId) { + let elt = inspector.panelDoc.getElementById(elementId); + ok(!elt.hasAttribute("disabled"), + '"' + elt.label + '" context menu option is not disabled'); + } + + function checkDisabled(elementId) { + let elt = inspector.panelDoc.getElementById(elementId); + ok(elt.hasAttribute("disabled"), + '"' + elt.label + '" context menu option is disabled'); + } + + function dispatchCommandEvent(node) { + info("Dispatching command event on " + node); + let commandEvent = document.createEvent("XULCommandEvent"); + commandEvent.initCommandEvent("command", true, true, window, 0, false, false, + false, false, null); + node.dispatchEvent(commandEvent); + } + + function contextMenuClick(element) { + info("Simulating contextmenu event on " + element); + let evt = element.ownerDocument.createEvent('MouseEvents'); + let button = 2; // right click + + evt.initMouseEvent('contextmenu', true, true, + element.ownerDocument.defaultView, 1, 0, 0, 0, 0, false, + false, false, false, button, null); + + element.dispatchEvent(evt); + } + + function getLabelFor(elt) { + if (typeof elt === "string") + elt = inspector.panelDoc.querySelector(elt); + let isInPasteSubMenu = elt.matches("#node-menu-paste-extra-submenu *"); + return `"${isInPasteSubMenu ? "Paste > " : ""}${elt.label}"`; + } +});
rename from browser/devtools/inspector/test/doc_inspector_menu.html rename to browser/devtools/inspector/test/doc_inspector_menu-01.html
new file mode 100644 --- /dev/null +++ b/browser/devtools/inspector/test/doc_inspector_menu-02.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> + <head> + <title>Inspector Tree Menu Test</title> + <meta charset="utf-8"> + </head> + <body> + <div> + <div id="paste-area"> + <h1>Inspector Tree Menu Test</h1> + <p class="inner">Unset</p> + <p class="adjacent"> + <span class="ref">3</span> + </p> + </div> + <p data-id="copy">Paragraph for testing copy</p> + <p id="sensitivity">Paragraph for sensitivity</p> + <p id="delete">This has to be deleted</p> + </div> + </body> +</html>
--- a/browser/devtools/inspector/test/head.js +++ b/browser/devtools/inspector/test/head.js @@ -659,8 +659,48 @@ function executeInContent(name, data={}, mm.sendAsyncMessage(name, data, objects); if (expectResponse) { return waitForContentMessage(name); } else { return promise.resolve(); } } + +/** + * Undo the last markup-view action and wait for the corresponding mutation to + * occur + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return a promise that resolves when the markup-mutation has been treated or + * rejects if no undo action is possible + */ +function undoChange(inspector) { + let canUndo = inspector.markup.undo.canUndo(); + ok(canUndo, "The last change in the markup-view can be undone"); + if (!canUndo) { + return promise.reject(); + } + + let mutated = inspector.once("markupmutation"); + inspector.markup.undo.undo(); + return mutated; +} + +/** + * Redo the last markup-view action and wait for the corresponding mutation to + * occur + * @param {InspectorPanel} inspector The instance of InspectorPanel currently + * loaded in the toolbox + * @return a promise that resolves when the markup-mutation has been treated or + * rejects if no redo action is possible + */ +function redoChange(inspector) { + let canRedo = inspector.markup.undo.canRedo(); + ok(canRedo, "The last change in the markup-view can be redone"); + if (!canRedo) { + return promise.reject(); + } + + let mutated = inspector.once("markupmutation"); + inspector.markup.undo.redo(); + return mutated; +}
--- a/browser/devtools/markupview/markup-view.js +++ b/browser/devtools/markupview/markup-view.js @@ -28,17 +28,17 @@ Cu.import("resource://gre/modules/devtoo Cu.import("resource://gre/modules/devtools/Templater.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); loader.lazyGetter(this, "DOMParser", function() { return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser); }); loader.lazyGetter(this, "AutocompletePopup", () => { - return require("devtools/shared/autocomplete-popup").AutocompletePopup + return require("devtools/shared/autocomplete-popup").AutocompletePopup; }); /** * Vocabulary for the purposes of this file: * * MarkupContainer - the structure that holds an editor and its * immediate children in the markup panel. * - MarkupElementContainer: markup container for element nodes @@ -697,26 +697,29 @@ MarkupView.prototype = { if (type === "attributes" || type === "characterData") { addedOrEditedContainers.add(container); } else if (type === "childList") { // If there has been removals, flash the parent if (removed.length) { removedContainers.add(container); } - // If there has been additions, flash the nodes + // If there has been additions, flash the nodes if their associated + // container exist (so if their parent is expanded in the inspector). added.forEach(added => { let addedContainer = this.getContainer(added); - addedOrEditedContainers.add(addedContainer); + if (addedContainer) { + addedOrEditedContainers.add(addedContainer); - // The node may be added as a result of an append, in which case it - // it will have been removed from another container first, but in - // these cases we don't want to flash both the removal and the - // addition - removedContainers.delete(container); + // The node may be added as a result of an append, in which case + // it will have been removed from another container first, but in + // these cases we don't want to flash both the removal and the + // addition + removedContainers.delete(container); + } }); } } } for (let container of removedContainers) { container.flashMutation(); } @@ -796,29 +799,55 @@ MarkupView.prototype = { * Collapse the node's children. */ collapseNode: function(aNode) { let container = this.getContainer(aNode); container.expanded = false; }, /** + * Returns either the innerHTML or the outerHTML for a remote node. + * @param aNode The NodeFront to get the outerHTML / innerHTML for. + * @param isOuter A boolean that, if true, makes the function return the + * outerHTML, otherwise the innerHTML. + * @returns A promise that will be resolved with the outerHTML / innerHTML. + */ + _getNodeHTML: function(aNode, isOuter) { + let walkerPromise = null; + + if (isOuter) { + walkerPromise = this.walker.outerHTML(aNode); + } else { + walkerPromise = this.walker.innerHTML(aNode); + } + + return walkerPromise.then(longstr => { + return longstr.string().then(html => { + longstr.release().then(null, console.error); + return html; + }); + }); + }, + + /** * Retrieve the outerHTML for a remote node. * @param aNode The NodeFront to get the outerHTML for. * @returns A promise that will be resolved with the outerHTML. */ getNodeOuterHTML: function(aNode) { - let def = promise.defer(); - this.walker.outerHTML(aNode).then(longstr => { - longstr.string().then(outerHTML => { - longstr.release().then(null, console.error); - def.resolve(outerHTML); - }); - }); - return def.promise; + return this._getNodeHTML(aNode, true); + }, + + /** + * Retrieve the innerHTML for a remote node. + * @param aNode The NodeFront to get the innerHTML for. + * @returns A promise that will be resolved with the innerHTML. + */ + getNodeInnerHTML: function(aNode) { + return this._getNodeHTML(aNode); }, /** * Listen to mutations, expect a given node to be removed and try and select * the node that sits at the same place instead. * This is useful when changing the outerHTML or the tag name so that the * newly inserted node gets selected instead of the one that just got removed. */ @@ -881,41 +910,99 @@ MarkupView.prototype = { this._removedNodeObserver = null; this.emit("canceledreselectonremoved"); } }, /** * Replace the outerHTML of any node displayed in the inspector with * some other HTML code - * @param aNode node which outerHTML will be replaced. - * @param newValue The new outerHTML to set on the node. - * @param oldValue The old outerHTML that will be used if the user undos the update. + * @param {NodeFront} node node which outerHTML will be replaced. + * @param {string} newValue The new outerHTML to set on the node. + * @param {string} oldValue The old outerHTML that will be used if the + * user undoes the update. * @returns A promise that will resolve when the outer HTML has been updated. */ - updateNodeOuterHTML: function(aNode, newValue, oldValue) { - let container = this._containers.get(aNode); + updateNodeOuterHTML: function(node, newValue, oldValue) { + let container = this.getContainer(node); if (!container) { return promise.reject(); } // Changing the outerHTML removes the node which outerHTML was changed. // Listen to this removal to reselect the right node afterwards. - this.reselectOnRemoved(aNode, "outerhtml"); - return this.walker.setOuterHTML(aNode, newValue).then(null, () => { + this.reselectOnRemoved(node, "outerhtml"); + return this.walker.setOuterHTML(node, newValue).then(null, () => { this.cancelReselectOnRemoved(); }); }, /** + * Replace the innerHTML of any node displayed in the inspector with + * some other HTML code + * @param {Node} node node which innerHTML will be replaced. + * @param {string} newValue The new innerHTML to set on the node. + * @param {string} oldValue The old innerHTML that will be used if the user + * undoes the update. + * @returns A promise that will resolve when the inner HTML has been updated. + */ + updateNodeInnerHTML: function(node, newValue, oldValue) { + let container = this.getContainer(node); + if (!container) { + return promise.reject(); + } + + let def = promise.defer(); + + container.undo.do(() => { + this.walker.setInnerHTML(node, newValue).then(def.resolve, def.reject); + }, () => { + this.walker.setInnerHTML(node, oldValue); + }); + + return def.promise; + }, + + /** + * Insert adjacent HTML to any node displayed in the inspector. + * + * @param {NodeFront} node The reference node. + * @param {string} position The position as specified for Element.insertAdjacentHTML + * (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd"). + * @param {string} newValue The adjacent HTML. + * @returns A promise that will resolve when the adjacent HTML has + * been inserted. + */ + insertAdjacentHTMLToNode: function(node, position, value) { + let container = this.getContainer(node); + if (!container) { + return promise.reject(); + } + + let def = promise.defer(); + + let injectedNodes = []; + container.undo.do(() => { + this.walker.insertAdjacentHTML(node, position, value).then(nodeArray => { + injectedNodes = nodeArray.nodes; + return nodeArray; + }).then(def.resolve, def.reject); + }, () => { + this.walker.removeNodes(injectedNodes); + }); + + return def.promise; + }, + + /** * Open an editor in the UI to allow editing of a node's outerHTML. * @param aNode The NodeFront to edit. */ beginEditingOuterHTML: function(aNode) { - this.getNodeOuterHTML(aNode).then((oldValue)=> { + this.getNodeOuterHTML(aNode).then(oldValue => { let container = this.getContainer(aNode); if (!container) { return; } this.htmlEditor.show(container.tagLine, oldValue); this.htmlEditor.once("popuphidden", (e, aCommit, aValue) => { // Need to focus the <html> element instead of the frame / window // in order to give keyboard focus back to doc (from editor). @@ -1212,17 +1299,17 @@ MarkupView.prototype = { this._frame.contentWindow.removeEventListener("keydown", this._boundKeyDown, false); this._boundKeyDown = null; this._inspector.selection.off("new-node-front", this._boundOnNewSelection); this._boundOnNewSelection = null; - this.walker.off("mutations", this._boundMutationObserver) + this.walker.off("mutations", this._boundMutationObserver); this._boundMutationObserver = null; this.walker.off("display-change", this._boundOnDisplayChange); this._boundOnDisplayChange = null; this._elt.removeEventListener("mousemove", this._onMouseMove, false); this._elt.removeEventListener("mouseleave", this._onMouseLeave, false); this._elt = null; @@ -1919,17 +2006,17 @@ function TextEditor(aContainer, aNode, a this.container.undo.do(() => { this.node.setNodeValue(aVal).then(() => { this.markup.nodeChanged(this.node); }); }, () => { this.node.setNodeValue(oldValue).then(() => { this.markup.nodeChanged(this.node); - }) + }); }); }); }); } }); this.update(); } @@ -2150,17 +2237,17 @@ ElementEditor.prototype = { try { this._saveAttribute(aAttr.name, undoMods); doMods.removeAttribute(aAttr.name); this._applyAttributes(aVal, attr, doMods, undoMods); this.container.undo.do(() => { doMods.apply(); }, () => { undoMods.apply(); - }) + }); } catch(ex) { console.error(ex); } } }); // Figure out where we should place the attribute. let before = aBefore;
--- a/browser/locales/en-US/chrome/browser/devtools/inspector.dtd +++ b/browser/locales/en-US/chrome/browser/devtools/inspector.dtd @@ -1,26 +1,96 @@ +<!-- LOCALIZATION NOTE (inspectorHTMLEdit.label): This is the label shown + in the inspector contextual-menu for the item that lets users edit the + (outer) HTML of the current node --> <!ENTITY inspectorHTMLEdit.label "Edit As HTML"> <!ENTITY inspectorHTMLEdit.accesskey "E"> +<!-- LOCALIZATION NOTE (inspectorHTMLCopyInner.label): This is the label shown + in the inspector contextual-menu for the item that lets users copy the + inner HTML of the current node --> <!ENTITY inspectorHTMLCopyInner.label "Copy Inner HTML"> <!ENTITY inspectorHTMLCopyInner.accesskey "I"> +<!-- LOCALIZATION NOTE (inspectorHTMLCopyOuter.label): This is the label shown + in the inspector contextual-menu for the item that lets users copy the + outer HTML of the current node --> <!ENTITY inspectorHTMLCopyOuter.label "Copy Outer HTML"> <!ENTITY inspectorHTMLCopyOuter.accesskey "O"> +<!-- LOCALIZATION NOTE (inspectorCopyUniqueSelector.label): This is the label + shown in the inspector contextual-menu for the item that lets users copy + the CSS Selector of the current node --> <!ENTITY inspectorCopyUniqueSelector.label "Copy Unique Selector"> <!ENTITY inspectorCopyUniqueSelector.accesskey "U"> +<!-- LOCALIZATION NOTE (inspectorHTMLPasteOuter.label): This is the label shown + in the inspector contextual-menu for the item that lets users paste outer + HTML in the current node --> <!ENTITY inspectorHTMLPasteOuter.label "Paste Outer HTML"> <!ENTITY inspectorHTMLPasteOuter.accesskey "P"> +<!-- LOCALIZATION NOTE (inspectorHTMLPasteInner.label): This is the label shown + in the inspector contextual-menu for the item that lets users paste inner + HTML in the current node --> +<!ENTITY inspectorHTMLPasteInner.label "Paste Inner HTML"> +<!ENTITY inspectorHTMLPasteInner.accesskey "N"> + +<!-- LOCALIZATION NOTE (inspectorHTMLPasteExtraSubmenu.label): This is the label + shown in the inspector contextual-menu for the sub-menu of the other Paste + items, which allow to paste HTML: + - before the current node + - after the current node + - as the first child of the current node + - as the last child of the current node --> +<!ENTITY inspectorHTMLPasteExtraSubmenu.label "Paste ..."> +<!ENTITY inspectorHTMLPasteExtraSubmenu.accesskey "T"> + +<!-- LOCALIZATION NOTE (inspectorHTMLPasteBefore.label): This is the label shown + in the inspector contextual-menu for the item that lets users paste + the HTML before the current node --> +<!ENTITY inspectorHTMLPasteBefore.label "Before"> +<!ENTITY inspectorHTMLPasteBefore.accesskey "B"> + +<!-- LOCALIZATION NOTE (inspectorHTMLPasteAfter.label): This is the label shown + in the inspector contextual-menu for the item that lets users paste + the HTML after the current node --> +<!ENTITY inspectorHTMLPasteAfter.label "After"> +<!ENTITY inspectorHTMLPasteAfter.accesskey "A"> + +<!-- LOCALIZATION NOTE (inspectorHTMLPasteFirstChild.label): This is the label + shown in the inspector contextual-menu for the item that lets users paste + the HTML as the first child the current node --> +<!ENTITY inspectorHTMLPasteFirstChild.label "As First Child"> +<!ENTITY inspectorHTMLPasteFirstChild.accesskey "F"> + +<!-- LOCALIZATION NOTE (inspectorHTMLPasteLastChild.label): This is the label + shown in the inspector contextual-menu for the item that lets users paste + the HTML as the last child the current node --> +<!ENTITY inspectorHTMLPasteLastChild.label "As Last Child"> +<!ENTITY inspectorHTMLPasteLastChild.accesskey "L"> + + +<!-- LOCALIZATION NOTE (inspectorHTMLDelete.label): This is the label shown in + the inspector contextual-menu for the item that lets users delete the + current node --> <!ENTITY inspectorHTMLDelete.label "Delete Node"> <!ENTITY inspectorHTMLDelete.accesskey "D"> <!ENTITY inspector.selectButton.tooltip "Select element with mouse"> +<!-- LOCALIZATION NOTE (inspectorSearchHTML.label): This is the label shown as + the placeholder in inspector search box --> <!ENTITY inspectorSearchHTML.label "Search HTML"> <!ENTITY inspectorSearchHTML.key "F"> +<!-- LOCALIZATION NOTE (inspectorCopyImageDataUri.label): This is the label + shown in the inspector contextual-menu for the item that lets users copy + the URL embedding the image data encoded in Base 64 (what we name + here Image Data URL). For more information: + https://developer.mozilla.org/en-US/docs/Web/HTTP/data_URIs --> <!ENTITY inspectorCopyImageDataUri.label "Copy Image Data-URL"> +<!-- LOCALIZATION NOTE (inspectorShowDOMProperties.label): This is the label + shown in the inspector contextual-menu for the item that lets users see + the DOM properties of the current node. When triggered, this item + opens the split Console and displays the properties in its side panel. --> <!ENTITY inspectorShowDOMProperties.label "Show DOM Properties">
--- a/toolkit/devtools/server/actors/inspector.js +++ b/toolkit/devtools/server/actors/inspector.js @@ -1037,24 +1037,17 @@ var NodeListActor = exports.NodeListActo response: RetVal("disconnectedNode") }), /** * Get a range of the items from the node list. */ items: method(function(start=0, end=this.nodeList.length) { let items = [this.walker._ref(item) for (item of Array.prototype.slice.call(this.nodeList, start, end))]; - let newParents = new Set(); - for (let item of items) { - this.walker.ensurePathToRoot(item, newParents); - } - return { - nodes: items, - newParents: [node for (node of newParents)] - } + return this.walker.attachElements(items); }, { request: { start: Arg(0, "nullable:number"), end: Arg(1, "nullable:number") }, response: RetVal("disconnectedNodeArray") }), @@ -1294,22 +1287,53 @@ var WalkerActor = protocol.ActorClass({ * * Keeping these actor methods for now allows newer client-side debuggers to * inspect fxos 1.2 remote targets or older firefox desktop remote targets. */ pick: method(function() {}, {request: {}, response: RetVal("disconnectedNode")}), cancelPick: method(function() {}), highlight: method(function(node) {}, {request: {node: Arg(0, "nullable:domnode")}}), + /** + * Ensures that the node is attached and it can be accessed from the root. + * + * @param {(Node|NodeActor)} nodes The nodes + * @return {Object} An object compatible with the disconnectedNode type. + */ attachElement: function(node) { - node = this._ref(node); - let newParents = this.ensurePathToRoot(node); + let { nodes, newParents } = this.attachElements([node]); return { - node: node, - newParents: [parent for (parent of newParents)] + node: nodes[0], + newParents: newParents + }; + }, + + /** + * Ensures that the nodes are attached and they can be accessed from the root. + * + * @param {(Node[]|NodeActor[])} nodes The nodes + * @return {Object} An object compatible with the disconnectedNodeArray type. + */ + attachElements: function(nodes) { + let nodeActors = []; + let newParents = new Set(); + for (let node of nodes) { + // Be sure we deal with NodeActor only. + if (!(node instanceof NodeActor)) + node = this._ref(node); + + this.ensurePathToRoot(node, newParents); + // If nodes may be an array of raw nodes, we're sure to only have + // NodeActors with the following array. + nodeActors.push(node); + } + + return { + nodes: nodeActors, + newParents: [...newParents] }; }, /** * Watch the given document node for mutations using the DOM observer * API. */ _watchDocument: function(actor) { @@ -2049,31 +2073,55 @@ var WalkerActor = protocol.ActorClass({ node: Arg(0, "domnode") }, response: { value: RetVal("longstring") } }), /** + * Set a node's innerHTML property. + * + * @param {NodeActor} node The node. + * @param {string} value The piece of HTML content. + */ + setInnerHTML: method(function(node, value) { + let rawNode = node.rawNode; + if (rawNode.nodeType !== rawNode.ownerDocument.ELEMENT_NODE) + throw new Error("Can only change innerHTML to element nodes"); + rawNode.innerHTML = value; + }, { + request: { + node: Arg(0, "domnode"), + value: Arg(1, "string"), + }, + response: {} + }), + + /** * Get a node's outerHTML property. + * + * @param {NodeActor} node The node. */ outerHTML: method(function(node) { return LongStringActor(this.conn, node.rawNode.outerHTML); }, { request: { node: Arg(0, "domnode") }, response: { value: RetVal("longstring") } }), /** * Set a node's outerHTML property. + * + * @param {NodeActor} node The node. + * @param {string} value The piece of HTML content. */ setOuterHTML: method(function(node, value) { let parsedDOM = DOMParser.parseFromString(value, "text/html"); let rawNode = node.rawNode; let parentNode = rawNode.parentNode; // Special case for head and body. Setting document.body.outerHTML // creates an extra <head> tag, and document.head.outerHTML creates @@ -2112,48 +2160,148 @@ var WalkerActor = protocol.ActorClass({ rawNode.replaceChild(parsedDOM.head, rawNode.querySelector("head")); rawNode.replaceChild(parsedDOM.body, rawNode.querySelector("body")); } else { rawNode.outerHTML = value; } }, { request: { node: Arg(0, "domnode"), - value: Arg(1), + value: Arg(1, "string"), }, response: {} }), /** + * Insert adjacent HTML to a node. + * + * @param {Node} node + * @param {string} position One of "beforeBegin", "afterBegin", "beforeEnd", + * "afterEnd" (see Element.insertAdjacentHTML). + * @param {string} value The HTML content. + */ + insertAdjacentHTML: method(function(node, position, value) { + let rawNode = node.rawNode; + // Don't insert anything adjacent to the document element, + // the head or the body. + if (node.isDocumentElement()) { + throw new Error("Can't insert adjacent element to the root."); + } + + let isInsertAsSibling = position === "beforeBegin" || + position === "afterEnd"; + if ((rawNode.tagName === "BODY" || rawNode.tagName === "HEAD") && + isInsertAsSibling) { + throw new Error("Can't insert element before or after the body " + + "or the head."); + } + + let rawParentNode = rawNode.parentNode; + if (!rawParentNode && isInsertAsSibling) { + throw new Error("Can't insert as sibling without parent node."); + } + + // We can't use insertAdjacentHTML, because we want to return the nodes + // being created (so the front can remove them if the user undoes + // the change). So instead, use Range.createContextualFragment(). + let range = rawNode.ownerDocument.createRange(); + if (position === "beforeBegin" || position === "afterEnd") { + range.selectNode(rawNode); + } else { + range.selectNodeContents(rawNode); + } + let docFrag = range.createContextualFragment(value); + let newRawNodes = Array.from(docFrag.childNodes); + switch (position) { + case "beforeBegin": + rawParentNode.insertBefore(docFrag, rawNode); + break; + case "afterEnd": + // Note: if the second argument is null, rawParentNode.insertBefore + // behaves like rawParentNode.appendChild. + rawParentNode.insertBefore(docFrag, rawNode.nextSibling); + case "afterBegin": + rawNode.insertBefore(docFrag, rawNode.firstChild); + break; + case "beforeEnd": + rawNode.appendChild(docFrag); + break; + default: + throw new Error('Invalid position value. Must be either ' + + '"beforeBegin", "beforeEnd", "afterBegin" or "afterEnd".'); + } + + return this.attachElements(newRawNodes); + }, { + request: { + node: Arg(0, "domnode"), + position: Arg(1, "string"), + value: Arg(2, "string") + }, + response: RetVal("disconnectedNodeArray") + }), + + /** + * Test whether a node is a document or a document element. + * + * @param {NodeActor} node The node to remove. + * @return {boolean} True if the node is a document or a document element. + */ + isDocumentOrDocumentElementNode: function(node) { + return ((node.rawNode.ownerDocument && + node.rawNode.ownerDocument.documentElement === this.rawNode) || + node.rawNode.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE); + }, + + /** * Removes a node from its parent node. * + * @param {NodeActor} node The node to remove. * @returns The node's nextSibling before it was removed. */ removeNode: method(function(node) { - if ((node.rawNode.ownerDocument && - node.rawNode.ownerDocument.documentElement === this.rawNode) || - node.rawNode.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) { + if (this.isDocumentOrDocumentElementNode(node)) throw Error("Cannot remove document or document elements."); - } let nextSibling = this.nextSibling(node); - if (node.rawNode.parentNode) { - node.rawNode.parentNode.removeChild(node.rawNode); - // Mutation events will take care of the rest. - } + node.rawNode.remove(); + // Mutation events will take care of the rest. return nextSibling; }, { request: { node: Arg(0, "domnode") }, response: { nextSibling: RetVal("nullable:domnode") } }), /** + * Removes an array of nodes from their parent node. + * + * @param {NodeActor[]} nodes The nodes to remove. + */ + removeNodes: method(function(nodes) { + // Check that all nodes are valid before processing the removals. + for (let node of nodes) { + if (this.isDocumentOrDocumentElementNode(node)) + throw Error("Cannot remove document or document elements."); + } + + for (let node of nodes) { + node.rawNode.remove(); + // Mutation events will take care of the rest. + } + }, { + request: { + node: Arg(0, "array:domnode") + }, + response: {} + }), + + /** * Insert a node into the DOM. */ insertBefore: method(function(node, parent, sibling) { parent.rawNode.insertBefore(node.rawNode, sibling ? sibling.rawNode : null); }, { request: { node: Arg(0, "domnode"), parent: Arg(1, "domnode"),
--- a/toolkit/devtools/server/actors/root.js +++ b/toolkit/devtools/server/actors/root.js @@ -114,17 +114,21 @@ function RootActor(aConnection, aParamet } RootActor.prototype = { constructor: RootActor, applicationType: "browser", traits: { sources: true, + // Whether the inspector actor allows modifying outer HTML. editOuterHTML: true, + // Whether the inspector actor allows modifying innerHTML and inserting + // adjacent HTML. + pasteHTML: true, // Whether the server-side highlighter actor exists and can be used to // remotely highlight nodes (see server/actors/highlighter.js) highlightable: true, // Which custom highlighter does the server-side highlighter actor supports? // (see server/actors/highlighter.js) customHighlighters: [ "BoxModelHighlighter", "CssTransformHighlighter",