From 802c833161baaf8a790e26fdfdb9bf6714d300f7 Mon Sep 17 00:00:00 2001 From: Brian Grinstead Date: Thu, 24 Oct 2013 08:41:03 -0500 Subject: [PATCH] Bug 895561 - 'Edit As HTML' option in the markup view - browser changes, r=jwalker --- browser/devtools/inspector/inspector-panel.js | 25 +- browser/devtools/inspector/inspector.xul | 4 + browser/devtools/markupview/html-editor.js | 182 +++++++++++ browser/devtools/markupview/markup-view.css | 31 ++ browser/devtools/markupview/markup-view.js | 151 ++++++++- browser/devtools/markupview/markup-view.xhtml | 2 +- browser/devtools/markupview/test/browser.ini | 1 + ...browser_inspector_markup_edit_outerhtml.js | 295 ++++++++++++++++++ browser/devtools/markupview/test/head.js | 1 + .../sourceeditor/codemirror/mozilla.css | 4 - browser/devtools/sourceeditor/editor.js | 12 +- .../chrome/browser/devtools/inspector.dtd | 3 + browser/themes/shared/devtools/dark-theme.css | 84 ++++- .../themes/shared/devtools/light-theme.css | 82 ++++- 14 files changed, 836 insertions(+), 41 deletions(-) create mode 100644 browser/devtools/markupview/html-editor.js create mode 100644 browser/devtools/markupview/test/browser_inspector_markup_edit_outerhtml.js diff --git a/browser/devtools/inspector/inspector-panel.js b/browser/devtools/inspector/inspector-panel.js index f157c051d8fa..c6fb0b7c8fb8 100644 --- a/browser/devtools/inspector/inspector-panel.js +++ b/browser/devtools/inspector/inspector-panel.js @@ -86,6 +86,8 @@ InspectorPanel.prototype = { _deferredOpen: function(defaultSelection) { let deferred = promise.defer(); + this.outerHTMLEditable = this._target.client.traits.editOuterHTML; + this.onNewRoot = this.onNewRoot.bind(this); this.walker.on("new-root", this.onNewRoot); @@ -593,7 +595,8 @@ InspectorPanel.prototype = { let unique = this.panelDoc.getElementById("node-menu-copyuniqueselector"); let copyInnerHTML = this.panelDoc.getElementById("node-menu-copyinner"); let copyOuterHTML = this.panelDoc.getElementById("node-menu-copyouter"); - if (this.selection.isElementNode()) { + let selectionIsElement = this.selection.isElementNode(); + if (selectionIsElement) { unique.removeAttribute("disabled"); copyInnerHTML.removeAttribute("disabled"); copyOuterHTML.removeAttribute("disabled"); @@ -602,6 +605,13 @@ InspectorPanel.prototype = { copyInnerHTML.setAttribute("disabled", "true"); copyOuterHTML.setAttribute("disabled", "true"); } + + let editHTML = this.panelDoc.getElementById("node-menu-edithtml"); + if (this.outerHTMLEditable && selectionIsElement) { + editHTML.removeAttribute("disabled"); + } else { + editHTML.setAttribute("disabled", "true"); + } }, _resetNodeMenu: function InspectorPanel_resetNodeMenu() { @@ -705,6 +715,19 @@ InspectorPanel.prototype = { } }, + /** + * Edit the outerHTML of the selected Node. + */ + editHTML: function InspectorPanel_editHTML() + { + if (!this.selection.isNode()) { + return; + } + if (this.markup) { + this.markup.beginEditingOuterHTML(this.selection.nodeFront); + } + }, + /** * Copy the innerHTML of the selected Node to the clipboard. */ diff --git a/browser/devtools/inspector/inspector.xul b/browser/devtools/inspector/inspector.xul index 3aa4cd416d0f..b3ebe0c1d694 100644 --- a/browser/devtools/inspector/inspector.xul +++ b/browser/devtools/inspector/inspector.xul @@ -33,6 +33,10 @@ + { + this.hide(false); + }).then(null, (err) => console.log(err.message)); +} + +HTMLEditor.prototype = { + + /** + * Need to refresh position by manually setting CSS values, so this will + * need to be called on resizes and other sizing changes. + */ + refresh: function() { + let element = this._attachedElement; + + if (element) { + this.container.style.top = element.offsetTop + "px"; + this.container.style.left = element.offsetLeft + "px"; + this.container.style.width = element.offsetWidth + "px"; + this.container.style.height = element.parentNode.offsetHeight + "px"; + this.editor.refresh(); + } + }, + + /** + * Anchor the editor to a particular element. + * + * @param DOMNode element + * The element that the editor will be anchored to. + * Should belong to the HTMLDocument passed into the constructor. + */ + _attach: function(element) + { + this._detach(); + this._attachedElement = element; + element.classList.add("html-editor-container"); + this.refresh(); + }, + + /** + * Unanchor the editor from an element. + */ + _detach: function() + { + if (this._attachedElement) { + this._attachedElement.classList.remove("html-editor-container"); + this._attachedElement = undefined; + } + }, + + /** + * Anchor the editor to a particular element, and show the editor. + * + * @param DOMNode element + * The element that the editor will be anchored to. + * Should belong to the HTMLDocument passed into the constructor. + * @param string text + * Value to set the contents of the editor to + * @param function cb + * The function to call when hiding + */ + show: function(element, text) + { + if (this._visible) { + return; + } + + this._originalValue = text; + this.editor.setText(text); + this._attach(element); + this.container.style.display = "flex"; + this._visible = true; + + this.editor.refresh(); + this.editor.focus(); + }, + + /** + * Hide the editor, optionally committing the changes + * + * @param bool shouldCommit + * A change will be committed by default. If this param + * strictly equals false, no change will occur. + */ + hide: function(shouldCommit) + { + if (!this._visible) { + return; + } + + this.container.style.display = "none"; + this._detach(); + + let newValue = this.editor.getText(); + let valueHasChanged = this._originalValue !== newValue; + let preventCommit = shouldCommit === false || !valueHasChanged; + this.emit("popup-hidden", !preventCommit, newValue); + this._originalValue = undefined; + this._visible = undefined; + }, + + /** + * Destroy this object and unbind all event handlers + */ + destroy: function() + { + this.doc.defaultView.removeEventListener("resize", + this.refresh, true); + this.container.removeEventListener("click", this.hide, false); + this.editorInner.removeEventListener("click", stopPropagation, false); + + this.hide(false); + this.container.parentNode.removeChild(this.container); + } +}; \ No newline at end of file diff --git a/browser/devtools/markupview/markup-view.css b/browser/devtools/markupview/markup-view.css index 48cb796e5577..ad62a281710a 100644 --- a/browser/devtools/markupview/markup-view.css +++ b/browser/devtools/markupview/markup-view.css @@ -14,6 +14,32 @@ content: ""; display: block; clear: both; + position:relative; +} + +.html-editor { + display: none; + position: absolute; + z-index: 2; + + /* Use the same margin/padding trick used by .child tags to ensure that + * the editor covers up any content to the left (including expander arrows + * and hover effects). */ + margin-left: -1000em; + padding-left: 1000em; +} + +.html-editor-inner { + border: solid .1px; + flex: 1 1 auto; +} + +.html-editor iframe { + height: 100%; + width: 100%; + border: none; + margin: 0; + padding: 0; } .children { @@ -36,6 +62,11 @@ position: relative; } +.html-editor-container { + position: relative; + min-height: 200px; +} + /* This extra element placed in each tag is positioned absolutely to cover the * whole tag line and is used for background styling (when a selection is made * or when the tag is flashing) */ diff --git a/browser/devtools/markupview/markup-view.js b/browser/devtools/markupview/markup-view.js index a4832ec9fddb..aa1a88301e62 100644 --- a/browser/devtools/markupview/markup-view.js +++ b/browser/devtools/markupview/markup-view.js @@ -18,6 +18,7 @@ const CONTAINER_FLASHING_DURATION = 500; const {UndoStack} = require("devtools/shared/undo"); const {editableField, InplaceEditor} = require("devtools/shared/inplace-editor"); const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); +const {HTMLEditor} = require("devtools/markupview/html-editor"); const {OutputParser} = require("devtools/output-parser"); const promise = require("sdk/core/promise"); @@ -57,6 +58,7 @@ function MarkupView(aInspector, aFrame, aControllerWindow) { this.doc = this._frame.contentDocument; this._elt = this.doc.querySelector("#root"); this._outputParser = new OutputParser(); + this.htmlEditor = new HTMLEditor(this.doc); this.layoutHelpers = new LayoutHelpers(this.doc.defaultView); @@ -149,6 +151,7 @@ MarkupView.prototype = { * Highlight the inspector selected node. */ _onNewSelection: function() { + this.htmlEditor.hide(); let done = this._inspector.updating("markup-view"); if (this._inspector.selection.isNode()) { this.showNode(this._inspector.selection.nodeFront, true).then(() => { @@ -336,8 +339,8 @@ MarkupView.prototype = { } let node = aContainer.node; - this.markNodeAsSelected(node); - this._inspector.selection.setNodeFront(node, "treepanel"); + this.markNodeAsSelected(node, "treepanel"); + // This event won't be fired if the node is the same. But the highlighter // need to lock the node if it wasn't. this._inspector.selection.emit("new-node"); @@ -390,6 +393,9 @@ MarkupView.prototype = { */ _mutationObserver: function(aMutations) { let requiresLayoutChange = false; + let reselectParent; + let reselectChildIndex; + for (let mutation of aMutations) { let type = mutation.type; let target = mutation.target; @@ -418,20 +424,51 @@ MarkupView.prototype = { requiresLayoutChange = true; } } else if (type === "childList") { + let isFromOuterHTML = mutation.removed.some((n) => { + return n === this._outerHTMLNode; + }); + + // Keep track of which node should be reselected after mutations. + if (isFromOuterHTML) { + reselectParent = target; + reselectChildIndex = this._outerHTMLChildIndex; + + delete this._outerHTMLNode; + delete this._outerHTMLChildIndex; + } + container.childrenDirty = true; - // Update the children to take care of changes in the DOM - // Passing true as the last parameter asks for mutation flashing of the - // new nodes - this._updateChildren(container, {flash: true}); + // Update the children to take care of changes in the markup view DOM. + this._updateChildren(container, {flash: !isFromOuterHTML}); } } if (requiresLayoutChange) { this._inspector.immediateLayoutChange(); } - this._waitForChildren().then(() => { + this._waitForChildren().then((nodes) => { this._flashMutatedNodes(aMutations); - this._inspector.emit("markupmutation"); + this._inspector.emit("markupmutation", aMutations); + + // Since the htmlEditor is absolutely positioned, a mutation may change + // the location in which it should be shown. + this.htmlEditor.refresh(); + + // If a node has had its outerHTML set, the parent node will be selected. + // Reselect the original node immediately. + if (this._inspector.selection.nodeFront === reselectParent) { + this.walker.children(reselectParent).then((o) => { + let node = o.nodes[reselectChildIndex]; + let container = this._containers.get(node); + if (node && container) { + this.markNodeAsSelected(node, "outerhtml"); + if (container.hasChildren) { + this.expandNode(node); + } + } + }); + + } }); }, @@ -551,6 +588,94 @@ MarkupView.prototype = { container.expanded = false; }, + /** + * 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; + }, + + /** + * Retrieve the index of a child within its parent's children list. + * @param aNode The NodeFront to find the index of. + * @returns A promise that will be resolved with the integer index. + * If the child cannot be found, returns -1 + */ + getNodeChildIndex: function(aNode) { + let def = promise.defer(); + let parentNode = aNode.parentNode(); + + // Node may have been removed from the DOM, instead of throwing an error, + // return -1 indicating that it isn't inside of its parent children list. + if (!parentNode) { + def.resolve(-1); + } else { + this.walker.children(parentNode).then(children => { + def.resolve(children.nodes.indexOf(aNode)); + }); + } + + return def.promise; + }, + + /** + * Retrieve the index of a child within its parent's children collection. + * @param aNode The NodeFront to find the index of. + * @param newValue The new outerHTML to set on the node. + * @param oldValue The old outerHTML that will be reverted to find the index of. + * @returns A promise that will be resolved with the integer index. + * If the child cannot be found, returns -1 + */ + updateNodeOuterHTML: function(aNode, newValue, oldValue) { + let container = this._containers.get(aNode); + if (!container) { + return; + } + + this.getNodeChildIndex(aNode).then((i) => { + this._outerHTMLChildIndex = i; + this._outerHTMLNode = aNode; + + container.undo.do(() => { + this.walker.setOuterHTML(aNode, newValue); + }, () => { + this.walker.setOuterHTML(aNode, oldValue); + }); + }); + }, + + /** + * 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)=> { + let container = this._containers.get(aNode); + if (!container) { + return; + } + this.htmlEditor.show(container.tagLine, oldValue); + this.htmlEditor.once("popup-hidden", (e, aCommit, aValue) => { + if (aCommit) { + this.updateNodeOuterHTML(aNode, aValue, oldValue); + } + }); + }); + }, + + /** + * Mark the given node expanded. + * @param aNode The NodeFront to mark as expanded. + */ setNodeExpanded: function(aNode, aExpanded) { if (aExpanded) { this.expandNode(aNode); @@ -560,9 +685,11 @@ MarkupView.prototype = { }, /** - * Mark the given node selected. + * Mark the given node selected, and update the inspector.selection + * object's NodeFront to keep consistent state between UI and selection. + * @param aNode The NodeFront to mark as selected. */ - markNodeAsSelected: function(aNode) { + markNodeAsSelected: function(aNode, reason) { let container = this._containers.get(aNode); if (this._selectedContainer === container) { return false; @@ -575,6 +702,7 @@ MarkupView.prototype = { this._selectedContainer.selected = true; } + this._inspector.selection.setNodeFront(aNode, reason || "nodeselected"); return true; }, @@ -779,6 +907,9 @@ MarkupView.prototype = { destroy: function() { gDevTools.off("pref-changed", this._handlePrefChange); + this.htmlEditor.destroy(); + delete this.htmlEditor; + this.undo.destroy(); delete this.undo; diff --git a/browser/devtools/markupview/markup-view.xhtml b/browser/devtools/markupview/markup-view.xhtml index 3537f771d118..bf79e4acc136 100644 --- a/browser/devtools/markupview/markup-view.xhtml +++ b/browser/devtools/markupview/markup-view.xhtml @@ -11,7 +11,7 @@ - diff --git a/browser/devtools/markupview/test/browser.ini b/browser/devtools/markupview/test/browser.ini index 336e24ab6136..b278097ea240 100644 --- a/browser/devtools/markupview/test/browser.ini +++ b/browser/devtools/markupview/test/browser.ini @@ -6,6 +6,7 @@ support-files = head.js skip-if = true [browser_inspector_markup_edit.html] [browser_inspector_markup_edit.js] +[browser_inspector_markup_edit_outerhtml.js] [browser_inspector_markup_mutation.html] [browser_inspector_markup_mutation.js] [browser_inspector_markup_mutation_flashing.html] diff --git a/browser/devtools/markupview/test/browser_inspector_markup_edit_outerhtml.js b/browser/devtools/markupview/test/browser_inspector_markup_edit_outerhtml.js new file mode 100644 index 000000000000..bce0a1f62d30 --- /dev/null +++ b/browser/devtools/markupview/test/browser_inspector_markup_edit_outerhtml.js @@ -0,0 +1,295 @@ +/* Any copyright", " is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + + +function test() { + let inspector; + let doc; + + waitForExplicitFinish(); + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function onload() { + gBrowser.selectedBrowser.removeEventListener("load", onload, true); + doc = content.document; + waitForFocus(setupTest, content); + }, true); + + let outerHTMLs = [ + { + selector: "#one", + oldHTML: '
First Div
', + newHTML: '
First Div
', + validate: function(pageNode, selectedNode) { + is (pageNode.textContent, "First Div", "New div has expected text content"); + ok (!doc.querySelector("#one em"), "No em remaining") + } + }, + { + selector: "#removedChildren", + oldHTML: '
removedChild Italic Bold Underline Normal
', + newHTML: '
removedChild
' + }, + { + selector: "#addedChildren", + oldHTML: '
addedChildren
', + newHTML: '
addedChildren Italic Bold Underline Normal
' + }, + { + selector: "#addedAttribute", + oldHTML: '
addedAttribute
', + newHTML: '
addedAttribute
', + validate: function(pageNode, selectedNode) { + is (pageNode, selectedNode, "Original element is selected"); + is (pageNode.outerHTML, '
addedAttribute
', + "Attributes have been added"); + } + }, + { + selector: "#changedTag", + oldHTML: '
changedTag
', + newHTML: '

changedTag

' + }, + { + selector: "#badMarkup1", + oldHTML: '
badMarkup1
', + newHTML: '
badMarkup1
hanging', + validate: function(pageNode, selectedNode) { + is (pageNode, selectedNode, "Original element is selected"); + + let textNode = pageNode.nextSibling; + + is (textNode.nodeName, "#text", "Sibling is a text element"); + is (textNode.data, " hanging", "New text node has expected text content"); + } + }, + { + selector: "#badMarkup2", + oldHTML: '
badMarkup2
', + newHTML: '
badMarkup2
hanging
', + validate: function(pageNode, selectedNode) { + is (pageNode, selectedNode, "Original element is selected"); + + let textNode = pageNode.nextSibling; + + is (textNode.nodeName, "#text", "Sibling is a text element"); + is (textNode.data, " hanging", "New text node has expected text content"); + } + }, + { + selector: "#badMarkup3", + oldHTML: '
badMarkup3
', + newHTML: '
badMarkup3 Emphasized and strong
', + validate: function(pageNode, selectedNode) { + is (pageNode, selectedNode, "Original element is selected"); + + let em = doc.querySelector("#badMarkup3 em"); + let strong = doc.querySelector("#badMarkup3 strong"); + + is (em.textContent, "Emphasized and strong", " was auto created"); + is (strong.textContent, " and strong", " was auto created"); + } + }, + { + selector: "#badMarkup4", + oldHTML: '
badMarkup4
', + newHTML: '
badMarkup4

', + validate: function(pageNode, selectedNode) { + is (pageNode, selectedNode, "Original element is selected"); + + let div = doc.querySelector("#badMarkup4"); + let p = doc.querySelector("#badMarkup4 p"); + + is (div.textContent, "badMarkup4", "textContent is correct"); + is (div.tagName, "DIV", "did not change to

tag"); + is (p.textContent, "", "The

tag has no children"); + is (p.tagName, "P", "Created an empty

tag"); + } + }, + { + selector: "#badMarkup5", + oldHTML: '

badMarkup5

', + newHTML: '

badMarkup5

with a nested div

', + validate: function(pageNode, selectedNode) { + is (pageNode, selectedNode, "Original element is selected"); + + let p = doc.querySelector("#badMarkup5"); + let nodiv = doc.querySelector("#badMarkup5 div"); + let div = doc.querySelector("#badMarkup5 ~ div"); + + ok (!nodiv, "The invalid markup got created as a sibling"); + is (p.textContent, "badMarkup5 ", "The

tag does not take in the

content"); + is (p.tagName, "P", "Did not change to a
tag"); + is (div.textContent, "with a nested div", "textContent is correct"); + is (div.tagName, "DIV", "Did not change to

tag"); + } + }, + { + selector: "#siblings", + oldHTML: '

siblings
', + newHTML: '
before sibling
' + + '
siblings (updated)
' + + '
after sibling
', + validate: function(pageNode, selectedNode) { + let beforeSiblingNode = doc.querySelector("#siblings-before-sibling"); + let afterSiblingNode = doc.querySelector("#siblings-after-sibling"); + + is (beforeSiblingNode, selectedNode, "Sibling has been selected"); + is (pageNode.textContent, "siblings (updated)", "New div has expected text content"); + is (beforeSiblingNode.textContent, "before sibling", "Sibling has been inserted"); + is (afterSiblingNode.textContent, "after sibling", "Sibling has been inserted"); + } + } + ]; + content.location = "data:text/html," + + "" + + "" + + "" + + [outer.oldHTML for (outer of outerHTMLs) ].join("\n") + + "" + + ""; + + function setupTest() { + var target = TargetFactory.forTab(gBrowser.selectedTab); + gDevTools.showToolbox(target, "inspector").then(function(toolbox) { + inspector = toolbox.getCurrentPanel(); + inspector.once("inspector-updated", startTests); + }); + } + + function startTests() { + inspector.markup._frame.focus(); + nextStep(0); + } + + function nextStep(cursor) { + if (cursor >= outerHTMLs.length) { + testBody(); + return; + } + + let currentTestData = outerHTMLs[cursor]; + let selector = currentTestData.selector; + let oldHTML = currentTestData.oldHTML; + let newHTML = currentTestData.newHTML; + let rawNode = doc.querySelector(selector); + + inspector.selection.once("new-node", () => { + + let oldNodeFront = inspector.selection.nodeFront; + + // markupmutation fires once the outerHTML is set, with a target + // as the parent node and a type of "childList". + inspector.once("markupmutation", (e, aMutations) => { + + // Check to make the sure the correct mutation has fired, and that the + // parent is selected (this will be reset to the child once the mutation is complete. + let node = inspector.selection.node; + let nodeFront = inspector.selection.nodeFront; + let mutation = aMutations[0]; + let isFromOuterHTML = mutation.removed.some((n) => { + return n === oldNodeFront; + }); + + ok (isFromOuterHTML, "The node is in the 'removed' list of the mutation"); + is (mutation.type, "childList", "Mutation is a childList after updating outerHTML"); + is (mutation.target, nodeFront, "Parent node is selected immediately after setting outerHTML"); + + // Wait for node to be reselected after outerHTML has been set + inspector.selection.once("new-node", () => { + + // Typically selectedNode will === pageNode, but if a new element has been injected in front + // of it, this will not be the case. If this happens. + let selectedNode = inspector.selection.node; + let nodeFront = inspector.selection.nodeFront; + let pageNode = doc.querySelector(selector); + + if (currentTestData.validate) { + currentTestData.validate(pageNode, selectedNode); + } else { + is (pageNode, selectedNode, "Original node (grabbed by selector) is selected"); + is (pageNode.outerHTML, newHTML, "Outer HTML has been updated"); + } + + nextStep(cursor + 1); + }); + + }); + + is (inspector.selection.node, rawNode, "Selection is on the correct node"); + inspector.markup.updateNodeOuterHTML(inspector.selection.nodeFront, newHTML, oldHTML); + }); + + inspector.selection.setNode(rawNode); + } + + function testBody() { + let body = doc.querySelector("body"); + let bodyHTML = '

'; + let bodyFront = inspector.markup.walker.frontForRawNode(body); + inspector.once("markupmutation", (e, aMutations) => { + is (doc.querySelector("body").outerHTML, bodyHTML, " HTML has been updated"); + is (doc.querySelectorAll("head").length, 1, "no extra s have been added"); + testHead(); + }); + inspector.markup.updateNodeOuterHTML(bodyFront, bodyHTML, body.outerHTML); + } + + function testHead() { + let head = doc.querySelector("head"); + let headHTML = 'New Title'; + let headFront = inspector.markup.walker.frontForRawNode(head); + inspector.once("markupmutation", (e, aMutations) => { + is (doc.title, "New Title", "New title has been added"); + is (doc.defaultView.foo, undefined, "Script has not been executed"); + is (doc.querySelector("head").outerHTML, headHTML, " HTML has been updated"); + is (doc.querySelectorAll("body").length, 1, "no extra s have been added"); + testDocumentElement(); + }); + inspector.markup.updateNodeOuterHTML(headFront, headHTML, head.outerHTML); + } + + function testDocumentElement() { + let docElement = doc.documentElement; + let docElementHTML = 'Updated from document element

Hello

'; + let docElementFront = inspector.markup.walker.frontForRawNode(docElement); + inspector.once("markupmutation", (e, aMutations) => { + is (doc.title, "Updated from document element", "New title has been added"); + is (doc.defaultView.foo, undefined, "Script has not been executed"); + is (doc.documentElement.id, "updated", " ID has been updated"); + is (doc.documentElement.className, "", " class has been updated"); + is (doc.documentElement.getAttribute("foo"), "bar", " attribute has been updated"); + is (doc.documentElement.outerHTML, docElementHTML, " HTML has been updated"); + is (doc.querySelectorAll("head").length, 1, "no extra s have been added"); + is (doc.querySelectorAll("body").length, 1, "no extra s have been added"); + is (doc.body.textContent, "Hello", "document.body.textContent has been updated"); + testDocumentElement2(); + }); + inspector.markup.updateNodeOuterHTML(docElementFront, docElementHTML, docElement.outerHTML); + } + + function testDocumentElement2() { + let docElement = doc.documentElement; + let docElementHTML = 'Updated again from document element

Hello again

'; + let docElementFront = inspector.markup.walker.frontForRawNode(docElement); + inspector.once("markupmutation", (e, aMutations) => { + is (doc.title, "Updated again from document element", "New title has been added"); + is (doc.defaultView.foo, undefined, "Script has not been executed"); + is (doc.documentElement.id, "somethingelse", " ID has been updated"); + is (doc.documentElement.className, "updated", " class has been updated"); + is (doc.documentElement.getAttribute("foo"), null, " attribute has been removed"); + is (doc.documentElement.outerHTML, docElementHTML, " HTML has been updated"); + is (doc.querySelectorAll("head").length, 1, "no extra s have been added"); + is (doc.querySelectorAll("body").length, 1, "no extra s have been added"); + is (doc.body.textContent, "Hello again", "document.body.textContent has been updated"); + finishUp(); + }); + inspector.markup.updateNodeOuterHTML(docElementFront, docElementHTML, docElement.outerHTML); + } + + function finishUp() { + doc = inspector = null; + gBrowser.removeCurrentTab(); + finish(); + } +} diff --git a/browser/devtools/markupview/test/head.js b/browser/devtools/markupview/test/head.js index 7a9d43bb144b..66de55710f2d 100644 --- a/browser/devtools/markupview/test/head.js +++ b/browser/devtools/markupview/test/head.js @@ -6,6 +6,7 @@ const Cu = Components.utils; let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); let TargetFactory = devtools.TargetFactory; +let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {}); // Clear preferences that may be set during the course of tests. function clearUserPrefs() { diff --git a/browser/devtools/sourceeditor/codemirror/mozilla.css b/browser/devtools/sourceeditor/codemirror/mozilla.css index 69de3cbc0dd4..1217803e5fd8 100644 --- a/browser/devtools/sourceeditor/codemirror/mozilla.css +++ b/browser/devtools/sourceeditor/codemirror/mozilla.css @@ -23,8 +23,4 @@ .breakpoint.debugLocation { background-image: url("chrome://browser/skin/devtools/orion-debug-location.png"), url("chrome://browser/skin/devtools/orion-breakpoint.png"); -} - -.CodeMirror-activeline-background { - background: #e8f2ff; } \ No newline at end of file diff --git a/browser/devtools/sourceeditor/editor.js b/browser/devtools/sourceeditor/editor.js index 371ed6f90ef7..0435fc0d617f 100644 --- a/browser/devtools/sourceeditor/editor.js +++ b/browser/devtools/sourceeditor/editor.js @@ -27,12 +27,14 @@ const L10N = Services.strings.createBundle(L10N_BUNDLE); // order to initialize a CodeMirror instance. const CM_STYLES = [ + "chrome://browser/skin/devtools/common.css", "chrome://browser/content/devtools/codemirror/codemirror.css", "chrome://browser/content/devtools/codemirror/dialog.css", "chrome://browser/content/devtools/codemirror/mozilla.css" ]; const CM_SCRIPTS = [ + "chrome://browser/content/devtools/theme-switching.js", "chrome://browser/content/devtools/codemirror/codemirror.js", "chrome://browser/content/devtools/codemirror/dialog.js", "chrome://browser/content/devtools/codemirror/searchcursor.js", @@ -58,7 +60,7 @@ const CM_IFRAME = " " + [ " " for (style of CM_STYLES) ].join("\n") + " " + - " " + + " " + ""; const CM_MAPPING = [ @@ -74,7 +76,8 @@ const CM_MAPPING = [ "clearHistory", "openDialog", "cursorCoords", - "lineCount" + "lineCount", + "refresh" ]; const CM_JUMP_DIALOG = [ @@ -132,7 +135,8 @@ function Editor(config) { matchBrackets: true, extraKeys: {}, indentWithTabs: useTabs, - styleActiveLine: true + styleActiveLine: true, + theme: "mozilla" }; // Overwrite default config with user-provided, if needed. @@ -182,7 +186,7 @@ Editor.prototype = { let def = promise.defer(); let cm = editors.get(this); let doc = el.ownerDocument; - let env = doc.createElementNS(XUL_NS, "iframe"); + let env = doc.createElement("iframe"); env.flex = 1; if (cm) diff --git a/browser/locales/en-US/chrome/browser/devtools/inspector.dtd b/browser/locales/en-US/chrome/browser/devtools/inspector.dtd index 7b3236bf8708..4dd7d20dac7b 100644 --- a/browser/locales/en-US/chrome/browser/devtools/inspector.dtd +++ b/browser/locales/en-US/chrome/browser/devtools/inspector.dtd @@ -1,3 +1,6 @@ + + + diff --git a/browser/themes/shared/devtools/dark-theme.css b/browser/themes/shared/devtools/dark-theme.css index 84c0e7ca1280..243fef2bfb5f 100644 --- a/browser/themes/shared/devtools/dark-theme.css +++ b/browser/themes/shared/devtools/dark-theme.css @@ -8,7 +8,7 @@ */ .theme-body { background: #131c26; - color: #8fa1b2 + color: #8fa1b2; } .theme-twisty { @@ -47,7 +47,8 @@ background: #26394D; } -.theme-bg-darker { +.theme-bg-darker, +.cm-s-mozilla .CodeMirror-gutters { background-color: rgba(0,0,0,0.5); } @@ -55,11 +56,14 @@ background: #a18650; } -.theme-link { /* blue */ +.theme-link, +.cm-s-mozilla .cm-link { /* blue */ color: #3689b2; } -.theme-comment { /* grey */ +.theme-comment, +.cm-s-mozilla .cm-meta, +.cm-s-mozilla .cm-hr { /* grey */ color: #5c6773; } @@ -73,31 +77,51 @@ border-color: #303b47; } -.theme-fg-color1 { /* green */ +.theme-fg-color1, +.cm-s-mozilla .cm-variable-2, +.cm-s-mozilla .cm-quote, +.cm-s-mozilla .CodeMirror-matchingbracket { /* green */ color: #5c9966; } -.theme-fg-color2 { /* blue */ +.theme-fg-color2, +.cm-s-mozilla .cm-attribute, +.cm-s-mozilla .cm-builtin, +.cm-s-mozilla .cm-variable, +.cm-s-mozilla .cm-def, +.cm-s-mozilla .cm-variable-3, +.cm-s-mozilla .cm-property, +.cm-s-mozilla .cm-qualifier { /* blue */ color: #3689b2; } -.theme-fg-color3 { /* pink/lavender */ +.theme-fg-color3, +.cm-s-mozilla .cm-tag, +.cm-s-mozilla .cm-header { /* pink/lavender */ color: #a673bf; } -.theme-fg-color4 { /* purple/violet */ +.theme-fg-color4, +.cm-s-mozilla .cm-comment { /* purple/violet */ color: #6270b2; } -.theme-fg-color5 { /* Yellow */ +.theme-fg-color5, +.cm-s-mozilla .cm-bracket, +.cm-s-mozilla .cm-atom, +.cm-s-mozilla .cm-keyword { /* Yellow */ color: #a18650; } -.theme-fg-color6 { /* Orange */ +.theme-fg-color6, +.cm-s-mozilla .cm-string { /* Orange */ color: #b26b47; } -.theme-fg-color7 { /* Red */ +.theme-fg-color7, +.cm-s-mozilla .CodeMirror-nonmatchingbracket, +.cm-s-mozilla .cm-string-2, +.cm-s-mozilla .cm-error { /* Red */ color: #bf5656; } @@ -110,3 +134,41 @@ .markupview-colorswatch { box-shadow: 0 0 0 1px rgba(0,0,0,0.5); } + +/* CodeMirror specific styles. + * Best effort to match the existing theme, some of the colors + * are duplicated here to prevent weirdness in the main theme. */ + +.CodeMirror { /* Inherit platform specific font sizing and styles */ + font-family: inherit; + font-size: inherit; + background: transparent; +} + +.CodeMirror pre, +.cm-s-mozilla .cm-operator, +.cm-s-mozilla .cm-special, +.cm-s-mozilla .cm-number { /* theme-body color */ + color: #8fa1b2; +} + +.cm-s-mozilla .CodeMirror-lines .CodeMirror-cursor { + border-left: solid 1px #fff; +} + +.cm-s-mozilla.CodeMirror-focused .CodeMirror-selected { /* selected text (focused) */ + background: rgb(185, 215, 253); +} + +.dcm-s-mozilla .CodeMirror-selected { /* selected text (unfocused) */ + background: rgb(176, 176, 176); +} + +.CodeMirror-activeline-background { /* selected color with alpha */ + background: rgba(185, 215, 253, .05); +} + +.cm-s-markup-view pre { + line-height: 1.4em; + min-height: 1.4em; +} diff --git a/browser/themes/shared/devtools/light-theme.css b/browser/themes/shared/devtools/light-theme.css index d8a0fc913d16..a2cfd9caec09 100644 --- a/browser/themes/shared/devtools/light-theme.css +++ b/browser/themes/shared/devtools/light-theme.css @@ -47,7 +47,8 @@ background-color: #CCC; } -.theme-bg-darker { +.theme-bg-darker, +.cm-s-mozilla .CodeMirror-gutters { background: #EFEFEF; } @@ -55,11 +56,14 @@ background: #a18650; } -.theme-link { /* blue */ +.theme-link, +.cm-s-mozilla .cm-link { /* blue */ color: hsl(208,56%,40%); } -.theme-comment { /* grey */ +.theme-comment, +.cm-s-mozilla .cm-meta, +.cm-s-mozilla .cm-hr { /* grey */ color: hsl(90,2%,46%); } @@ -73,31 +77,51 @@ border-color: #cddae5; } -.theme-fg-color1 { /* green */ +.theme-fg-color1, +.cm-s-mozilla .cm-variable-2, +.cm-s-mozilla .cm-quote, +.cm-s-mozilla .CodeMirror-matchingbracket { /* green */ color: hsl(72,100%,27%); } -.theme-fg-color2 { /* blue */ +.theme-fg-color2, +.cm-s-mozilla .cm-attribute, +.cm-s-mozilla .cm-builtin, +.cm-s-mozilla .cm-variable, +.cm-s-mozilla .cm-def, +.cm-s-mozilla .cm-variable-3, +.cm-s-mozilla .cm-property, +.cm-s-mozilla .cm-qualifier { /* blue */ color: hsl(208,56%,40%); } -.theme-fg-color3 { /* dark blue */ +.theme-fg-color3, +.cm-s-mozilla .cm-tag, +.cm-s-mozilla .cm-header { /* dark blue */ color: hsl(208,81%,21%) } -.theme-fg-color4 { /* Orange */ +.theme-fg-color4, +.cm-s-mozilla .cm-comment { /* Orange */ color: hsl(24,85%,39%); } -.theme-fg-color5 { /* Yellow */ +.theme-fg-color5, +.cm-s-mozilla .cm-bracket, +.cm-s-mozilla .cm-keyword, +.cm-s-mozilla .cm-atom { /* Yellow */ color: #a18650; } -.theme-fg-color6 { /* Orange */ +.theme-fg-color6, +.cm-s-mozilla .cm-string { /* Orange */ color: hsl(24,85%,39%); } -.theme-fg-color7 { /* Red */ +.theme-fg-color7, +.cm-s-mozilla .CodeMirror-nonmatchingbracket, +.cm-s-mozilla .cm-string-2, +.cm-s-mozilla .cm-error { /* Red */ color: #bf5656; } @@ -110,3 +134,41 @@ .markupview-colorswatch { box-shadow: 0 0 0 1px #EFEFEF; } + +/* CodeMirror specific styles. + * Best effort to match the existing theme, some of the colors + * are duplicated here to prevent weirdness in the main theme. */ + +.CodeMirror { /* Inherit platform specific font sizing and styles */ + font-family: inherit; + font-size: inherit; + background: transparent; +} + +.CodeMirror pre, +.cm-s-mozilla .cm-operator, +.cm-s-mozilla .cm-special, +.cm-s-mozilla .cm-number { /* theme-body color */ + color: black; +} + +.cm-s-mozilla .CodeMirror-lines .CodeMirror-cursor { + border-left: solid 1px black; +} + +.cm-s-mozilla.CodeMirror-focused .CodeMirror-selected { /* selected text (focused) */ + background: rgb(185, 215, 253); +} + +.cm-s-mozilla .CodeMirror-selected { /* selected text (unfocused) */ + background: rgb(176, 176, 176); +} + +.CodeMirror-activeline-background { /* selected color with alpha */ + background: rgba(185, 215, 253, .4); +} + +.cm-s-markup-view pre { + line-height: 1.4em; + min-height: 1.4em; +}