Bug 895561 - 'Edit As HTML' option in the markup view - browser changes, r=jwalker

This commit is contained in:
Brian Grinstead 2013-10-24 08:41:03 -05:00
parent a89ef93583
commit fd589740da
14 changed files with 836 additions and 41 deletions

View File

@ -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.
*/

View File

@ -33,6 +33,10 @@
<popupset id="inspectorPopupSet">
<!-- Used by the Markup Panel, the Highlighter and the Breadcrumbs -->
<menupopup id="inspector-node-popup">
<menuitem id="node-menu-edithtml"
label="&inspectorHTMLEdit.label;"
accesskey="&inspectorHTMLEdit.accesskey;"
oncommand="inspector.editHTML()"/>
<menuitem id="node-menu-copyinner"
label="&inspectorHTMLCopyInner.label;"
accesskey="&inspectorHTMLCopyInner.accesskey;"

View File

@ -0,0 +1,182 @@
/* vim:set ts=2 sw=2 sts=2 et tw=80:
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {Cu} = require("chrome");
const Editor = require("devtools/sourceeditor/editor");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource:///modules/devtools/shared/event-emitter.js");
exports.HTMLEditor = HTMLEditor;
function ctrl(k) {
return (Services.appinfo.OS == "Darwin" ? "Cmd-" : "Ctrl-") + k;
}
function stopPropagation(e) {
e.stopPropagation();
}
/**
* A wrapper around the Editor component, that allows editing of HTML.
*
* The main functionality this provides around the Editor is the ability
* to show/hide/position an editor inplace. It only appends once to the
* body, and uses CSS to position the editor. The reason it is done this
* way is that the editor is loaded in an iframe, and calling appendChild
* causes it to reload.
*
* Meant to be embedded inside of an HTML page, as in markup-view.xhtml.
*
* @param HTMLDocument htmlDocument
* The document to attach the editor to. Will also use this
* document as a basis for listening resize events.
*/
function HTMLEditor(htmlDocument)
{
this.doc = htmlDocument;
this.container = this.doc.createElement("div");
this.container.className = "html-editor theme-body";
this.container.style.display = "none";
this.editorInner = this.doc.createElement("div");
this.editorInner.className = "html-editor-inner";
this.container.appendChild(this.editorInner);
this.doc.body.appendChild(this.container);
this.hide = this.hide.bind(this);
this.refresh = this.refresh.bind(this);
EventEmitter.decorate(this);
this.doc.defaultView.addEventListener("resize",
this.refresh, true);
let config = {
mode: Editor.modes.html,
lineWrapping: true,
styleActiveLine: false,
extraKeys: {},
theme: "mozilla markup-view"
};
config.extraKeys[ctrl("Enter")] = this.hide;
config.extraKeys["Esc"] = this.hide.bind(this, false);
this.container.addEventListener("click", this.hide, false);
this.editorInner.addEventListener("click", stopPropagation, false);
this.editor = new Editor(config);
this.editor.appendTo(this.editorInner).then(() => {
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);
}
};

View File

@ -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) */

View File

@ -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;

View File

@ -11,7 +11,7 @@
<link rel="stylesheet" href="chrome://browser/skin/devtools/markup-view.css" type="text/css"/>
<link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
<script type="application/javascript;version=1.8" src="theme-switching.js"/>
<script type="application/javascript;version=1.8" src="chrome://browser/content/devtools/theme-switching.js"></script>
</head>
<body class="theme-body devtools-monospace" role="application">

View File

@ -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]

View File

@ -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: '<div id="one">First <em>Div</em></div>',
newHTML: '<div id="one">First Div</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: '<div id="removedChildren">removedChild <i>Italic <b>Bold <u>Underline</u></b></i> Normal</div>',
newHTML: '<div id="removedChildren">removedChild</div>'
},
{
selector: "#addedChildren",
oldHTML: '<div id="addedChildren">addedChildren</div>',
newHTML: '<div id="addedChildren">addedChildren <i>Italic <b>Bold <u>Underline</u></b></i> Normal</div>'
},
{
selector: "#addedAttribute",
oldHTML: '<div id="addedAttribute">addedAttribute</div>',
newHTML: '<div id="addedAttribute" class="important" disabled checked>addedAttribute</div>',
validate: function(pageNode, selectedNode) {
is (pageNode, selectedNode, "Original element is selected");
is (pageNode.outerHTML, '<div id="addedAttribute" class="important" disabled="" checked="">addedAttribute</div>',
"Attributes have been added");
}
},
{
selector: "#changedTag",
oldHTML: '<div id="changedTag">changedTag</div>',
newHTML: '<p id="changedTag" class="important">changedTag</p>'
},
{
selector: "#badMarkup1",
oldHTML: '<div id="badMarkup1">badMarkup1</div>',
newHTML: '<div id="badMarkup1">badMarkup1</div> hanging</div>',
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: '<div id="badMarkup2">badMarkup2</div>',
newHTML: '<div id="badMarkup2">badMarkup2</div> hanging<div></div></div></div></body>',
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: '<div id="badMarkup3">badMarkup3</div>',
newHTML: '<div id="badMarkup3">badMarkup3 <em>Emphasized <strong> and strong</div>',
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", "<em> was auto created");
is (strong.textContent, " and strong", "<strong> was auto created");
}
},
{
selector: "#badMarkup4",
oldHTML: '<div id="badMarkup4">badMarkup4</div>',
newHTML: '<div id="badMarkup4">badMarkup4</p>',
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 <p> tag");
is (p.textContent, "", "The <p> tag has no children");
is (p.tagName, "P", "Created an empty <p> tag");
}
},
{
selector: "#badMarkup5",
oldHTML: '<p id="badMarkup5">badMarkup5</p>',
newHTML: '<p id="badMarkup5">badMarkup5 <div>with a nested div</div></p>',
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 <p> tag does not take in the <div> content");
is (p.tagName, "P", "Did not change to a <div> tag");
is (div.textContent, "with a nested div", "textContent is correct");
is (div.tagName, "DIV", "Did not change to <p> tag");
}
},
{
selector: "#siblings",
oldHTML: '<div id="siblings">siblings</div>',
newHTML: '<div id="siblings-before-sibling">before sibling</div>' +
'<div id="siblings">siblings (updated)</div>' +
'<div id="siblings-after-sibling">after sibling</div>',
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," +
"<!DOCTYPE html>" +
"<head><meta charset='utf-8' /></head>" +
"<body>" +
[outer.oldHTML for (outer of outerHTMLs) ].join("\n") +
"</body>" +
"</html>";
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 = '<body id="updated"><p></p></body>';
let bodyFront = inspector.markup.walker.frontForRawNode(body);
inspector.once("markupmutation", (e, aMutations) => {
is (doc.querySelector("body").outerHTML, bodyHTML, "<body> HTML has been updated");
is (doc.querySelectorAll("head").length, 1, "no extra <head>s have been added");
testHead();
});
inspector.markup.updateNodeOuterHTML(bodyFront, bodyHTML, body.outerHTML);
}
function testHead() {
let head = doc.querySelector("head");
let headHTML = '<head id="updated"><title>New Title</title><script>window.foo="bar";</script></head>';
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, "<head> HTML has been updated");
is (doc.querySelectorAll("body").length, 1, "no extra <body>s have been added");
testDocumentElement();
});
inspector.markup.updateNodeOuterHTML(headFront, headHTML, head.outerHTML);
}
function testDocumentElement() {
let docElement = doc.documentElement;
let docElementHTML = '<html id="updated" foo="bar"><head><title>Updated from document element</title><script>window.foo="bar";</script></head><body><p>Hello</p></body></html>';
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", "<html> ID has been updated");
is (doc.documentElement.className, "", "<html> class has been updated");
is (doc.documentElement.getAttribute("foo"), "bar", "<html> attribute has been updated");
is (doc.documentElement.outerHTML, docElementHTML, "<html> HTML has been updated");
is (doc.querySelectorAll("head").length, 1, "no extra <head>s have been added");
is (doc.querySelectorAll("body").length, 1, "no extra <body>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 = '<html class="updated" id="somethingelse"><head><title>Updated again from document element</title><script>window.foo="bar";</script></head><body><p>Hello again</p></body></html>';
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", "<html> ID has been updated");
is (doc.documentElement.className, "updated", "<html> class has been updated");
is (doc.documentElement.getAttribute("foo"), null, "<html> attribute has been removed");
is (doc.documentElement.outerHTML, docElementHTML, "<html> HTML has been updated");
is (doc.querySelectorAll("head").length, 1, "no extra <head>s have been added");
is (doc.querySelectorAll("body").length, 1, "no extra <body>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();
}
}

View File

@ -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() {

View File

@ -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;
}

View File

@ -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 =
" </style>" +
[ " <link rel='stylesheet' href='" + style + "'>" for (style of CM_STYLES) ].join("\n") +
" </head>" +
" <body></body>" +
" <body class='theme-body devtools-monospace'></body>" +
"</html>";
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)

View File

@ -1,3 +1,6 @@
<!ENTITY inspectorHTMLEdit.label "Edit As HTML">
<!ENTITY inspectorHTMLEdit.accesskey "E">
<!ENTITY inspectorHTMLCopyInner.label "Copy Inner HTML">
<!ENTITY inspectorHTMLCopyInner.accesskey "I">

View File

@ -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;
}

View File

@ -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;
}