Bug 1202458 - part1: inline text nodes in markupview only if they are short enough;r=pbro

The markup view will now inline a textnode in its container if and only if:
- the text node is the only child (pseudo elements included)
- the text node length is smaller than a predefined limit

If a container is expanded, its text nodes will now always be rendered in full,
no longer as a short version with an ellipsis. When selecting or navigating on
a textnode, the layout will no longer be modified on the fly.

MozReview-Commit-ID: HcDMqjbOesN

--HG--
extra : rebase_source : 908ba5c1cab86018116271402c90992c1fca6d62
extra : histedit_source : f0083b1e4c54cde0fdce12e1d139baa41e7d6cda
This commit is contained in:
Julian Descottes 2016-06-02 10:41:49 +02:00
parent b33a96b184
commit fc46d5012b
10 changed files with 259 additions and 156 deletions

View file

@ -162,9 +162,9 @@ Selection.prototype = {
setNodeFront: function (value, reason = "unknown") { setNodeFront: function (value, reason = "unknown") {
this.reason = reason; this.reason = reason;
// If a singleTextChild text node is being set, then set it's parent instead. // If an inlineTextChild text node is being set, then set it's parent instead.
let parentNode = value && value.parentNode(); let parentNode = value && value.parentNode();
if (value && parentNode && parentNode.singleTextChild === value) { if (value && parentNode && parentNode.inlineTextChild === value) {
value = parentNode; value = parentNode;
} }

View file

@ -44,8 +44,6 @@ const EventEmitter = require("devtools/shared/event-emitter");
const Heritage = require("sdk/core/heritage"); const Heritage = require("sdk/core/heritage");
const {parseAttribute} = const {parseAttribute} =
require("devtools/client/shared/node-attribute-parser"); require("devtools/client/shared/node-attribute-parser");
const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis",
Ci.nsIPrefLocalizedString).data;
const {Task} = require("devtools/shared/task"); const {Task} = require("devtools/shared/task");
const {scrollIntoViewIfNeeded} = require("devtools/shared/layout/utils"); const {scrollIntoViewIfNeeded} = require("devtools/shared/layout/utils");
const {PrefObserver} = require("devtools/client/styleeditor/utils"); const {PrefObserver} = require("devtools/client/styleeditor/utils");
@ -989,6 +987,10 @@ MarkupView.prototype = {
// accessibility where necessary. // accessibility where necessary.
this._updateChildren(container, {flash: true}).then(() => this._updateChildren(container, {flash: true}).then(() =>
container.updateLevel()); container.updateLevel());
} else if (type === "inlineTextChild") {
container.childrenDirty = true;
this._updateChildren(container, {flash: true});
container.update();
} }
} }
@ -1549,19 +1551,19 @@ MarkupView.prototype = {
return promise.resolve(container); return promise.resolve(container);
} }
if (container.singleTextChild if (container.inlineTextChild
&& container.singleTextChild != container.node.singleTextChild) { && container.inlineTextChild != container.node.inlineTextChild) {
// This container was doing double duty as a container for a single // This container was doing double duty as a container for a single
// text child, back that out. // text child, back that out.
this._containers.delete(container.singleTextChild); this._containers.delete(container.inlineTextChild);
container.clearSingleTextChild(); container.clearInlineTextChild();
if (container.hasChildren && container.selected) { if (container.hasChildren && container.selected) {
container.setExpanded(true); container.setExpanded(true);
} }
} }
if (container.node.singleTextChild) { if (container.node.inlineTextChild) {
container.setExpanded(false); container.setExpanded(false);
// this container will do double duty as the container for the single // this container will do double duty as the container for the single
// text child. // text child.
@ -1569,9 +1571,9 @@ MarkupView.prototype = {
container.children.removeChild(container.children.firstChild); container.children.removeChild(container.children.firstChild);
} }
container.setSingleTextChild(container.node.singleTextChild); container.setInlineTextChild(container.node.inlineTextChild);
this._containers.set(container.node.singleTextChild, container); this._containers.set(container.node.inlineTextChild, container);
container.childrenDirty = false; container.childrenDirty = false;
return promise.resolve(container); return promise.resolve(container);
} }
@ -2008,7 +2010,7 @@ MarkupContainer.prototype = {
* True if the current node can be expanded. * True if the current node can be expanded.
*/ */
get canExpand() { get canExpand() {
return this._hasChildren && !this.node.singleTextChild; return this._hasChildren && !this.node.inlineTextChild;
}, },
/** /**
@ -2717,13 +2719,13 @@ MarkupElementContainer.prototype = Heritage.extend(MarkupContainer.prototype, {
}); });
}, },
setSingleTextChild: function (singleTextChild) { setInlineTextChild: function (inlineTextChild) {
this.singleTextChild = singleTextChild; this.inlineTextChild = inlineTextChild;
this.editor.updateTextEditor(); this.editor.updateTextEditor();
}, },
clearSingleTextChild: function () { clearInlineTextChild: function () {
this.singleTextChild = undefined; this.inlineTextChild = undefined;
this.editor.updateTextEditor(); this.editor.updateTextEditor();
}, },
@ -2905,25 +2907,14 @@ TextEditor.prototype = {
}, },
update: function () { update: function () {
if (!this.selected || !this.node.incompleteValue) { let longstr = null;
let text = this.node.shortValue; this.node.getNodeValue().then(ret => {
if (this.node.incompleteValue) { longstr = ret;
text += ELLIPSIS; return longstr.string();
} }).then(str => {
this.value.textContent = text; longstr.release().then(null, console.error);
} else { this.value.textContent = str;
let longstr = null; }).then(null, console.error);
this.node.getNodeValue().then(ret => {
longstr = ret;
return longstr.string();
}).then(str => {
longstr.release().then(null, console.error);
if (this.selected) {
this.value.textContent = str;
this.markup.emit("text-expand");
}
}).then(null, console.error);
}
}, },
destroy: function () {}, destroy: function () {},
@ -3114,7 +3105,7 @@ ElementEditor.prototype = {
* Update the inline text editor in case of a single text child node. * Update the inline text editor in case of a single text child node.
*/ */
updateTextEditor: function () { updateTextEditor: function () {
let node = this.node.singleTextChild; let node = this.node.inlineTextChild;
if (this.textEditor && this.textEditor.node != node) { if (this.textEditor && this.textEditor.node != node) {
this.elt.removeChild(this.textEditor.elt); this.elt.removeChild(this.textEditor.elt);

View file

@ -132,6 +132,7 @@ skip-if = e10s # Bug 1036409 - The last selected node isn't reselected
[browser_markup_tag_edit_11.js] [browser_markup_tag_edit_11.js]
[browser_markup_tag_edit_12.js] [browser_markup_tag_edit_12.js]
[browser_markup_tag_edit_13-other.js] [browser_markup_tag_edit_13-other.js]
[browser_markup_textcontent_display.js]
[browser_markup_textcontent_edit_01.js] [browser_markup_textcontent_edit_01.js]
[browser_markup_textcontent_edit_02.js] [browser_markup_textcontent_edit_02.js]
[browser_markup_toggle_01.js] [browser_markup_toggle_01.js]

View file

@ -89,7 +89,7 @@ const TEST_DATA = [
}, },
check: function* (inspector) { check: function* (inspector) {
let container = yield getContainerForSelector("#node1", inspector); let container = yield getContainerForSelector("#node1", inspector);
ok(container.singleTextChild, "Has single text child."); ok(container.inlineTextChild, "Has single text child.");
} }
}, },
{ {
@ -99,9 +99,9 @@ const TEST_DATA = [
}, },
check: function* (inspector) { check: function* (inspector) {
let container = yield getContainerForSelector("#node1", inspector); let container = yield getContainerForSelector("#node1", inspector);
ok(container.singleTextChild, "Has single text child."); ok(container.inlineTextChild, "Has single text child.");
ok(!container.canExpand, "Can't expand container with singleTextChild."); ok(!container.canExpand, "Can't expand container with inlineTextChild.");
ok(!container.singleTextChild.canExpand, "Can't expand singleTextChild."); ok(!container.inlineTextChild.canExpand, "Can't expand inlineTextChild.");
is(container.editor.elt.querySelector(".text").textContent.trim(), is(container.editor.elt.querySelector(".text").textContent.trim(),
"newtext", "Single text child editor updated."); "newtext", "Single text child editor updated.");
} }
@ -117,7 +117,7 @@ const TEST_DATA = [
}, },
check: function* (inspector) { check: function* (inspector) {
let container = yield getContainerForSelector("#node1", inspector); let container = yield getContainerForSelector("#node1", inspector);
ok(!container.singleTextChild, "Does not have single text child."); ok(!container.inlineTextChild, "Does not have single text child.");
ok(container.canExpand, "Can expand container with child nodes."); ok(container.canExpand, "Can expand container with child nodes.");
ok(container.editor.elt.querySelector(".text") == null, ok(container.editor.elt.querySelector(".text") == null,
"Single text child editor removed."); "Single text child editor removed.");
@ -130,9 +130,9 @@ const TEST_DATA = [
}, },
check: function* (inspector) { check: function* (inspector) {
let container = yield getContainerForSelector("#node1", inspector); let container = yield getContainerForSelector("#node1", inspector);
ok(container.singleTextChild, "Has single text child."); ok(container.inlineTextChild, "Has single text child.");
ok(!container.canExpand, "Can't expand container with singleTextChild."); ok(!container.canExpand, "Can't expand container with inlineTextChild.");
ok(!container.singleTextChild.canExpand, "Can't expand singleTextChild."); ok(!container.inlineTextChild.canExpand, "Can't expand inlineTextChild.");
ok(container.editor.elt.querySelector(".text").textContent.trim(), ok(container.editor.elt.querySelector(".text").textContent.trim(),
"newtext", "Single text child editor updated."); "newtext", "Single text child editor updated.");
}, },
@ -144,7 +144,7 @@ const TEST_DATA = [
}, },
check: function* (inspector) { check: function* (inspector) {
let container = yield getContainerForSelector("#node1", inspector); let container = yield getContainerForSelector("#node1", inspector);
ok(!container.singleTextChild, "Does not have single text child."); ok(!container.inlineTextChild, "Does not have single text child.");
ok(!container.canExpand, "Can't expand empty container."); ok(!container.canExpand, "Can't expand empty container.");
ok(container.editor.elt.querySelector(".text") == null, ok(container.editor.elt.querySelector(".text") == null,
"Single text child editor removed."); "Single text child editor removed.");
@ -157,9 +157,9 @@ const TEST_DATA = [
}, },
check: function* (inspector) { check: function* (inspector) {
let container = yield getContainerForSelector("#node1", inspector); let container = yield getContainerForSelector("#node1", inspector);
ok(container.singleTextChild, "Has single text child."); ok(container.inlineTextChild, "Has single text child.");
ok(!container.canExpand, "Can't expand container with singleTextChild."); ok(!container.canExpand, "Can't expand container with inlineTextChild.");
ok(!container.singleTextChild.canExpand, "Can't expand singleTextChild."); ok(!container.inlineTextChild.canExpand, "Can't expand inlineTextChild.");
ok(container.editor.elt.querySelector(".text").textContent.trim(), ok(container.editor.elt.querySelector(".text").textContent.trim(),
"newtext", "Single text child editor updated."); "newtext", "Single text child editor updated.");
}, },

View file

@ -0,0 +1,89 @@
/* 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 the rendering of text nodes in the markup view.
const LONG_VALUE = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do " +
"eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.";
const SCHEMA = "data:text/html;charset=UTF-8,";
const TEST_URL = `${SCHEMA}<!DOCTYPE html>
<html>
<body>
<div id="shorttext">Short text</div>
<div id="longtext">${LONG_VALUE}</div>
<div id="shortcomment"><!--Short comment--></div>
<div id="longcomment"><!--${LONG_VALUE}--></div>
<div id="shorttext-and-node">Short text<span>Other element</span></div>
<div id="longtext-and-node">${LONG_VALUE}<span>Other element</span></div>
</body>
</html>`;
const TEST_DATA = [{
desc: "Test node containing a short text, short text nodes can be inlined.",
selector: "#shorttext",
inline: true,
value: "Short text",
}, {
desc: "Test node containing a long text, long text nodes are not inlined.",
selector: "#longtext",
inline: false,
value: LONG_VALUE,
}, {
desc: "Test node containing a short comment, comments are not inlined.",
selector: "#shortcomment",
inline: false,
value: "Short comment",
}, {
desc: "Test node containing a long comment, comments are not inlined.",
selector: "#longcomment",
inline: false,
value: LONG_VALUE,
}, {
desc: "Test node containing a short text and a span.",
selector: "#shorttext-and-node",
inline: false,
value: "Short text",
}, {
desc: "Test node containing a long text and a span.",
selector: "#longtext-and-node",
inline: false,
value: LONG_VALUE,
}, ];
add_task(function* () {
let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
for (let data of TEST_DATA) {
yield checkNode(inspector, testActor, data);
}
});
function* checkNode(inspector, testActor, {desc, selector, inline, value}) {
info(desc);
let container = yield getContainerForSelector(selector, inspector);
let nodeValue = yield getFirstChildNodeValue(selector, testActor);
is(nodeValue, value, "The test node's text content is correct");
is(!!container.inlineTextChild, inline, "Container inlineTextChild is as expected");
is(!container.canExpand, inline, "Container canExpand property is as expected");
let textContainer;
if (inline) {
textContainer = container.elt.querySelector("pre");
ok(!!textContainer, "Text container is already rendered for inline text elements");
} else {
textContainer = container.elt.querySelector("pre");
ok(!textContainer, "Text container is not rendered for collapsed text nodes");
yield inspector.markup.expandNode(container.node);
yield waitForMultipleChildrenUpdates(inspector);
textContainer = container.elt.querySelector("pre");
ok(!!textContainer, "Text container is rendered after expanding the container");
}
is(textContainer.textContent, value, "The complete text node is rendered.");
}

View file

@ -7,6 +7,7 @@
// Test editing a node's text content // Test editing a node's text content
const TEST_URL = URL_ROOT + "doc_markup_edit.html"; const TEST_URL = URL_ROOT + "doc_markup_edit.html";
const {DEFAULT_VALUE_SUMMARY_LENGTH} = require("devtools/server/actors/inspector");
add_task(function* () { add_task(function* () {
let {inspector, testActor} = yield openInspectorForURL(TEST_URL); let {inspector, testActor} = yield openInspectorForURL(TEST_URL);
@ -26,55 +27,38 @@ add_task(function* () {
newValue: "LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT. " + newValue: "LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT. " +
"DONEC POSUERE PLACERAT MAGNA ET IMPERDIET.", "DONEC POSUERE PLACERAT MAGNA ET IMPERDIET.",
oldValue: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + oldValue: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
"Donec posuere placerat magna et imperdiet.", "Donec posuere placerat magna et imperdiet."
shortValue: true
}); });
yield editContainer(inspector, testActor, { yield editContainer(inspector, testActor, {
selector: "#node17", selector: "#node17",
newValue: "New value", newValue: "New value",
oldValue: "LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT. " + oldValue: "LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT. " +
"DONEC POSUERE PLACERAT MAGNA ET IMPERDIET."
});
yield editContainer(inspector, testActor, {
selector: "#node17",
newValue: "LOREM IPSUM DOLOR SIT AMET, CONSECTETUR ADIPISCING ELIT. " +
"DONEC POSUERE PLACERAT MAGNA ET IMPERDIET.", "DONEC POSUERE PLACERAT MAGNA ET IMPERDIET.",
shortValue: true oldValue: "New value"
}); });
}); });
function* getNodeValue(selector, testActor) {
let nodeValue = yield testActor.eval(`
content.document.querySelector("${selector}").firstChild.nodeValue;
`);
return nodeValue;
}
function* editContainer(inspector, testActor, function* editContainer(inspector, testActor,
{selector, newValue, oldValue, shortValue}) { {selector, newValue, oldValue}) {
let nodeValue = yield getNodeValue(selector, testActor); let nodeValue = yield getFirstChildNodeValue(selector, testActor);
is(nodeValue, oldValue, "The test node's text content is correct"); is(nodeValue, oldValue, "The test node's text content is correct");
info("Changing the text content"); info("Changing the text content");
let onMutated = inspector.once("markupmutation"); let onMutated = inspector.once("markupmutation");
let container = yield focusNode(selector, inspector); let container = yield focusNode(selector, inspector);
let isOldValueInline = oldValue.length <= DEFAULT_VALUE_SUMMARY_LENGTH;
is(!!container.inlineTextChild, isOldValueInline, "inlineTextChild is as expected");
is(!container.canExpand, isOldValueInline, "canExpand property is as expected");
let field = container.elt.querySelector("pre"); let field = container.elt.querySelector("pre");
if (shortValue) {
is(oldValue.indexOf(
field.textContent.substring(0, field.textContent.length - 1)),
0,
"The shortened value starts with the full value " + field.textContent);
ok(oldValue.length > field.textContent.length,
"The shortened value is short");
} else {
is(field.textContent, oldValue,
"The text node has the correct original value");
}
inspector.markup.markNodeAsSelected(container.node);
if (shortValue) {
info("Waiting for the text to be updated");
yield inspector.markup.once("text-expand");
}
is(field.textContent, oldValue, is(field.textContent, oldValue,
"The text node has the correct original value after selecting"); "The text node has the correct original value after selecting");
setEditableFieldValue(field, newValue, inspector); setEditableFieldValue(field, newValue, inspector);
@ -82,9 +66,18 @@ function* editContainer(inspector, testActor,
info("Listening to the markupmutation event"); info("Listening to the markupmutation event");
yield onMutated; yield onMutated;
nodeValue = yield getNodeValue(selector, testActor); nodeValue = yield getFirstChildNodeValue(selector, testActor);
is(nodeValue, newValue, "The test node's text content has changed"); is(nodeValue, newValue, "The test node's text content has changed");
let isNewValueInline = newValue.length <= DEFAULT_VALUE_SUMMARY_LENGTH;
is(!!container.inlineTextChild, isNewValueInline, "inlineTextChild is as expected");
is(!container.canExpand, isNewValueInline, "canExpand property is as expected");
if (isOldValueInline != isNewValueInline) {
is(container.expanded, !isNewValueInline,
"Container was automatically expanded/collapsed");
}
info("Selecting the <body> to reset the selection"); info("Selecting the <body> to reset the selection");
let bodyContainer = yield getContainerForSelector("body", inspector); let bodyContainer = yield getContainerForSelector("body", inspector);
inspector.markup.markNodeAsSelected(bodyContainer.node); inspector.markup.markNodeAsSelected(bodyContainer.node);

View file

@ -17,7 +17,7 @@ add_task(function* () {
yield inspector.markup.expandAll(); yield inspector.markup.expandAll();
yield waitForMultipleChildrenUpdates(inspector); yield waitForMultipleChildrenUpdates(inspector);
let nodeValue = yield getNodeValue(SELECTOR, testActor); let nodeValue = yield getFirstChildNodeValue(SELECTOR, testActor);
let expectedValue = "line6"; let expectedValue = "line6";
is(nodeValue, expectedValue, "The test node's text content is correct"); is(nodeValue, expectedValue, "The test node's text content is correct");
@ -84,17 +84,10 @@ add_task(function* () {
yield sendKey("VK_RETURN", {}, editor, inspector.panelWin); yield sendKey("VK_RETURN", {}, editor, inspector.panelWin);
yield onMutated; yield onMutated;
nodeValue = yield getNodeValue(SELECTOR, testActor); nodeValue = yield getFirstChildNodeValue(SELECTOR, testActor);
is(nodeValue, expectedValue, "The test node's text content is correct"); is(nodeValue, expectedValue, "The test node's text content is correct");
}); });
function* getNodeValue(selector, testActor) {
let nodeValue = yield testActor.eval(`
content.document.querySelector("${selector}").firstChild.nodeValue;
`);
return nodeValue;
}
/** /**
* Check that the editor selection is at the expected positions. * Check that the editor selection is at the expected positions.
*/ */

View file

@ -89,6 +89,20 @@ var getContainerForSelector = Task.async(function* (selector, inspector) {
return container; return container;
}); });
/**
* Retrieve the nodeValue for the firstChild of a provided selector on the content page.
*
* @param {String} selector
* @param {TestActorFront} testActor The current TestActorFront instance.
* @return {String} the nodeValue of the first
*/
function* getFirstChildNodeValue(selector, testActor) {
let nodeValue = yield testActor.eval(`
content.document.querySelector("${selector}").firstChild.nodeValue;
`);
return nodeValue;
}
/** /**
* Using the markupview's _waitForChildren function, wait for all queued * Using the markupview's _waitForChildren function, wait for all queued
* children updates to be handled. * children updates to be handled.

View file

@ -248,7 +248,7 @@ var NodeActor = exports.NodeActor = protocol.ActorClassWithSpec(nodeSpec, {
} }
let parentNode = this.walker.parentNode(this); let parentNode = this.walker.parentNode(this);
let singleTextChild = this.walker.singleTextChild(this); let inlineTextChild = this.walker.inlineTextChild(this);
let form = { let form = {
actor: this.actorID, actor: this.actorID,
@ -257,9 +257,10 @@ var NodeActor = exports.NodeActor = protocol.ActorClassWithSpec(nodeSpec, {
nodeType: this.rawNode.nodeType, nodeType: this.rawNode.nodeType,
namespaceURI: this.rawNode.namespaceURI, namespaceURI: this.rawNode.namespaceURI,
nodeName: this.rawNode.nodeName, nodeName: this.rawNode.nodeName,
nodeValue: this.rawNode.nodeValue,
displayName: getNodeDisplayName(this.rawNode), displayName: getNodeDisplayName(this.rawNode),
numChildren: this.numChildren, numChildren: this.numChildren,
singleTextChild: singleTextChild ? singleTextChild.form() : undefined, inlineTextChild: inlineTextChild ? inlineTextChild.form() : undefined,
// doctype attributes // doctype attributes
name: this.rawNode.name, name: this.rawNode.name,
@ -285,18 +286,6 @@ var NodeActor = exports.NodeActor = protocol.ActorClassWithSpec(nodeSpec, {
form.isDocumentElement = true; form.isDocumentElement = true;
} }
if (this.rawNode.nodeValue) {
// We only include a short version of the value if it's longer than
// gValueSummaryLength
if (this.rawNode.nodeValue.length > gValueSummaryLength) {
form.shortValue = this.rawNode.nodeValue
.substring(0, gValueSummaryLength);
form.incompleteValue = true;
} else {
form.shortValue = this.rawNode.nodeValue;
}
}
// Add an extra API for custom properties added by other // Add an extra API for custom properties added by other
// modules/extensions. // modules/extensions.
form.setFormProperty = (name, value) => { form.setFormProperty = (name, value) => {
@ -330,6 +319,7 @@ var NodeActor = exports.NodeActor = protocol.ActorClassWithSpec(nodeSpec, {
nativeAnonymousChildList: true, nativeAnonymousChildList: true,
attributes: true, attributes: true,
characterData: true, characterData: true,
characterDataOldValue: true,
childList: true, childList: true,
subtree: true subtree: true
}); });
@ -1147,12 +1137,12 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
}, },
/** /**
* If the given NodeActor only has a single text node as a child, * If the given NodeActor only has a single text node as a child with a text
* return that child's NodeActor. * content small enough to be inlined, return that child's NodeActor.
* *
* @param NodeActor node * @param NodeActor node
*/ */
singleTextChild: function (node) { inlineTextChild: function (node) {
// Quick checks to prevent creating a new walker if possible. // Quick checks to prevent creating a new walker if possible.
if (node.isBeforePseudoElement || if (node.isBeforePseudoElement ||
node.isAfterPseudoElement || node.isAfterPseudoElement ||
@ -1164,11 +1154,15 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
let docWalker = this.getDocumentWalker(node.rawNode); let docWalker = this.getDocumentWalker(node.rawNode);
let firstChild = docWalker.firstChild(); let firstChild = docWalker.firstChild();
// If the first child isn't a text node, or there are multiple children // Bail out if:
// then bail out // - more than one child
// - unique child is not a text node
// - unique child is a text node, but is too long to be inlined
if (!firstChild || if (!firstChild ||
docWalker.nextSibling() ||
firstChild.nodeType !== Ci.nsIDOMNode.TEXT_NODE || firstChild.nodeType !== Ci.nsIDOMNode.TEXT_NODE ||
docWalker.nextSibling()) { firstChild.nodeValue.length > gValueSummaryLength
) {
return undefined; return undefined;
} }
@ -2200,8 +2194,7 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
* newValue: <string> - The new value of the attribute, if any. * newValue: <string> - The new value of the attribute, if any.
* *
* `characterData` type: * `characterData` type:
* newValue: <string> - the new shortValue for the node * newValue: <string> - the new nodeValue for the node
* [incompleteValue: true] - True if the shortValue was truncated.
* *
* `childList` type is returned when the set of children for a node * `childList` type is returned when the set of children for a node
* has changed. Includes extra data, which can be used by the client to * has changed. Includes extra data, which can be used by the client to
@ -2211,7 +2204,7 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
* seen by the client* that were added to the target node. * seen by the client* that were added to the target node.
* removed: array of <domnode actor ID> The list of actors *previously * removed: array of <domnode actor ID> The list of actors *previously
* seen by the client* that were removed from the target node. * seen by the client* that were removed from the target node.
* singleTextChild: If the node now has a single text child, it will * inlineTextChild: If the node now has a single text child, it will
* be sent here. * be sent here.
* *
* Actors that are included in a MutationRecord's `removed` but * Actors that are included in a MutationRecord's `removed` but
@ -2288,13 +2281,8 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
targetNode.getAttribute(mutation.attributeName) targetNode.getAttribute(mutation.attributeName)
: null; : null;
} else if (type === "characterData") { } else if (type === "characterData") {
if (targetNode.nodeValue.length > gValueSummaryLength) { mutation.newValue = targetNode.nodeValue;
mutation.newValue = targetNode.nodeValue this._maybeQueueInlineTextChildMutation(change, targetNode);
.substring(0, gValueSummaryLength);
mutation.incompleteValue = true;
} else {
mutation.newValue = targetNode.nodeValue;
}
} else if (type === "childList" || type === "nativeAnonymousChildList") { } else if (type === "childList" || type === "nativeAnonymousChildList") {
// Get the list of removed and added actors that the client has seen // Get the list of removed and added actors that the client has seen
// so that it can keep its ownership tree up to date. // so that it can keep its ownership tree up to date.
@ -2330,15 +2318,50 @@ var WalkerActor = protocol.ActorClassWithSpec(walkerSpec, {
mutation.removed = removedActors; mutation.removed = removedActors;
mutation.added = addedActors; mutation.added = addedActors;
let singleTextChild = this.singleTextChild(targetActor); let inlineTextChild = this.inlineTextChild(targetActor);
if (singleTextChild) { if (inlineTextChild) {
mutation.singleTextChild = singleTextChild.form(); mutation.inlineTextChild = inlineTextChild.form();
} }
} }
this.queueMutation(mutation); this.queueMutation(mutation);
} }
}, },
/**
* Check if the provided mutation could change the way the target element is
* inlined with its parent node. If it might, a custom mutation of type
* "inlineTextChild" will be queued.
*
* @param {MutationRecord} mutation
* A characterData type mutation
*/
_maybeQueueInlineTextChildMutation: function (mutation) {
let {oldValue, target} = mutation;
let newValue = target.nodeValue;
let limit = gValueSummaryLength;
if ((oldValue.length <= limit && newValue.length <= limit) ||
(oldValue.length > limit && newValue.length > limit)) {
// Bail out if the new & old values are both below/above the size limit.
return;
}
let parentActor = this.getNode(target.parentNode);
if (!parentActor || parentActor.rawNode.children.length > 0) {
// If the parent node has other children, a character data mutation will
// not change anything regarding inlining text nodes.
return;
}
let inlineTextChild = this.inlineTextChild(parentActor);
this.queueMutation({
type: "inlineTextChild",
target: parentActor.actorID,
inlineTextChild:
inlineTextChild ? inlineTextChild.form() : undefined
});
},
onFrameLoad: function ({ window, isTopLevel }) { onFrameLoad: function ({ window, isTopLevel }) {
if (!this.rootDoc && isTopLevel) { if (!this.rootDoc && isTopLevel) {
this.rootDoc = window.document; this.rootDoc = window.document;

View file

@ -3,7 +3,6 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict"; "use strict";
const Services = require("Services");
const { Ci } = require("chrome"); const { Ci } = require("chrome");
require("devtools/shared/fronts/styles"); require("devtools/shared/fronts/styles");
require("devtools/shared/fronts/highlighters"); require("devtools/shared/fronts/highlighters");
@ -15,7 +14,6 @@ const {
preEvent, preEvent,
types types
} = require("devtools/shared/protocol.js"); } = require("devtools/shared/protocol.js");
const { makeInfallible } = require("devtools/shared/DevToolsUtils");
const { const {
inspectorSpec, inspectorSpec,
nodeSpec, nodeSpec,
@ -71,15 +69,6 @@ const AttributeModificationList = Class({
} }
}); });
// A resolve that hits the main loop first.
function delayedResolve(value) {
let deferred = promise.defer();
Services.tm.mainThread.dispatch(makeInfallible(() => {
deferred.resolve(value);
}), 0);
return deferred.promise;
}
/** /**
* Client side of the node actor. * Client side of the node actor.
* *
@ -122,6 +111,14 @@ const NodeFront = FrontClassWithSpec(nodeSpec, {
this.actorID = form; this.actorID = form;
return; return;
} }
// backward-compatibility: shortValue indicates we are connected to old server
if (form.shortValue) {
// If the value is not complete, set nodeValue to null, it will be fetched
// when calling getNodeValue()
form.nodeValue = form.incompleteValue ? null : form.shortValue;
}
// Shallow copy of the form. We could just store a reference, but // Shallow copy of the form. We could just store a reference, but
// eventually we'll want to update some of the data. // eventually we'll want to update some of the data.
this._form = object.merge(form); this._form = object.merge(form);
@ -135,11 +132,11 @@ const NodeFront = FrontClassWithSpec(nodeSpec, {
this.reparent(parentNodeFront); this.reparent(parentNodeFront);
} }
if (form.singleTextChild) { if (form.inlineTextChild) {
this.singleTextChild = this.inlineTextChild =
types.getType("domnode").read(form.singleTextChild, ctx); types.getType("domnode").read(form.inlineTextChild, ctx);
} else { } else {
this.singleTextChild = undefined; this.inlineTextChild = undefined;
} }
}, },
@ -186,8 +183,7 @@ const NodeFront = FrontClassWithSpec(nodeSpec, {
}); });
} }
} else if (change.type === "characterData") { } else if (change.type === "characterData") {
this._form.shortValue = change.newValue; this._form.nodeValue = change.newValue;
this._form.incompleteValue = change.incompleteValue;
} else if (change.type === "pseudoClassLock") { } else if (change.type === "pseudoClassLock") {
this._form.pseudoClassLocks = change.pseudoClassLocks; this._form.pseudoClassLocks = change.pseudoClassLocks;
} else if (change.type === "events") { } else if (change.type === "events") {
@ -259,12 +255,6 @@ const NodeFront = FrontClassWithSpec(nodeSpec, {
get tagName() { get tagName() {
return this.nodeType === Ci.nsIDOMNode.ELEMENT_NODE ? this.nodeName : null; return this.nodeType === Ci.nsIDOMNode.ELEMENT_NODE ? this.nodeName : null;
}, },
get shortValue() {
return this._form.shortValue;
},
get incompleteValue() {
return !!this._form.incompleteValue;
},
get isDocumentElement() { get isDocumentElement() {
return !!this._form.isDocumentElement; return !!this._form.isDocumentElement;
@ -324,11 +314,14 @@ const NodeFront = FrontClassWithSpec(nodeSpec, {
}, },
getNodeValue: custom(function () { getNodeValue: custom(function () {
if (!this.incompleteValue) { // backward-compatibility: if nodevalue is null and shortValue is defined, the actual
return delayedResolve(new ShortLongString(this.shortValue)); // value of the node needs to be fetched on the server.
if (this._form.nodeValue === null && this._form.shortValue) {
return this._getNodeValue();
} }
return this._getNodeValue(); let str = this._form.nodeValue || "";
return promise.resolve(new ShortLongString(str));
}, { }, {
impl: "_getNodeValue" impl: "_getNodeValue"
}), }),
@ -806,13 +799,6 @@ const WalkerFront = FrontClassWithSpec(walkerSpec, {
addedFronts.push(addedFront); addedFronts.push(addedFront);
} }
if (change.singleTextChild) {
targetFront.singleTextChild =
types.getType("domnode").read(change.singleTextChild, this);
} else {
targetFront.singleTextChild = undefined;
}
// Before passing to users, replace the added and removed actor // Before passing to users, replace the added and removed actor
// ids with front in the mutation record. // ids with front in the mutation record.
emittedMutation.added = addedFronts; emittedMutation.added = addedFronts;
@ -858,6 +844,19 @@ const WalkerFront = FrontClassWithSpec(walkerSpec, {
targetFront.updateMutation(change); targetFront.updateMutation(change);
} }
// Update the inlineTextChild property of the target for a selected list of
// mutation types.
if (change.type === "inlineTextChild" ||
change.type === "childList" ||
change.type === "nativeAnonymousChildList") {
if (change.inlineTextChild) {
targetFront.inlineTextChild =
types.getType("domnode").read(change.inlineTextChild, this);
} else {
targetFront.inlineTextChild = undefined;
}
}
emitMutations.push(emittedMutation); emitMutations.push(emittedMutation);
} }